Skip to content

LP Formulation

This spec presents the complete stage subproblem LP for the Cobre SDDP solver: the objective function with its cost taxonomy, all constraint families, slack/penalty variables, and the Benders cut interface to the future cost function. It uses the parallel blocks formulation by default.

Reading order: SDDP algorithmsystem elementsthis specequipment formulations

For what each physical element represents and its decision variables, see system elements. For variable naming conventions and index sets, see notation conventions.

The objective function includes three categories of penalty/cost terms plus resource costs. This taxonomy aligns with Penalty System. Understanding these categories is essential for setting appropriate parameter values and interpreting solution reports.

1.1 Resource Costs (Actual Operating Expenses)

Section titled “1.1 Resource Costs (Actual Operating Expenses)”

Resource costs represent actual generation or contractual expenditures:

CostSymbolUnitsObjective Term
Thermal generationcj,sthc^{th}_{j,s}$/MWhj,k,sτkcj,sthgj,k,s\sum_{j,k,s} \tau_k \cdot c^{th}_{j,s} \cdot g_{j,k,s}
Contract dispatchccctrc^{ctr}_c$/MWhc,kτkccctrχc,k\sum_{c,k} \tau_k \cdot c^{ctr}_c \cdot \chi_{c,k}

Contract prices are positive for imports (cost) and negative for exports (revenue), so a single summation naturally handles both directions. See system elements §8 for the unidirectional contract model.

1.2 Category 1: Recourse Slacks (LP Feasibility)

Section titled “1.2 Category 1: Recourse Slacks (LP Feasibility)”

These ensure the SDDP algorithm has relatively complete recourse — every subproblem must be feasible regardless of scenario realization:

PenaltySymbolUnitsPurpose
Deficitcb,sdefc^{def}_{b,s}$/MWhValue of unserved energy (piecewise)
Excess generationcbexcc^{exc}_b$/MWhAbsorb uncontrollable surplus

1.3 Category 2: Constraint Violation Penalties (Policy Shaping)

Section titled “1.3 Category 2: Constraint Violation Penalties (Policy Shaping)”

These provide slack for physical or operational constraints that may be impossible to satisfy under extreme conditions. Their cost must be high enough to affect the value function in earlier stages:

PenaltySymbolUnitsViolated Constraint
Storage below minimumchsvc^{sv-}_h$/hm³vhVhv_h \geq \underline{V}_h
Filling target shortfallchfillc^{fill}_h$/hm³vhVttargetv_h \geq V^{\text{target}}_t (per-stage filling floor)
Turbined flow minimumchtvc^{tv-}_h$/(m³/s·h)qh,kQhq_{h,k} \geq \underline{Q}_h
Outflow minimumchovc^{ov-}_h$/(m³/s·h)oh,kOho_{h,k} \geq \underline{O}_h
Outflow maximumchov+c^{ov+}_h$/(m³/s·h)oh,kOˉho_{h,k} \leq \bar{O}_h
Generation minimumchgvc^{gv-}_h$/MWhgh,kGhg_{h,k} \geq \underline{G}_h
Evaporation violationchevc^{ev}_h$/(m³/s·h)Evaporation within physical limits
Withdrawal violationchwvc^{wv}_h$/(m³/s·h)Water withdrawal commitment (bidirectional: under/over)

1.4 Category 3: Regularization Costs (Solution Guidance)

Section titled “1.4 Category 3: Regularization Costs (Solution Guidance)”

Small costs that guide the solver toward physically preferred solutions when the LP would otherwise be indifferent. Must be orders of magnitude smaller than any economic cost:

CostSymbolUnitsPurpose
Spillagechspillc^{spill}_h$/(m³/s·h)Prefer turbining over spilling when indifferent
FPHA turbined flowchfphac^{fpha}_h$/(m³/s·h)Prevent interior FPHA solutions (FPHA-only)
Diversionchdivc^{div}_h$/(m³/s·h)Prefer main channel flow
Curtailmentcrcurtc^{curt}_r$/MWhPrioritize using available NCS generation
Exchangecexchc^{exch}_\ell$/MWhPrevent unnecessary power flows

The following ordering must be maintained (from highest to lowest):

csv>cdef>ctv,cov±,cgv,cev,cwv>cth,cctr>cspill,cfpha,cdiv,ccurt,cexchc^{sv-} > c^{def} > c^{tv-}, c^{ov\pm}, c^{gv-}, c^{ev}, c^{wv} > c^{th}, c^{ctr} > c^{spill}, c^{fpha}, c^{div}, c^{curt}, c^{exch}

with the filling-target penalty pinned below deficit on a separate rung: cdef>cfillc^{def} > c^{fill}.

  1. Storage violation (csvc^{sv-}): Highest penalty — reservoir below dead volume risks dam safety, so it must exceed deficit
  2. Deficit (cdefc^{def}): Value of lost load; exceeds any generation cost
  3. Constraint violations (ctvc^{tv-}, cov±c^{ov\pm}, cgvc^{gv-}, cevc^{ev}, cwvc^{wv}): Exceed typical marginal cost but allow violation when physically necessary
  4. Resource costs (cthc^{th}, cctrc^{ctr}): Market-based or fuel-based
  5. Regularization (cspillc^{spill}, cfphac^{fpha}, cdivc^{div}, ccurtc^{curt}, cexchc^{exch}): Near-zero
  6. Filling target (cfillc^{fill}): Pinned below deficit — a commissioning fill schedule is not defended as hard as load serving. Its position relative to the operational-constraint tier (item 3) is left to study calibration.

