Skip to content

Toy Single-Reservoir SDDP Walkthrough

This chapter walks through two complete SDDP iterations on a deliberately small system — one reservoir, one thermal unit, one demand block, four stages, and a 0-order (pure seasonal-sampling) inflow model — using numbers chosen so every cut coefficient and every dual variable can be verified by hand. The goal is to make the abstract machinery of forward pass, backward pass, cut construction, and lower-bound update tangible.

Novomodelo ships a reference case, examples/1dtoy/ in the novomodelo repository, that novomodelo init --template 1dtoy <dir> scaffolds and the Quickstart runs end to end. That case has one bus, one hydro plant, two thermal plants, four monthly stages (January to April 2024), ten openings per stage and an annual discount rate of 0.12. The walkthrough keeps the single bus, the single hydro plant and the four stages, and replaces the rest with numbers chosen for hand verification: one thermal unit, three openings per stage, no discounting and idealised units (see the units note in section 1). This walkthrough is a pedagogical caricature, not a reproduction of the shipped case.

The chapter does not explain how the underlying mechanisms work; it shows them working. It does not cover multi-reservoir effects, spatial inflow correlation, the FPHA production model, risk-measure weighting, or autoregressive inflow memory. Those topics belong to the chapters cited in section 10 and to the multi-reservoir companion in Toy Four-Reservoir Walkthrough.


The system has one bus, one reservoir, one thermal unit, and one demand block. The reservoir receives a stochastic inflow drawn from a 0-order seasonal-sampling model; the thermal unit and a deficit slack cover any shortfall that the reservoir cannot serve.

Inflow aₜ(0-order)Reservoir vcap 100Hydro qThermal g_thcost 50Deficit δcost 1000Busdemand D = 40

Case parameters (chosen for hand-verifiability):

ParameterSymbolValue
StagesTT4
Inflow openings per stageNtN_t3
Initial storagev0v_030 (storage units)
Reservoir capacityVˉ\bar{V}100
Demand per stageDD40
Inflow seasonal meanμ\mu30
Inflow seasonal std deviationσ\sigma10
Thermal marginal costcthc^{th}50
Deficit costcdefc^{def}1000
Discount factordd1.0

Demand is set above the mean inflow (D=40>μ=30D = 40 > \mu = 30) so the reservoir depletes over the four stages, eventually forcing thermal dispatch and producing non-trivial dual variables in the backward pass. The thermal unit has no capacity limit, so the deficit δ\delta stays at zero throughout.


Each stage tt solves the following LP (specialised from the general formulation in LP Formulation to one hydro, one thermal, one demand block, and a 0-order inflow):

Objective:

min⁡50 gth+1000 δ+θ\min \quad 50\, g_{th} + 1000\, \delta + \theta

Load balance:

q+gth+δ=40q + g_{th} + \delta = 40

Water balance:

v=vin+a−qv = v^{in} + a - q

Incoming-storage pinning (binds incoming storage to the trial value by equal column bounds; the pinned column’s reduced cost becomes the cut coefficient):

v‾in=vˉin=v^t−1\underline{v}^{in} = \bar{v}^{in} = \hat{v}_{t-1}

Inflow (treated as data, not a state variable; see section 3):

a=at(ω)(realised from the 0-order sampler)a = a_t(\omega) \quad \text{(realised from the 0-order sampler)}

Bounds:

0≤v≤100,q≥0,gth≥0,δ≥0,θ≥00 \leq v \leq 100, \quad q \geq 0, \quad g_{th} \geq 0, \quad \delta \geq 0, \quad \theta \geq 0

Cost-to-go variable θ\theta: in the terminal stage 4 there are no cuts, and θ\theta is bound to zero. As the backward pass runs, cuts of the form θ≥β0+βv v\theta \geq \beta_0 + \beta^v\, v are added to earlier stages’ LPs. The cost-to-go V(v)V(v) is non-increasing in storage (more water never raises the cost of the remaining stages), so every cut slope βv\beta^v is zero or negative.

Note the absence of any AR-lag state variable: the 0-order inflow has no memory across stages, so storage is the only state.


