2021 · Problem A — Storing the Sun
Optimization Energy storage LCOE / NPV Time seriesThe prompt, restated
A utility is building a 100 MW utility-scale solar PV plant in the desert Southwest and wants to pair it with on-site energy storage so that the combined plant can serve a more predictable load profile — in particular, push significant output into the post-sunset evening peak rather than dumping all the production at noon. The utility is choosing between three storage families (lithium-ion batteries, pumped-hydro storage, and concentrated-solar thermal salt storage), each with very different round-trip efficiencies, depth-of-discharge limits, cost curves, and lifetimes.
The team is asked to (1) develop a model for the daily energy balance between the PV plant, storage, and a stylized load profile, (2) recommend a storage technology and size (MWh) for the chosen site, (3) extend the model to weekly and seasonal scales — solar irradiance in December in Phoenix is roughly half what it is in June — and (4) write a one-page memo to the utility's chief operating officer recommending a build plan, including a sensitivity statement about what breaks the recommendation. The model is expected to use real solar resource data (typical meteorological year, TMY3) and real cost data (NREL ATB or Lazard LCOS).
Key modeling idea
This is fundamentally a shifted-supply matching problem: hour-by-hour PV generation $P_{\text{pv}}(t)$ must be reshaped into a delivered profile $P_{\text{out}}(t)$ that tracks demand, with storage absorbing the surplus during midday and discharging it during the evening peak. The decision variables are storage power rating ($P_s$, MW), storage energy capacity ($E_s$, MWh), and a dispatch schedule. The objective is minimum levelized cost of storage (LCOS) — or equivalently maximum NPV — subject to the energy balance and physical constraints.
Suggested approach
- Step 1 — Build the supply trace. Pull a TMY3 hourly irradiance file for your chosen site; convert to AC output with a standard PVWatts-style derate. Plot one summer day and one winter day side-by-side so you can see the shape.
- Step 2 — Define the target load shape. Either use a stylized "evening peak" block (e.g., 80 MW × 4 h after sunset) or a CAISO duck-curve net-load proxy.
- Step 3 — Formulate a linear program (see Optimization): variables are charge/discharge per hour and state-of-charge; constraints are SOC dynamics, power-rating limits, depth-of-discharge limits, and round-trip efficiency. Solve with PuLP for one representative week per season.
- Step 4 — Run an outer sweep over $(P_s, E_s)$ and compute LCOS for each candidate technology, then pick the Pareto-front design.
- Step 5 — Stress test with Monte Carlo (technique 10) over irradiance noise, cost uncertainty, and a low-resource winter week.
Data sources to consider
| Source | What you get |
|---|---|
| NREL National Solar Radiation Database (NSRDB) | Hourly GHI/DNI/DHI for any US site, 1998–present |
| NREL System Advisor Model (SAM) | Reference PV plant performance curves and degradation |
| NREL Annual Technology Baseline (ATB) | Capex / opex / LCOS by year for Li-ion, PHS, CSP |
| Lazard LCOS report | Independent storage cost benchmarks |
| EIA Form 860 / 923 | Real utility-scale plant performance records |
| CAISO OASIS | Real net-load (duck curve) traces for a reference grid |
Common pitfalls and judge commentary patterns
- Ignoring round-trip efficiency. Li-ion is ~88%, PHS ~78%, CSP thermal ~40%. This dominates the answer; teams that skip it score poorly.
- Sizing only for the average day. The binding constraint is a low-irradiance week, not the mean. Judges flag teams who never tested December.
- Treating "100 MW solar" as 100 MW × 24 h. Capacity factor for desert PV is roughly 26–30%; failing to apply it produces wildly oversized storage.
- No degradation. Li-ion loses ~2% capacity/year; over a 20-year horizon this matters for LCOS.
- Recommendation without a sensitivity table. Top papers pair every number with a one-paragraph "what if costs fall 30%" alternative.
Python sketch
A minimal LP dispatch for one representative day, using PuLP. Extend to 8760 hours for the real submission.
import numpy as np
import pulp
# --- inputs (illustrative) ---
H = 24
pv = np.array([0,0,0,0,0,0, 5,20,45,70,85,92, 95,92,82,65,40,15, 3,0,0,0,0,0]) # MW
load = np.array([20,18,16,15,15,18, 25,40,55,60,55,50, 45,42,40,45,55,80, 90,85,70,55,40,28]) # MW
eta_c, eta_d = 0.94, 0.94 # one-way efficiencies (Li-ion)
SOC_max = 400 # MWh capacity (decision variable in outer loop)
P_rate = 80 # MW power rating
SOC0 = 0.5 * SOC_max
m = pulp.LpProblem("dispatch", pulp.LpMinimize)
c = [pulp.LpVariable(f"c{t}", 0, P_rate) for t in range(H)] # charge MW
d = [pulp.LpVariable(f"d{t}", 0, P_rate) for t in range(H)] # discharge MW
soc = [pulp.LpVariable(f"s{t}", 0.1*SOC_max, SOC_max) for t in range(H)]
unmet = [pulp.LpVariable(f"u{t}", 0) for t in range(H)]
# objective: minimize unmet load (penalty) + small charge cost
m += pulp.lpSum(1000*unmet[t] + 0.01*c[t] for t in range(H))
for t in range(H):
# delivered = pv - charge + discharge; must >= load - unmet
m += pv[t] - c[t] + d[t] + unmet[t] >= load[t]
prev = SOC0 if t == 0 else soc[t-1]
m += soc[t] == prev + eta_c*c[t] - d[t]/eta_d
m.solve(pulp.PULP_CBC_CMD(msg=False))
print("unmet MWh:", sum(u.value() for u in unmet))
Sensitivity & validation checklist
- Re-run sizing for a December week and a July week — does the recommendation hold?
- Vary Li-ion capex ±30% (this is the dominant cost driver).
- Vary discount rate from 4% to 8% (utility WACC bands).
- Compare against a no-storage baseline: how much PV is curtailed?
- Validate that delivered MWh / generated MWh ≈ round-trip efficiency on net-charging days.