For the full penalty specification, cascade resolution, and stage-varying overrides, see Penalty System.

The complete stage objective is:

min  Cresourcethermal, contracts+Crecoursedeficit, excess+Cviolationconstraint slacks+Cregularizationspillage, exchange, ...+θ\min \; \underbrace{C^{resource}}_{\text{thermal, contracts}} + \underbrace{C^{recourse}}_{\text{deficit, excess}} + \underbrace{C^{violation}}_{\text{constraint slacks}} + \underbrace{C^{regularization}}_{\text{spillage, exchange, ...}} + \theta

where each component is summed over blocks with appropriate time weighting:

Ccomponent=kKτk(cost terms for component)C^{component} = \sum_{k \in \mathcal{K}} \tau_k \cdot (\text{cost terms for component}) minkKτk[jTscj,sthgj,k,sThermal cost+cCccctrχc,kContract cost\min \sum_{k \in \mathcal{K}} \tau_k \Bigg[ \underbrace{\sum_{j \in \mathcal{T}} \sum_s c^{th}_{j,s} g_{j,k,s}}_{\text{Thermal cost}} + \underbrace{\sum_{c \in \mathcal{C}} c^{ctr}_c \chi_{c,k}}_{\text{Contract cost}} +bBsSbcb,sdefδb,k,sDeficit (piecewise)+bBcbexcϵb,kExcess + \underbrace{\sum_{b \in \mathcal{B}} \sum_{s \in \mathcal{S}_b} c^{def}_{b,s} \delta_{b,k,s}}_{\text{Deficit (piecewise)}} + \underbrace{\sum_{b \in \mathcal{B}} c^{exc}_b \epsilon_{b,k}}_{\text{Excess}} +hHchspillsh,k+hHfphachfphaqh,k+hHchdivuh,kHydro regularization + \underbrace{\sum_{h \in \mathcal{H}} c^{spill}_h s_{h,k} + \sum_{h \in \mathcal{H}^{fpha}} c^{fpha}_h q_{h,k} + \sum_{h \in \mathcal{H}} c^{div}_h u_{h,k}}_{\text{Hydro regularization}} +rRcrcurt(Argr,knc)Curtailment (regularization)+lLclexch(fl,k++fl,k)Exchange (regularization) + \underbrace{\sum_{r \in \mathcal{R}} c^{curt}_r (A_r - g^{nc}_{r,k})}_{\text{Curtailment (regularization)}} + \underbrace{\sum_{l \in \mathcal{L}} c^{exch}_l (f^+_{l,k} + f^-_{l,k})}_{\text{Exchange (regularization)}} +Constraint violation penaltiesSee §9] + \underbrace{\text{Constraint violation penalties}}_{\text{See §9}} \Bigg] +hH[chsvσhv+chfillσhfill]Storage violations (not per-block)+  θ+ \underbrace{\sum_{h \in \mathcal{H}} \Big[ c^{sv-}_h \sigma^{v-}_h + c^{fill}_h \sigma^{fill}_h \Big]}_{\text{Storage violations (not per-block)}} + \; \theta

For each bus bBb \in \mathcal{B} and block kKk \in \mathcal{K}:

hHbgh,k+jTbsgj,k,s+rRbgr,knc+cCbimpχc,k\sum_{h \in \mathcal{H}_b} g_{h,k} + \sum_{j \in \mathcal{T}_b} \sum_s g_{j,k,s} + \sum_{r \in \mathcal{R}_b} g^{nc}_{r,k} + \sum_{c \in \mathcal{C}^{imp}_b} \chi_{c,k} +l:target=bηlfl,k++l:source=bηlfl,k+ \sum_{l: \text{target}=b} \eta_l f^+_{l,k} + \sum_{l: \text{source}=b} \eta_l f^-_{l,k} l:source=bfl,k+l:target=bfl,kcCbexpχc,kjPbγjpj,k+sSbδb,k,sϵb,k=Db,k- \sum_{l: \text{source}=b} f^+_{l,k} - \sum_{l: \text{target}=b} f^-_{l,k} - \sum_{c \in \mathcal{C}^{exp}_b} \chi_{c,k} - \sum_{j \in \mathcal{P}_b} \gamma_j p_{j,k} + \sum_{s \in \mathcal{S}_b} \delta_{b,k,s} - \epsilon_{b,k} = D_{b,k}

Dual variable: πb,klb\pi^{lb}_{b,k} (marginal cost of energy at bus bb, block kk, in $/MW; divide by τk\tau_k for $/MWh — see Variable Units Convention)

For the physical meaning of each element in the balance, see system elements.

For each hydro hHh \in \mathcal{H} (parallel blocks formulation):