The inflow at every stage is sampled independently from a normal distribution with stage-seasonal mean and standard deviation:

at=μ+σ εt,εt∼N(0,1)a_t = \mu + \sigma\, \varepsilon_t, \qquad \varepsilon_t \sim \mathcal{N}(0, 1)

with μ=30\mu = 30 and σ=10\sigma = 10 for every stage in this walkthrough. The draws across stages are independent. This is the degenerate p=0p = 0 case of the PAR(p) Inflow Model: no lag terms, no AR coefficients, no initial-lag state. A novomodelo case expresses it by supplying scenarios/inflow_seasonal_stats.parquet (μt\mu_t and σt\sigma_t per hydro and per stage) with neither inflow_history.parquet nor inflow_ar_coefficients.parquet: novomodelo then applies the seasonal statistics as white noise, pm=0p_m = 0 for every season, and runs no order selection (the order selection of PAR(p) Inflow Model §3.6 needs an inflow history). The shipped 1dtoy is built this way, and novomodelo run reports its estimation path as user_stats_white_noise.

The three openings used in the backward pass correspond to ε∈{−1,0,+1}\varepsilon \in \{-1, 0, +1\} with equal probability p=1/3p = 1/3, giving inflows at∈{20,30,40}a_t \in \{20, 30, 40\}. Because the openings are identical across stages and there is no AR memory, the same three-point distribution applies at every stage.


The forward pass samples one trajectory using εt=0\varepsilon_t = 0 at every stage. Under zero noise the inflow stays at the seasonal mean throughout: at=30a_t = 30 for t=1,2,3,4t = 1, 2, 3, 4.

At each stage, the LP minimises 50 gth+1000 δ+θ50\, g_{th} + 1000\, \delta + \theta with θ\theta free at zero (no cuts in iteration 1). Pure hydro dispatch dominates whenever water is available.

Stage 1 — incoming storage v^0=30\hat{v}_0 = 30, inflow a1=30a_1 = 30. Available water: 30+30=60≥4030 + 30 = 60 \geq 40. Optimal: q=40q = 40, gth=0g_{th} = 0, δ=0\delta = 0. End storage v1=30+30−40=20v_1 = 30 + 30 - 40 = 20. Stage cost: 00.

Stage 2 — v^1=20\hat{v}_1 = 20, a2=30a_2 = 30. Available: 50≥4050 \geq 40. q=40q = 40, gth=0g_{th} = 0, v2=10v_2 = 10. Stage cost: 00.

Stage 3 — v^2=10\hat{v}_2 = 10, a3=30a_3 = 30. Available: 40=4040 = 40. q=40q = 40, gth=0g_{th} = 0, v3=0v_3 = 0. Stage cost: 00.

Stage 4 — v^3=0\hat{v}_3 = 0, a4=30a_4 = 30. Available: 30<4030 < 40. The LP sets q=30q = 30 (all water turbined), gth=10g_{th} = 10 (thermal fills the gap), δ=0\delta = 0, v4=0v_4 = 0. Stage cost: 50×10=50050 \times 10 = 500.

Forward-pass summary:

Stagev^t−1\hat{v}_{t-1}ata_tqqgthg_{th}vtv_tStage cost
13030400200
22030400100
3103040000
403030100500

Upper-bound estimate from this trajectory: 0+0+0+500=5000 + 0 + 0 + 500 = 500. This is one realisation of total cost, a statistical estimate. It falls below the iteration-1 lower bound of section 6, 5000/9≈555.65000/9 \approx 555.6, as a single sampled trajectory can; averaging many trajectories reduces the sampling error, and only an exact upper bound certifies the optimality gap (Cut Management — Tier 3).


The backward pass walks stages 4→24 \to 2. At each stage tt it pins the incoming storage to the forward pass’s trial point v^t−1\hat{v}_{t-1}, solves all three openings, reads the reduced cost of the pinned incoming-storage column (Reduced-Cost Extraction), computes per-opening intercepts, and aggregates them into one cut on stage t−1t-1 (Single-Cut Aggregation). The stage-1 solves are the lower-bound evaluation of section 6.

