STORM is a computational framework that jointly designs the contracting portfolio and behind-the-meter generation and storage for large electricity users in the Argentine Wholesale Market (MEM) — pricing volume, power-adequacy, and tail risk as first-class decisions rather than after-the-fact accounting.
STORM is a two-stage stochastic mixed-integer linear program. The first stage locks in monthly contract volumes QEk,m, power-adequacy coverage RPm, and installed capacity CPV, CBESS, PBESS before uncertainty is revealed. The second stage dispatches energy, storage, and demand response per scenario across S realizations of demand, spot price, PV yield, and tariff parameters.
Inputs are stochastic objects, not point forecasts. First-stage variables cannot adapt to a particular realization — they must hedge across the full envelope.
Under Resolución SE 400/2025 the procurement choice reopens for large industrial users. STORM models the cost stack the way the regulator records it — separating energy from power-adequacy, grid services, transport, distribution peaje, and local adders — so contracts and behind-the-meter assets are sized against the exposures they actually hedge.
Renewable energy contract. Bilateral MWh from wind, PV, biomass — long duration, profile-fit, counterparty exposure.
Energy contract with conventional, hydro, or renewable generators. Monthly take-or-pay / deliver-or-pay commitments.
Power-adequacy hedge tied to CAMMESA peak-hour requirements. Priced per MW of reserved peak, not per MWh.
Residual energy and power settled after contracts and self-supply. PPAD activates only when peak-hour exposure becomes expensive.
Inputs define an uncertainty envelope. First-stage decisions are fixed before uncertainty resolves. Second-stage recourse adapts the operation by scenario. The objective penalizes the conditional tail of OPEX via CVaR.
STORM is implemented in Python on top of Gurobi. The first-stage variables are declared outside the scenario loop so non-anticipativity is enforced structurally. The second stage indexes everything by (t, s) over T = 35,040 intervals and S scenarios — solved at root with a reported MIP gap of zero in the headline campaign.
import gurobipy as gp from gurobipy import GRB from storm import config as cfg from storm.scenario_gen import build_scenarios # 12 scenarios × 35,040 intervals (15-min, 365 d) scenarios = build_scenarios( demand = load_case("case3"), horizon = cfg.YEAR_HOURS, interval = cfg.DELTA_T, n = 12, seed = 2026, ) m = gp.Model("storm") # ── First stage: here-and-now ── Q_MATER = m.addVars(M, lb=0, name="Q_MATER") Q_MATE = m.addVars(M, lb=0, name="Q_MATE") R_MATP = m.addVars(M, lb=0, name="R_MATP") C_PV = m.addVar(lb=0, name="C_PV") C_BESS = m.addVar(lb=0, name="C_BESS") P_BESS = m.addVar(lb=0, name="P_BESS") # ── Second stage: per-scenario recourse ── e_spot = m.addVars(T, S, lb=0) e_con = m.addVars(K, T, S, lb=0) E_BESS = m.addVars(T, S, lb=0) d_red = m.addVars(T, S, lb=0) r_spotP = m.addVars(M, S, lb=0) y = m.addVars(T, S, vtype=GRB.BINARY) # 1 if charging # Objective: annualized CAPEX + E[OPEX] + β · CVaR_α(OPEX) capex = γ_PV*C_PV + γ_BESS*C_BESS + γ_P*P_BESS opex = (1/S) * gp.quicksum(opex_s[s] for s in S_set) cvar = η + (1/((1-α)*S)) * gp.quicksum(ζ[s] for s in S_set) m.setObjective(capex + opex + β*cvar, GRB.MINIMIZE) m.optimize() # MIP gap target 2% · reported 0.00%
Source: full-year Case 3 campaign. Increasing β shifts the first-stage portfolio toward physical hedges (PV, BESS) and reduces MATE energy commitments and MATP coverage.
Based on the full-year Case 3 campaign results. Each data point is an exact Gurobi solve; intermediate values are interpolated so you can explore the sensitivity surface continuously.
PV self-consumption + BESS discharge as % of total supply.
The objective has three layers: annualized capital cost on first-stage assets, the expected operating cost across scenarios, and the conditional value-at-risk of the OPEX tail. CAPEX does not affect the ordering of scenarios — only the OPEX distribution enters the tail-risk term.
β controls risk aversion. β = 0 yields STORM-RN, the risk-neutral expected-cost optimizer. Larger β shifts the portfolio toward conservative hedges — defining STORM-CVaR.
Rockafellar–Uryasev representation. With α = 0.95 the term penalizes the worst 5% of scenario OPEX outcomes, independent of CAPEX.
Supply from grid spot, MEM contracts, PV self-consumption, BESS discharge, and curtailed demand must equal native load plus charging energy at every (t, s).
Storage operating bounds are linear in the first-stage energy capacity. A linear discharge-throughput penalty captures the first-order economic cost of cycling.
Chargeable peak power is the maximum grid import during the regulated peak-hour window. MATP coverage absorbs it; the remainder is paid at the residual PPAD rate.
Unilateral departure from the monthly commitment is priced at the take-or-pay penalty cTOPk · δTOP. Setting cTOP=0 recovers the pure commitment-cost formulation.
Evaluated on the same 12-scenario fan, STORM beats every single-channel ablation: contracts-only and DER-only are both materially worse than co-optimizing the two. CVaR-aware first-stage decisions trade a modest expected-cost premium for a measurably tighter tail.
kUSD / year · expected total cost (dark) and empirical CVaR95% (cyan band). Bars drawn from Table II of the paper.
| Strategy | E[total] | CVaR95 | PV | BESS |
|---|---|---|---|---|
| GUDI full service | 779.7 | 779.9 | 0.00 | 0.000 |
| Deterministic EV | 520.7 | 521.7 | 1.86 | 0.000 |
| STORM-RN (β=0) | 520.4 | 520.7 | 1.86 | 0.000 |
| STORM-CVaR (β=0.5) | 525.4 | 525.6 | 2.29 | 0.000 |
| Contracts-only | 626.7 | 626.7 | 0.00 | 0.000 |
| DER-only | 551.0 | 551.2 | 2.33 | 0.000 |
| STORM-CVaR · no deg. | 525.5 | 525.8 | 2.29 | 0.006 |
Costs in kUSD/year. Capacities in MWp (PV) and MWh (BESS). MIP gap = 0% on all non-GUDI runs.
The same two-stage structure generalizes well beyond the Case 3 logistics center: any industrial-scale procurement decision exposed to layered regulatory cost stacks and operational uncertainty admits the same framing.
Migration analysis from GUDI to GUMA / GUME for users with mixed schedules and seasonal load profiles.
Joint sizing of behind-the-meter generation and storage against price, demand, and irradiance scenarios.
Optimal monthly volume across MATER, MATE, MATP — with take-or-pay structures explicit in the formulation.
CVaR-aware first-stage decisions for buyers required to demonstrate cost-stability under regulatory volatility.
Quantify when BESS substitutes for MATP power coverage as a function of installed storage cost.
Value voluntary load reduction under SE 379/2025 caps against full-year scenario distributions.
Side-by-side baseline vs. post-Resolution scenarios — measure migration value beyond aggregate tariff numbers.
Plug in any source of structured uncertainty — hydrology, fuel, FSA transition path — without rewriting the model.
STORM can be adopted at different levels of depth: from licensing the optimization model and integrating it with internal tools, to using the team as a technical partner for procurement studies, scenario design, and decision support.
Access to the STORM formulation, scenario structure, data templates, and reproducible optimization workflow for internal studies and recurring analyses.
Integration with Python pipelines, notebooks, dashboards, APIs, Gurobi environments, and existing data workflows used by technical teams.
A practical spreadsheet interface for organizations that need structured inputs, auditable assumptions, and exportable results before adopting a full Python stack.
One-off or periodic studies for contract portfolios, PV+BESS sizing, GUDI/GUMA/GUME migration, PPAD exposure, and CVaR-based risk analysis.
Scenario updates, model runs, sensitivity analysis, and executive reporting delivered as a recurring analytical service for management teams.
Decision memos, reproducible notebooks, Excel workbooks, scenario reports, investment recommendations, and technical appendices for auditability.
A two-stage stochastic MILP for procurement and DER sizing under the Argentine MEM normalization process — open implementation, reproducible campaign, anonymized for review.
Paper available on request · preprint coming after peer review.