vh=vhin+ζ[ah+kKwk(iUh(qi,k+si,k+ui,k)Inflow from upstream+i:div=hui,kDiverted inflow+j:dest=hpj,kPumped inflowv_h = v^{in}_h + \zeta \Bigg[ a_h + \sum_{k \in \mathcal{K}} w_k \Big( \underbrace{\sum_{i \in \mathcal{U}_h} (q_{i,k} + s_{i,k} + u_{i,k})}_{\text{Inflow from upstream}} + \underbrace{\sum_{i: \text{div}=h} u_{i,k}}_{\text{Diverted inflow}} + \underbrace{\sum_{j: \text{dest}=h} p_{j,k}}_{\text{Pumped inflow}} qh,ksh,kuh,kOutflowseh,kEvaporationj:src=hpj,kPumped outflow)rhWithdrawal (signed target)] - \underbrace{q_{h,k} - s_{h,k} - u_{h,k}}_{\text{Outflows}} - \underbrace{e_{h,k}}_{\text{Evaporation}} - \underbrace{\sum_{j: \text{src}=h} p_{j,k}}_{\text{Pumped outflow}} \Big) - \underbrace{r_h}_{\text{Withdrawal (signed target)}} \Bigg]

where:

  • vhinv^{in}_h = incoming storage LP variable, pinned to the previous stage’s value via column bounds (§4a)
  • aha_h = incremental inflow (from AR model, see PAR(p) inflow model); equivalently zhz_h from the z-inflow constraint (§5b)
  • rhr_h = water withdrawal target (m³/s), a signed fixed RHS parameter from water_withdrawal_m3s (not a per-block LP decision variable). rh>0r_h > 0 schedules a consumptive removal; rh<0r_h < 0 schedules an inter-basin return/addition that adds water to the reservoir. The realized withdrawal removed from the reservoir is Rh=rhσhw+σhw+R_h = r_h - \sigma^{w-}_h + \sigma^{w+}_h; see §9 for the slack bounds that prevent the realized flow from flipping sign. Withdrawal is applied at the stage level, outside the per-block summation
  • eh,ke_{h,k} = signed net evaporation flow (m³/s): positive values represent net evaporative loss subtracted from storage; negative values represent net rainfall input on the lake surface that adds to storage through the same coefficient (the leading - sign on eh,ke_{h,k} flips the contribution automatically — see penalty system §5)
  • wk=τk/jτjw_k = \tau_k / \sum_j \tau_j = block weight
  • ζ=0.0036×kτk\zeta = 0.0036 \times \sum_k \tau_k = time conversion factor

Dual variable: πhwb\pi^{wb}_h (water value — captures the marginal value of incoming storage as seen through the hydro balance, but is not used directly as a cut coefficient; the cut coefficient comes from the reduced cost of the pinned incoming-storage column, see §4a and cut management)

The water balance (§4), FPHA hyperplanes (§6), and generic constraints (§10) all involve the incoming storage value v^h\hat{v}_h. Rather than embedding v^h\hat{v}_h as a constant in the RHS of each of these constraints (which would require collecting duals from all of them to compute cut coefficients), Cobre introduces an explicit incoming storage LP variable vhinv^{in}_h that every such constraint references, and pins it to the trial value.

For each hydro hHh \in \mathcal{H}, the incoming-storage column (storage_in, §4b) is pinned by setting equal lower and upper column bounds:

vhin=vˉhin=v^h\underline{v}^{in}_h = \bar{v}^{in}_h = \hat{v}_h

where:

  • vhinv^{in}_h = LP variable representing the incoming storage for hydro hh
  • v^h\hat{v}_h = incoming state value (end-of-stage storage from the previous stage), written into both bounds per scenario via bound patching

The variable vhinv^{in}_h then appears as an LP variable (not a constant) in all constraints that depend on incoming storage: the water balance (§4), the FPHA average storage computation (§6), and any generic constraints (§10) that reference incoming storage.

Cut coefficient: the storage cut coefficient πhv\pi^v_h is the reduced cost of the pinned storage_in column (unscaled by its prescaler column factor — see §12 and cut management). No fixing-constraint dual is involved.

Why this design: By LP duality, when a column is pinned at x=xˉ\underline{x} = \bar{x} its reduced cost equals the sensitivity Qt/v^h\partial Q_t / \partial \hat{v}_h of the optimal value to the pinned bound — exactly the multiplier the equivalent equality row vhin=v^hv^{in}_h = \hat{v}_h would have carried (KKT parity). This sensitivity automatically accounts for all downstream effects through water balance, FPHA, and generic constraints, so a single reduced-cost value suffices — no combination of duals from multiple constraint types is needed. This is the same “fishing” technique used by SDDP.jl, realised through column bounds rather than a fixing row, and is analogous to how the AR lags (section 5a) are pinned for inflow history.

Column count: NN pinned incoming-storage columns, where N=HN = |\mathcal{H}| is the number of operating hydros. There is no corresponding fixing-row block.

LP column layout — state variables (storage, AR lags) first for contiguous reduced-cost extraction, dispatch variables per block, and θ (future cost) last for the Benders cuts, assembled with the constraint-row families below.

Decision variables — contiguous column orderConstraint rowsState (coupling)Dispatch (per block k, stage-local)FutureLoad balance — per bus, per blockWater balance — per hydro, per blockFixing constraints — state coupling, duals → πBenders cuts: θ ≥ α + πᵀxvₕ — storageaₕ,ℓ — AR lagsduals → cut coefficients πfₗ,ₖ — flowqₕ,ₖ — hydro genqᶠₕ,ₖ — turbinedsₕ,ₖ — spillgⱼ,ₖ — thermalrₙ,ₖ — NCSδᵦ,ₖ,ₛ — deficitθ — future costcut constraints assembled into stage LP