Kinks. When an opening’s available water exactly meets demand, or its end storage sits where two pieces of the stage’s cost-to-go approximation meet (two cuts, or a cut and the zero floor), the stage cost has a kink at the trial point: one unit less and one unit more of incoming storage change the cost at different rates, and every value between the two rates is a valid subgradient. The reduced cost an LP solver returns there depends on its final basis. This walkthrough takes, at every kink, the rate for one more unit of incoming storage (the right derivative).

Trial point: v^3=0\hat{v}_3 = 0. Inflows under the three openings: a4(ω)∈{20,30,40}a_4(\omega) \in \{20, 30, 40\}.

Openingε\varepsilona4a_4Available v^ ⁣+a4\hat{v}\!+a_4qqgthg_{th}Q4Q_4
ω1\omega_1−1-1202020201000
ω2\omega_20030303010500
ω3\omega_3+1+140404000

Storage cut coefficient β4v(ω)=∂Q4/∂v^3\beta^v_4(\omega) = \partial Q_4/\partial \hat{v}_3, the reduced cost of the pinned incoming-storage column. By the LP envelope theorem applied to the pinned bound v4in=v^3v^{in}_4 = \hat{v}_3:

  • For ω1\omega_1 and ω2\omega_2 (water-limited, thermal active): one extra unit of v^3\hat{v}_3 enables one extra unit of turbining, displacing one unit of thermal worth 5050. Optimal cost falls by 5050, so β4v(ω)=−50\beta^v_4(\omega) = -50.
  • For ω3\omega_3 (available water exactly meets demand) the stage cost has a kink: one more unit of v^3\hat{v}_3 ends in the terminal storage v4v_4, which has zero value, while one unit less needs one unit of thermal worth 5050. Every value in [−50,0][-50, 0] is a valid subgradient, and the walkthrough’s rule gives β4v(ω3)=0\beta^v_4(\omega_3) = 0.
Openingβ4v\beta^v_4
ω1\omega_1−50-50
ω2\omega_2−50-50
ω3\omega_300

Per-opening intercepts (anchoring each cut at the trial point):

β0,4(ω)=Q4(ω)−β4v(ω) v^3\beta_{0,4}(\omega) = Q_4(\omega) - \beta^v_4(\omega)\,\hat{v}_3

With v^3=0\hat{v}_3 = 0 this simplifies to β0,4(ω)=Q4(ω)\beta_{0,4}(\omega) = Q_4(\omega): β0,4=(1000, 500, 0)\beta_{0,4} = (1000,\, 500,\, 0).

Single-cut aggregation (uniform probability p=1/3p = 1/3; see Cut Management section 3):

βˉ0=13(1000+500+0)=500,βˉv=13(−50−50+0)=−1003\bar{\beta}_0 = \tfrac{1}{3}(1000 + 500 + 0) = 500, \qquad \bar{\beta}^v = \tfrac{1}{3}(-50 - 50 + 0) = -\tfrac{100}{3}

Cut added to stage 3’s LP:

θ  ≥  500  −  1003 v\theta \;\geq\; 500 \;-\; \tfrac{100}{3}\, v

Sanity check. At v=0v = 0 the cut gives θ≥500\theta \geq 500, matching the expected stage-4 cost from the table above. At v=15v = 15 the cut gives θ≥0\theta \geq 0, after which the implicit θ≥0\theta \geq 0 bound takes over.

Trial point: v^2=10\hat{v}_2 = 10.

The stage-3 LP now carries the cut θ≥500−(100/3) v3\theta \geq 500 - (100/3)\, v_3. For each opening a3∈{20,30,40}a_3 \in \{20, 30, 40\} the optimiser balances spending on thermal now against keeping more water for the future (worth 100/3100/3 per unit via the cut).

Openinga3a_3Availableqqgthg_{th}v3v_3θ∗\theta^*Q3Q_3
ω1\omega_120303010050050010001000
ω2\omega_230404000500500500500
ω3\omega_3405040010500/3500/3500/3500/3

For ω1\omega_1 the system is water-limited; the LP turbines all 30 units of available water and dispatches 10 MW of thermal, ending with v3=0v_3 = 0 and the cut binding at 500500.

For ω2\omega_2 available water exactly meets demand; no thermal, end v3=0v_3 = 0, cut value 500500.

For ω3\omega_3 the optimiser must choose how much of the surplus to release. Decreasing qq raises gthg_{th} by one (cost 5050) and raises v3v_3 by one (saves 100/3100/3 on the cut); the marginal cost of holding more is +50−100/3≈+16.7+50 - 100/3 \approx +16.7, so the optimiser pushes qq to its load-balance bound at 4040, leaving v3=10v_3 = 10 and the cut value at 500/3≈167500/3 \approx 167.

Storage cut coefficients β3v(ω)=∂Q3/∂v^2\beta^v_3(\omega) = \partial Q_3/\partial \hat{v}_2 (reduced costs of the pinned incoming-storage column):

  • ω1\omega_1 (water-limited): one extra unit of v^2\hat{v}_2 frees one extra turbine unit, saves 5050 of thermal. β3v(ω1)=−50\beta^v_3(\omega_1) = -50.
  • ω2\omega_2 (available water exactly meets demand): a kink. One unit less of v^2\hat{v}_2 needs one unit of thermal (−50-50); one unit more raises v3v_3 and lowers the cut value by 100/3100/3. Every value in [−50,−100/3][-50, -100/3] is a valid subgradient, and the rule gives β3v(ω2)=−100/3\beta^v_3(\omega_2) = -100/3.
  • ω3\omega_3 (water surplus, holding storage): one extra unit of v^2\hat{v}_2 raises v3v_3 by one, lowers the cut value by 100/3100/3. β3v(ω3)=−100/3\beta^v_3(\omega_3) = -100/3.

Per-opening intercepts β0,3(ω)=Q3(ω)−β3v(ω) v^2\beta_{0,3}(\omega) = Q_3(\omega) - \beta^v_3(\omega)\,\hat{v}_2:

OpeningQ3Q_3β3v⋅v^2\beta^v_3 \cdot \hat{v}_2β0,3\beta_{0,3}
ω1\omega_110001000−500-50015001500
ω2\omega_2500500−1000/3-1000/32500/32500/3
ω3\omega_3500/3500/3−1000/3-1000/3500500

Aggregation (p=1/3p = 1/3):

βˉ0=13(1500+25003+500)=85009,βˉv=13 ⁣(−50−1003−1003)=−3509\bar{\beta}_0 = \tfrac{1}{3}\Bigl(1500 + \tfrac{2500}{3} + 500\Bigr) = \tfrac{8500}{9}, \qquad \bar{\beta}^v = \tfrac{1}{3}\!\left(-50 - \tfrac{100}{3} - \tfrac{100}{3}\right) = -\tfrac{350}{9}

Cut added to stage 2’s LP:

θ  ≥  85009  −  3509 v\theta \;\geq\; \tfrac{8500}{9} \;-\; \tfrac{350}{9}\, v

Sanity check. At v=v^2=10v = \hat{v}_2 = 10 the cut evaluates to (8500−3500)/9=5000/9≈555.6(8500 - 3500)/9 = 5000/9 \approx 555.6, matching the probability-weighted expectation Qˉ3=(1/3)(1000+500+500/3)=5000/9≈555.6\bar{Q}_3 = (1/3)(1000 + 500 + 500/3) = 5000/9 \approx 555.6.

Trial point: v^1=20\hat{v}_1 = 20. The stage-2 LP carries the cut θ≥8500/9−(350/9) v2\theta \geq 8500/9 - (350/9)\, v_2. A stored unit is worth at most 350/9≈38.9<50350/9 \approx 38.9 < 50, so every opening turbines up to demand.

Openinga2a_2Availableqqgthg_{th}v2v_2θ∗\theta^*Q2Q_2β2v\beta^v_2β0,2\beta_{0,2}
ω1\omega_1204040008500/98500/98500/98500/9−350/9-350/915500/915500/9
ω2\omega_23050400105000/95000/95000/95000/9−350/9-350/94000/34000/3
ω3\omega_3406040020500/3500/3500/3500/3−350/9-350/98500/98500/9