The stage LP uses a fixed column and row layout that places state variables first, followed by auxiliary and equipment columns. State is pinned by column bounds on the incoming-state columns (§4a, §5a, §5c), and cut coefficients are read as the reduced costs of those columns — so the fixed column order, not a fixed row order, is what enables contiguous coefficient extraction. With N=HN = |\mathcal{H}| hydros, LL = maximum AR order, AA = number of anticipated thermals, and K=Kmax=maxiKiK = K_{\max} = \max_i K_i:

Column layout:

RegionCountDescription
storageNNOutgoing storage volumes (state) — first
inflow_lagsNLNLAR lag variables (state) — after storage
anticipated_stateKAK ARing-buffer slots for anticipated thermals (state, slot-major plant-minor)
z_inflowNNRealized inflow (auxiliary, not state) — after the state block
storage_inNNIncoming storage volumes (auxiliary, for §4a) — after z-inflow
theta11Future cost variable — last of the state prefix

Equipment columns (turbine, spillage, diversion, thermal, anticipated-decision and anticipated-state-out, line flows, deficit, excess, slacks) follow immediately after theta. Two auxiliary blocks are reserved for anticipated thermals: AA anticipated-decision columns dtid^i_t carrying the commitment placed at this stage for delivery KiK_i stages later, and AA anticipated-state-out columns ytiy^i_t used to decouple the post-shift state from the decision-write coefficient — see §5c.

The z_inflow region holds one free column per hydro representing the total realized inflow zh=ahz_h = a_h (m³/s) for each hydro at the current stage. These are auxiliary columns (zero objective cost, unbounded) whose primal values after solving give the realized inflow. They participate in the water balance (§4) and are defined by the z-inflow constraints (§5b).

Row layout (equality-constraint prefix):

Because state is pinned by column bounds (§4a, §5a, §5c) rather than by equality rows, there are no state-fixing rows: the former storage_fixing (NN), lag_fixing (NLNL), and anticipated_state_fixing (KAKA) row blocks are permanent empty sentinels (zero rows). The equality-constraint prefix therefore begins directly with the z-inflow definitions:

RegionCountDescription
z_inflowNNRealized-inflow definition constraints (§5b) — first equality block

Equipment rows (water balance, load balance, FPHA, evaporation, outflow bounds, anticipated-fishing and anticipated-state-out equalities, generic constraints, etc.) follow after the z-inflow rows.

Cut coefficients are not read from a contiguous dual slice over a fixing-row prefix. Instead, each incoming-state coordinate is pinned on its own LP column — storage_in for storage, inflow_lags for AR lags, anticipated_state for anticipated-thermal slots — and its cut coefficient is the reduced cost of that column (§4a, cut management). The map from a state coordinate to its pinned column is fixed (state_to_lp_incoming_column), and each of these incoming-state column regions is contiguous, so all storage, inflow-lag, and anticipated-state coefficients are still gathered by reading a few contiguous slices — of the reduced-cost vector rather than the dual vector.

Worked example (N=3N = 3, L=2L = 2, A=0A = 0): the storage region holds 3 columns, the AR lag region holds 6 (3 hydros × 2 lags), the z-inflow region holds 3, and the incoming-storage region holds 3, so theta is the 16th column. The state count (outgoing storage + AR lags) is N(1+L)=9N(1 + L) = 9.

The incremental inflow aha_h is determined by the PAR(p) autoregressive model:

ah=(μt=1Phψμt)deterministic base+=1Phψah,lag contribution+σtηtstochastic innovationa_h = \underbrace{\left( \mu_t - \sum_{\ell=1}^{P_h} \psi_\ell \mu_{t-\ell} \right)}_{\text{deterministic base}} + \underbrace{\sum_{\ell=1}^{P_h} \psi_\ell \cdot a_{h,\ell}}_{\text{lag contribution}} + \underbrace{\sigma_t \cdot \eta_t}_{\text{stochastic innovation}}

To maintain the Markov property, lagged inflows ah,a_{h,\ell} are promoted to state variables pinned by column bounds — see section 5a below.

See PAR(p) inflow model for the complete PAR(p) model specification.

The AR dynamics equation (section 5) uses lagged inflows ah,a_{h,\ell} as LP variables. To maintain the Markov property in the SDDP decomposition, each lag variable is pinned to its incoming state value via equal lower and upper column bounds on the inflow_lags column. This binds the lag variables to the known incoming state, and the reduced cost of each pinned column provides the cut coefficient πh,lag\pi^{lag}_{h,\ell} for the corresponding inflow-lag dimension of the Benders cuts (section 11).

For each hydro hHh \in \mathcal{H} and each lag {0,,L1}\ell \in \{0, \ldots, L-1\}:

ah,=aˉh,=a^h,\underline{a}_{h,\ell} = \bar{a}_{h,\ell} = \hat{a}_{h,\ell}

where:

  • ah,a_{h,\ell} = LP variable representing the inflow at lag \ell for hydro hh
  • a^h,\hat{a}_{h,\ell} = incoming state value (inflow observation from \ell stages ago), written into both bounds via bound patching
  • LL = maximum AR order across all hydros (uniform lag storage convention)