ω1\omega_1 is a kink (available water exactly meets demand; every value in [−50,−350/9][-50, -350/9] is a valid subgradient) and the rule gives −350/9-350/9; in ω2\omega_2 and ω3\omega_3 one more unit of v^1\hat{v}_1 raises v2v_2 along the cut. The intercepts are β0,2(ω)=Q2(ω)−β2v(ω) v^1\beta_{0,2}(\omega) = Q_2(\omega) - \beta^v_2(\omega)\,\hat{v}_1.

Aggregation (p=1/3p = 1/3):

βˉ0=13(155009+40003+85009)=40003,βˉv=−3509\bar{\beta}_0 = \tfrac{1}{3}\Bigl(\tfrac{15500}{9} + \tfrac{4000}{3} + \tfrac{8500}{9}\Bigr) = \tfrac{4000}{3}, \qquad \bar{\beta}^v = -\tfrac{350}{9}

Cut added to stage 1’s LP:

θ  ≥  40003  −  3509 v\theta \;\geq\; \tfrac{4000}{3} \;-\; \tfrac{350}{9}\, v

At v=v^1=20v = \hat{v}_1 = 20 the cut evaluates to 5000/9≈555.65000/9 \approx 555.6, the probability-weighted expectation Qˉ2\bar{Q}_2.

By the end of the backward pass, stages 1 through 3 each carry one cut. Stage 4 has none (it is the terminal stage and has no future to approximate).


After the backward pass installs a cut at stage 1, the iteration-1 lower bound solves the stage-1 LP for every opening with that cut active, from the fixed initial storage x0=v0=30x_0 = v_0 = 30, and takes the probability-weighted expectation of the opening objectives (the risk-neutral case of the lower bound):

z‾1=Eω[ Q11(x0,ω) ]\underline{z}^1 = \mathbb{E}_{\omega}\bigl[\, Q_1^1(x_0, \omega) \,\bigr]

where Q11Q_1^1 is the stage-1 optimal objective under opening ω\omega with the iteration-1 cut in place.

Openinga1a_1Availableqqgthg_{th}v1v_1θ∗\theta^*Q11Q_1^1
ω1\omega_12050400108500/98500/98500/98500/9
ω2\omega_23060400205000/95000/95000/95000/9
ω3\omega_3407040030500/3500/3500/3500/3
z‾1=13(85009+50009+5003)=50009≈555.6\underline{z}^1 = \tfrac{1}{3}\Bigl(\tfrac{8500}{9} + \tfrac{5000}{9} + \tfrac{500}{3}\Bigr) = \tfrac{5000}{9} \approx 555.6

The lower bound rises from 00 (before the first iteration, no cuts) to 5000/95000/9. It does not decrease across iterations, because cuts are only added; Cut Management — Tier 1 states when it is a valid lower bound. Here it stays below the optimal expected cost of section 9.


The second forward pass draws ε=(+1,0,0,0)\varepsilon = (+1, 0, 0, 0): a wet first stage, then mean inflows. Each stage LP carries its iteration-1 cut.

Stagev^t−1\hat{v}_{t-1}ata_tqqgthg_{th}vtv_tθ∗\theta^*Stage cost
1304040030500/3500/30
2303040020500/3500/30
3203040010500/3500/30
410304000000

The trajectory costs 00, and every trial point sits 10 units above the iteration-1 one. The backward pass then adds a second cut on each of stages 3, 2 and 1; each non-terminal stage LP it solves carries both of that stage’s cuts, and every kink takes the right derivative:

StageTrial pointOpeningata_tvtv_tθ∗\theta^*QtQ_tβtv\beta^v_tβ0,t\beta_{0,t}
4v^3=10\hat{v}_3 = 10ω1\omega_120000500500−50-5010001000
4v^3=10\hat{v}_3 = 10ω2\omega_2300000000 (kink)00
4v^3=10\hat{v}_3 = 10ω3\omega_3401000000000
3v^2=20\hat{v}_2 = 20ω1\omega_1200500500500500−100/3-100/3 (kink)3500/33500/3
3v^2=20\hat{v}_2 = 20ω2\omega_23010500/3500/3500/3500/3−50/3-50/3 (kink)500500
3v^2=20\hat{v}_2 = 20ω3\omega_34020000000 (kink)00
2v^1=30\hat{v}_1 = 30ω1\omega_120105000/95000/95000/95000/9−350/9-350/915500/915500/9
2v^1=30\hat{v}_1 = 30ω2\omega_230202000/92000/92000/92000/9−50/3-50/36500/96500/9
2v^1=30\hat{v}_1 = 30ω3\omega_34030500/9500/9500/9500/9−50/3-50/35000/95000/9

At stage 3 the available water of ω1\omega_1 exactly meets demand, the end storage v3=10v_3 = 10 of ω2\omega_2 sits where the two stage-3 cuts meet, and v3=20v_3 = 20 of ω3\omega_3 where the second cut reaches zero, so all three openings are kinks there. Averaging with p=1/3p = 1/3 gives the iteration-2 cuts:

Cut on stageFrom trial pointβˉ0\bar{\beta}_0βˉv\bar{\beta}^vAt the trial point
3v^3=10\hat{v}_3 = 101000/31000/3−50/3-50/3500/3500/3
2v^2=20\hat{v}_2 = 205000/95000/9−50/3-50/32000/92000/9
1v^1=30\hat{v}_1 = 3010001000−650/27-650/272500/92500/9

With both stage-1 cuts active, the stage-1 openings end at v1=10v_1 = 10, 2020 and 3030 with θ∗=8500/9\theta^* = 8500/9, 5000/95000/9 and 2500/92500/9 (the second cut is the larger one only at v1=30v_1 = 30), so

z‾2=13(85009+50009+25009)=1600027≈592.6\underline{z}^2 = \tfrac{1}{3}\Bigl(\tfrac{8500}{9} + \tfrac{5000}{9} + \tfrac{2500}{9}\Bigr) = \tfrac{16000}{27} \approx 592.6

After iteration 2 each non-terminal stage holds two cuts. In this case the cost-to-go Vt(v)V_t(v), the expected cost of stages tt to 44 from the storage vv left at the end of stage t−1t-1, is a convex, piecewise-linear and non-increasing function of one variable, and each cut on stage t−1t-1 is a line in the same variable. The stage-(t−1)(t-1) LP’s θ\theta sees the approximation

V‾tk(v)  =  max⁡{0,  max⁡i=1,…,k{βˉ0i+βˉv,i v}},\underline{V}_t^k(v) \;=\; \max\Bigl\{0,\; \max_{i = 1, \ldots, k} \bigl\{ \bar{\beta}_0^i + \bar{\beta}^{v,i}\, v \bigr\}\Bigr\},

the pointwise maximum (upper envelope) of the cut lines and the floor θ≥0\theta \geq 0. Every cut lies on or below VtV_t, so the upper envelope lies on or below VtV_t too: it is a lower approximation, and each new cut can only raise it.

The table compares V3V_3, the cost of stages 3 and 4, with the two cuts on stage 2 (trial points v^2=10\hat{v}_2 = 10 in iteration 1 and v^2=20\hat{v}_2 = 20 in iteration 2):

vvV3(v)V_3(v)Iteration-1 cutIteration-2 cutV‾32(v)\underline{V}_3^2(v)
0100010008500/98500/95000/95000/98500/98500/9
105000/95000/95000/95000/93500/93500/95000/95000/9
153500/93500/93250/93250/92750/92750/93250/93250/9
202000/92000/91500/91500/92000/92000/92000/92000/9
30500/9500/9−2000/9-2000/9500/9500/9500/9500/9
4000−5500/9-5500/9−1000/9-1000/900