Column count: N×LN \times L pinned lag columns, where N=HN = |\mathcal{H}| is the number of operating hydros and LL is the system-wide maximum lag. All hydros store LL lags regardless of their individual AR order PhP_h; hydros with Ph<LP_h < L have zero-valued AR coefficients (ψ=0\psi_\ell = 0 for >Ph\ell > P_h) in the dynamics equation, but their lag columns are still present and pinned. This uniform layout keeps the inflow_lags columns contiguous, so all lag cut coefficients are read in a single slice of the reduced-cost vector (section 4b). There is no corresponding lag-fixing row block.

Cut coefficient: πh,lag\pi^{lag}_{h,\ell} (marginal value of inflow history at lag \ell for hydro hh) is the reduced cost of the pinned inflow_lags column, unscaled by its prescaler column factor (§12) — see cut management.

5b. Realized-Inflow Definition Constraints (z-inflow)

Section titled “5b. Realized-Inflow Definition Constraints (z-inflow)”

For each hydro hHh \in \mathcal{H}, the LP includes an auxiliary variable zhz_h representing the total realized inflow (m³/s) at the current stage. These variables are defined by equality constraints that combine the deterministic base, lag contributions, and stochastic noise:

zh=bh,m(t)+=1Phψm(t),ah,+σm(t)ηtz_h = b_{h,m(t)} + \sum_{\ell=1}^{P_h} \psi_{m(t),\ell} \cdot a_{h,\ell} + \sigma_{m(t)} \cdot \eta_t

where:

  • zhz_h = LP variable representing the realized inflow for hydro hh (free column, zero cost)
  • bh,m(t)b_{h,m(t)} = deterministic base (precomputed from seasonal means and AR coefficients — see PAR(p) model §7.4)
  • ψm(t),\psi_{m(t),\ell} = original-unit AR coefficients (constraint matrix entries, set once at LP construction)
  • ah,a_{h,\ell} = LP variables for lagged inflows (state variables, fixed by §5a)
  • σm(t)ηt\sigma_{m(t)} \cdot \eta_t = noise innovation (patched into the constraint RHS per scenario)

The z-inflow variable zhz_h then enters the water balance constraint (§4) in place of the raw inflow term aha_h, and its primal value after solving gives the realized inflow for reporting and simulation extraction.

The z-inflow columns sit between the AR lag columns and the incoming storage columns in the column layout (section 4b). Because state is pinned by column bounds rather than fixing rows, their constraint rows form the first equality block (section 4b). The RHS is patched per scenario with bh,m(t)+σm(t)ηtb_{h,m(t)} + \sigma_{m(t)} \cdot \eta_t, where ηt\eta_t is the effective noise (possibly clamped for inflow non-negativity — see Inflow Non-Negativity). These are not state variables and do not contribute to cut coefficients.

Constraint count: NN total constraints, where N=HN = |\mathcal{H}| is the number of operating hydros. See section 4b for the row layout.

Anticipated thermals (see System Elements §4) introduce a per-plant ring buffer of KiK_i pending commitments and a per-stage commitment column. The incoming ring-buffer state is pinned by column bounds (like all other state, §4a); two constraint blocks then couple the remaining variables. The layout is engineered so that the reduced cost on slot 0 of the pinned anticipated-state column at stage t+1t + 1 propagates back to the predecessor’s commitment column via the standard SDDP cut machinery without any decision-side coefficient corrupting the routing.

State pinning (column bounds, one per (slot, plant))

Section titled “State pinning (column bounds, one per (slot, plant))”

For each plant i{0,,A1}i \in \{0, \ldots, A - 1\} and slot s{0,,K1}s \in \{0, \ldots, K - 1\}, the anticipated-state slot column is pinned by equal column bounds:

xs,i,ta  =  xˉs,i,ta  =  x^s,i,ta\underline{x}^{\mathrm{a}}_{s, i, t} \;=\; \bar{x}^{\mathrm{a}}_{s, i, t} \;=\; \widehat{x}^{\mathrm{a}}_{s, i, t}

The value x^s,i,ta\widehat{x}^{\mathrm{a}}_{s, i, t} is the incoming state from the previous stage’s ring-buffer shift (or, at t=0t = 0, the seed past_anticipated_commitments[i].values_mw[s]). The slot is pinned by its column bounds alone; no decision-write coefficient appears anywhere on the slot column. The cut subgradient with respect to the incoming-state coordinate, Qt/x^s,i,ta\partial Q_t / \partial \widehat{x}^{\mathrm{a}}_{s, i, t}, is the reduced cost of the pinned slot column (§4a). Padding slots sKis \geq K_i are pinned to zero by the same bounds; their reduced cost is zero because the slot carries no information.

Fishing equality (one row per anticipated plant, every stage)

Section titled “Fishing equality (one row per anticipated plant, every stage)”

For each plant ii and every stage t[0,T1]t \in [0, T - 1], the per-block generation of plant ii is bound to the matured commitment in slot 0:

b=0B1hbgi,b,t    Htx0,i,ta  =  0\sum_{b = 0}^{B - 1} h_b \cdot g_{i, b, t} \;-\; H_t \cdot x^{\mathrm{a}}_{0, i, t} \;=\; 0

where hbh_b is the block-bb duration and Ht=bhbH_t = \sum_b h_b. The row is active at every study stage; at t<Kit < K_i the slot-0 value comes from the seed values_mw[t] and the LP cannot freely choose the per-block generation. From tKit \geq K_i onward, slot 0 carries a past LP decision delivered via the ring buffer.

State-out equality (one row per active plant)

Section titled “State-out equality (one row per active plant)”

For each plant ii active at stage tt (i.e., t+Ki<Tt + K_i < T), one auxiliary row pins the anticipated-state-out column ytiy^i_t to the decision dtid^i_t:

yti    dti  =  0y^i_t \;-\; d^i_t \;=\; 0

The ring-buffer shift between stages uses ytiy^i_t — not dtid^i_t — as the value written into slot Ki1K_i - 1 of the next stage’s incoming state. The auxiliary yy column carries zero objective cost and serves only as the “carrier” that decouples the post-shift state from the decision column. Without this decoupling, the decision column dtid^i_t would feed directly into the slot whose pinned reduced cost the next stage’s cut reads back, corrupting the subgradient routing at Ki=1K_i = 1 (slot 0 = slot Ki1K_i - 1 collision).

Two terms enter the objective for each anticipated plant:

i=0A1ci(t+Ki)Ht+Kidt+KiNPVdti    ideliver(t)bci(t)hbdtNPVgi,b,t\sum_{i = 0}^{A - 1} c_i(t + K_i) \cdot H_{t + K_i} \cdot d^{\mathrm{NPV}}_{t + K_i} \cdot d^i_t \;-\; \sum_{i \in \mathrm{deliver}(t)} \sum_b c_i(t) \cdot h_b \cdot d^{\mathrm{NPV}}_t \cdot g_{i, b, t}

The first sum is the commitment cost discounted to the delivery stage t+Kit + K_i. The second sum subtracts the standard per-block thermal cost at every delivery stage so the same MWh is not charged twice — once through the matured commitment and once through the per-block dispatch. Anticipated-state columns and anticipated-state-out columns carry zero objective cost; they are pure carriers of state. In run cost output this commitment term is reported as its own anticipated_thermal_cost category (zero when no anticipated plants are present), distinct from the per-block thermal_cost, so the named cost categories sum to the stage’s immediate cost.

When the backward pass returns a subgradient on slot (s,i)(s, i) of stage t+1t + 1 — read as the reduced cost of that pinned slot column — the cut-row builder maps it to a column in the predecessor’s stage problem as follows:

  • s+1=Kis + 1 = K_i (slot 0 viewed from the next stage equals slot Ki1K_i - 1 viewed from this stage): the coefficient targets the predecessor’s commitment column dtid^i_{t} directly. This is the only branch that fires for Ki=1K_i = 1.
  • s+1<Kis + 1 < K_i: the coefficient targets the predecessor’s outgoing-state slot s+1s + 1, which holds the same commitment one stage earlier in its journey through the ring buffer.
  • sKis \geq K_i (padding): identity remap; the reduced cost is structurally zero so the cut coefficient on the padded slot does not propagate any sensitivity.

The recursion guarantees that, no matter how many stages elapse between commitment and delivery, the marginal cost of a future obligation reaches the original did^i column it should price.

Cobre supports two production models during training, in increasing order of complexity. A third model (linearized head) is available during simulation only — see hydro production models §3. The model can vary by stage or season per hydro.

Constant Productivity Model (for each hydro hHconsth \in \mathcal{H}^{const}, block kk):

gh,k=ρhqh,kg_{h,k} = \rho_h \cdot q_{h,k}

FPHA Model (for each plane mMhm \in \mathcal{M}_h, hydro hHfphah \in \mathcal{H}^{fpha}, block kk):

gh,kγ0m+γvmvhavg+γqmqh,k+γsmsh,kg_{h,k} \leq \gamma^m_0 + \gamma^m_v \cdot v^{avg}_h + \gamma^m_q \cdot q_{h,k} + \gamma^m_s \cdot s_{h,k}

where vhavg=(vhin+vh)/2v^{avg}_h = (v^{in}_h + v_h)/2 is the average storage during the stage, with vhinv^{in}_h being the incoming storage LP variable (§4a) and vhv_h the end-of-stage storage.

Generation Bounds (per hydro hh, block kk):

Ghσh,kggh,kGˉh\underline{G}_h - \sigma^{g-}_{h,k} \leq g_{h,k} \leq \bar{G}_h

Generation bounds are user-defined (not derived from turbined flow). The lower bound is soft (with slack σh,kg\sigma^{g-}_{h,k}); the upper bound is hard. See system elements §5.

For details on the FPHA construction and production function model variants, see hydro production models.

Outflow Definition (per hydro hh, block kk):

oh,k=qh,k+sh,ko_{h,k} = q_{h,k} + s_{h,k}

Outflow Bounds (with slacks for soft enforcement):

Ohσh,kooh,kOˉh+σh,ko+\underline{O}_h - \sigma^{o-}_{h,k} \leq o_{h,k} \leq \bar{O}_h + \sigma^{o+}_{h,k}

8. Variable Bounds and Minimum Constraints

Section titled “8. Variable Bounds and Minimum Constraints”
VhσhvvhVˉh\underline{V}_h - \sigma^{v-}_h \leq v_h \leq \bar{V}_h

The lower bound (dead volume) is soft — the slack σhv\sigma^{v-}_h has a very high penalty above deficit cost. The upper bound (reservoir capacity) is hard; excess water is handled by emergency spillage. During the filling period, this Vh\underline{V}_h lower bound is inactive (storage can be anywhere in [0,Vˉh][0, \bar{V}_h]); the per-stage filling floor below takes its place.