The envelope touches V3V_3 at both trial points and on [20,30][20, 30], where the iteration-2 cut coincides with a linear piece of V3V_3, and equals V3V_3 for v≥40v \geq 40, where the floor and V3V_3 are both zero; it stays strictly below V3V_3 elsewhere, as at v=0v = 0 and v=15v = 15. Later iterations that visit other trial points, low-storage ones in particular, add cuts that raise the envelope where it is still below V3V_3.

The figure plots the table: V3V_3 (solid), the iteration-1 and iteration-2 cuts on stage 2 (dashed), and their upper envelope with the floor θ≥0\theta \geq 0, against the storage vv left at the end of stage 2. The envelope meets V3V_3 at the two trial points (marked), on [20,30][20, 30] and for v≥40v \geq 40, where the floor and V3V_3 are both zero, and lies below it elsewhere. The plotted values come from a tested module that re-runs both iterations of this walkthrough.


Two iterations raise the lower bound from 5000/9≈555.65000/9 \approx 555.6 to 16000/27≈592.616000/27 \approx 592.6. The optimal expected cost of this case is 17000/27≈629.617000/27 \approx 629.6: no unit of stored water saves more than one unit of thermal output, so turbining up to demand is optimal at every stage, and averaging that policy’s cost over the 34=813^4 = 81 equally likely inflow sequences from v0=30v_0 = 30 gives the value. Later iterations keep or raise the lower bound, and under the hypotheses of Cut Management — Tier 2 it converges to this value.

The two trajectory costs, 500500 and 00, are single-sample estimates of the policy’s expected cost; both fall below the lower bound of their iteration and neither certifies anything. A gap certificate needs an exact upper bound from an enumerated forward pass (Tier 3); the percent form of the optimality gap then divides the gap by the magnitude of the lower bound. The shipped 1dtoy runs one sampled forward pass per iteration and stops at its iteration limit of 128; novomodelo rejects a gap stopping rule at setup under a sampled forward pass (see Stopping Rules). The upper-bound mechanisms are compared in Upper Bound Evaluation.


This case isolates the core SDDP loop in its simplest form. It cannot illustrate:

  • Multi-reservoir effects: the cut becomes a hyperplane with one storage coefficient per reservoir; the Toy Four-Reservoir Walkthrough shows one on four decoupled reservoirs. Trading water between reservoirs needs a coupling (transmission, a cascade or a shared constraint) that neither walkthrough includes.
  • Spatial inflow correlation: when multiple plants share a wet/dry signal, their innovations ε\varepsilon must be drawn from a correlated multivariate normal via the spectral factorisation described in PAR(p) Inflow Model §6.
  • Autoregressive inflow memory: a PAR(p) model with p≥1p \geq 1 adds one lag state variable per lag; whether a stage’s cuts carry a coefficient for each lag is that stage’s cut-state projection (State Augmentation §7). See PAR(p) Inflow Model.
  • FPHA production model: nonlinear head-dependent efficiency approximated by piecewise-linear hyperplanes; one of the planes can bind, contributing to the storage cut coefficient. See Hydro Production Function Models.
  • Risk-measure effects: CVaR weighting that shifts cut aggregation probabilities away from uniform p=1/Ntp = 1/N_t. See Risk Measures.

The Toy Four-Reservoir Walkthrough extends this case to four independent reservoirs at four buses with no transmission, illustrating how the cut hyperplane and the per-bus dispatch mechanics scale up.


  • SDDP Algorithm — Forward and backward pass structure, lower-bound computation, convergence monitoring
  • LP Formulation — Complete stage LP: load balance, water balance, objective taxonomy
  • State Augmentation — Column-bound state pinning
  • Cut Management — Dual extraction, per-opening intercepts, single-cut aggregation, cut validity, sign convention
  • PAR(p) Inflow Model — Inflow model definition; the p=0p = 0 degenerate case (white noise) used in this walkthrough; stored vs computed quantities
  • Stopping Rules — Iteration limit, gap threshold, bound-stalling criteria
  • Upper Bound Evaluation — Simulation-based and inner-approximation upper bounds
  • Toy Four-Reservoir Walkthrough — Multi-reservoir extension at 4 buses with no transmission and independent 0-order inflows