Filling floors (for filling hydros, at every stage t[start_stage_id,entry_stage_id)t \in [\text{start\_stage\_id}, \text{entry\_stage\_id})):

vh+σhfillVttarget,Vttarget=min ⁣(Vt+1targetζt+1ratet+1, Vh),VLtarget=Vhv_h + \sigma^{fill}_h \geq V^{\text{target}}_t, \qquad V^{\text{target}}_t = \min\!\Big( V^{\text{target}}_{t+1} - \zeta_{t+1}\,\text{rate}_{t+1},\ \underline{V}_h \Big), \quad V^{\text{target}}_{L} = \underline{V}_h

The minimum end-of-stage storage VttargetV^{\text{target}}_t ramps up at the configured accumulation rate filling_min_rate_m3s (= ratet\text{rate}_t) and reaches the dead volume Vh\underline{V}_h at the last filling stage L=entry_stage_id1L = \text{entry\_stage\_id} - 1; ζt\zeta_t converts the rate over the stage duration into hm³. The slack σhfill\sigma^{fill}_h is priced at chfillc^{fill}_h, which is pinned below deficit (not the system maximum). This replaces the earlier single terminal constraint at entry_stage_id - 1. See Penalty System §6.

Turbined Flow Bounds (per hydro hh, block kk)

Section titled “Turbined Flow Bounds (per hydro hhh, block kkk)”
Qhσh,kqqh,kQˉh\underline{Q}_h - \sigma^{q-}_{h,k} \leq q_{h,k} \leq \bar{Q}_h

Lower bound is soft; upper bound is hard.

Diversion Flow Bounds (per hydro hh, block kk)

Section titled “Diversion Flow Bounds (per hydro hhh, block kkk)”
0uh,kUˉh0 \leq u_{h,k} \leq \bar{U}_h

Both bounds are hard. Diversion cost is a regularization term (see §1.4), not a violation penalty.

Pumping Flow Bounds (per station jj, block kk)

Section titled “Pumping Flow Bounds (per station jjj, block kkk)”
Pjpj,kPˉj\underline{P}_j \leq p_{j,k} \leq \bar{P}_j

Both bounds are hard.

The per-block constraint violation penalties in the objective (referenced from §2) are:

kKτkhH[chtvσh,kq+chovσh,ko+chov+σh,ko++chgvσh,kg+chev+σh,ke++chevσh,ke]\sum_{k \in \mathcal{K}} \tau_k \sum_{h \in \mathcal{H}} \Big[ c^{tv-}_h \sigma^{q-}_{h,k} + c^{ov-}_h \sigma^{o-}_{h,k} + c^{ov+}_h \sigma^{o+}_{h,k} + c^{gv-}_h \sigma^{g-}_{h,k} + c^{ev+}_h \sigma^{e+}_{h,k} + c^{ev-}_h \sigma^{e-}_{h,k} \Big] +hHTchwv(σhw+σhw+)+ \sum_{h \in \mathcal{H}} T \cdot c^{wv}_h (\sigma^{w-}_h + \sigma^{w+}_h)

where T=kτkT = \sum_k \tau_k is the total stage duration in hours. Withdrawal violation slacks (σhw\sigma^{w-}_h, σhw+\sigma^{w+}_h) are stage-level (not per-block) and bidirectional: σhw\sigma^{w-}_h penalizes under-delivery (the realized withdrawal Rh=rhσhw+σhw+R_h = r_h - \sigma^{w-}_h + \sigma^{w+}_h falls short of the target), and σhw+\sigma^{w+}_h penalizes over-delivery. The withdrawal target rhr_h is signed (§4), and the slack bounds ensure the realized withdrawal cannot flip sign relative to the target:

  • rh>0r_h > 0 (scheduled removal): σhwrh\sigma^{w-}_h \leq r_h (under-delivery slack capped at the target magnitude; floors Rh0R_h \geq 0), σhw+\sigma^{w+}_h unbounded.
  • rh<0r_h < 0 (scheduled inter-basin return/addition): σhw+rh\sigma^{w+}_h \leq |r_h| (over-delivery slack capped at rh|r_h|; caps Rh0R_h \leq 0), σhw\sigma^{w-}_h unbounded.
  • rh=0r_h = 0: both slacks are pinned to zero (presolve-eliminated).

The cap is what keeps the under-delivery slack bounded: were it unbounded, in degenerate cases a run-of-river plant could “un-withdraw” past its target and inject phantom water into the reservoir.

Storage violation penalties (chsvσhvc^{sv-}_h \sigma^{v-}_h and chfillσhfillc^{fill}_h \sigma^{fill}_h) appear outside the τk\tau_k sum because they apply to end-of-stage storage — see §2.

The effective penalty for any (entity, stage, penalty_type) tuple follows a three-level cascade:

  1. Stage-specific override (from Parquet files in constraints/)
  2. Entity-specific override (from entity registry JSON)
  3. Global default (from penalties.json)

For the full resolution semantics and all penalty value definitions, see Penalty System.

User-defined linear constraints (per constraint gGg \in \mathcal{G}):

eγg,exe{,=,}bg\sum_{e} \gamma_{g,e} \cdot x_e \quad \{\leq, =, \geq\} \quad b_g

where xex_e can reference any LP variable using expression syntax:

  • hydro_storage(id), hydro_turbined(id), hydro_spillage(id)
  • thermal_generation(id), bus_deficit(id), etc.

The coefficients γg,e\gamma_{g,e} and the RHS bgb_g may be either literal numeric values or named scalar parameters. A scalar parameter resolves to a single number per stage and can carry one of four kinds:

KindValue semantics
constantOne value for every stage
per_stageExplicit value per stage
seasonalOne value per season; stages inherit the value from their season
computedValue derived from a small expression evaluated at LP-build time (e.g., a fraction of installed capacity)

Resolution happens once at LP-build time, so the LP coefficients are still numeric at solve time — the parameter mechanism does not introduce LP-variable coupling between constraints. Methodology relevance: it lets the corpus express ramping limits, capacity caps, and operator-imposed quotas that vary by stage or season without authoring a separate constraint per stage. Coefficient and RHS values can therefore be stage-varying constants, not just literal numbers.

Generic constraints can have optional slack variables with configurable penalties.

Row materialization: a constraint bound declared with block_id = None over a block-independent expression — one whose every term references a stock variable (incoming storage vhinv^{in}_h, outgoing storage vhv_h, evaporation outflow eh,ke_{h,k} collapsed to its stage average, or an anticipated-thermal commitment) — is materialized as a single stage-level row priced by the total stage hours TT, since per-block rows would be identical. A block_id = None bound on a block-level expression, or any block_id = Some(k) bound, still produces one row per relevant block. This is an LP row-count optimization that is cost- and parity-neutral.

For each active cut ii from previous iterations:

θαi+hHπi,hvvh+h,πi,h,lagah,\theta \geq \alpha_i + \sum_{h \in \mathcal{H}} \pi^v_{i,h} \cdot v_h + \sum_{h,\ell} \pi^{lag}_{i,h,\ell} \cdot a_{h,\ell}

where:

  • αi\alpha_i = cut intercept (RHS)
  • πi,hv\pi^v_{i,h} = coefficient for storage state variable
  • πi,h,lag\pi^{lag}_{i,h,\ell} = coefficient for AR lag state variable

When anticipated thermals are present, the cut carries one additional coefficient per anticipated-state slot (§5c), read from the same reduced-cost mechanism.

Cuts live in an append-only pool at stable slot indices: every cut ever generated is retained for the lifetime of the run, and only the active subset is baked into each iteration’s stage template. Deactivation toggles a cut row’s bound to a trivially-satisfied ±\pm\infty sentinel rather than removing the row, so slot indices stay stable and reactivation is exact. See cut management.

For cut coefficient derivation, aggregation, and selection strategies, see cut management.

The stage LP is numerically conditioned via a three-step scaling procedure applied once at template construction time. Scaling improves solver convergence by reducing the condition number of the constraint matrix without changing the optimization argmin.

All objective coefficients (except the future cost variable θ\theta) are divided by a constant factor K=1000K = 1000:

c~j=cjKfor all jθ\tilde{c}_j = \frac{c_j}{K} \quad \text{for all } j \neq \theta

The θ\theta variable retains its coefficient of 1.0 because the Benders cuts enforce θαscaled\theta \geq \alpha_{scaled} where αscaled=Qsuccessor/K\alpha_{scaled} = Q_{successor} / K, so θ\theta already operates in scaled cost space. The LP objective is jc~jxj+1.0θ\sum_j \tilde{c}_j x_j + 1.0 \cdot \theta, and the total scaled objective equals (Cstage+Cfuture)/K(C_{stage} + C_{future}) / K. All cost-domain outputs (objective values, duals, cost breakdowns) are multiplied by KK at the reporting boundary to recover original units.

After cost scaling, each column jj is assigned a scale factor:

djcol=1maxiAijminiAijd_j^{col} = \frac{1}{\sqrt{\max_i |A_{ij}| \cdot \min_i |A_{ij}|}}

where the max and min are taken over nonzero entries in column jj. Columns with no nonzero entries receive djcol=1d_j^{col} = 1. The transformation replaces:

  • Matrix entries: A~ij=Aijdjcol\tilde{A}_{ij} = A_{ij} \cdot d_j^{col}
  • Objective coefficients: c~j=cjdjcol\tilde{c}_j = c_j \cdot d_j^{col}
  • Column bounds: l~j=lj/djcol\tilde{l}_j = l_j / d_j^{col}, u~j=uj/djcol\tilde{u}_j = u_j / d_j^{col}

After column scaling, each row ii is assigned a scale factor using the same geometric-mean formula applied to the already column-scaled matrix:

dirow=1maxjA~ijminjA~ijd_i^{row} = \frac{1}{\sqrt{\max_j |\tilde{A}_{ij}| \cdot \min_j |\tilde{A}_{ij}|}}

The transformation replaces:

  • Matrix entries: A^ij=A~ijdirow\hat{A}_{ij} = \tilde{A}_{ij} \cdot d_i^{row}
  • Row bounds: l^irow=lirowdirow\hat{l}_i^{row} = l_i^{row} \cdot d_i^{row}, u^irow=uirowdirow\hat{u}_i^{row} = u_i^{row} \cdot d_i^{row}

Column bounds and objective coefficients are not modified by row scaling.

The combined scaling produces the standard DrADcD_r \cdot A \cdot D_c form where DrD_r and DcD_c are diagonal scaling matrices.