Toy Single-Reservoir SDDP Walkthrough
Purpose
Section titled “Purpose”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.
1. The Case in One Picture
Section titled “1. The Case in One Picture”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.
Case parameters (chosen for hand-verifiability):
| Parameter | Symbol | Value |
|---|---|---|
| Stages | 4 | |
| Inflow openings per stage | 3 | |
| Initial storage | 30 (storage units) | |
| Reservoir capacity | 100 | |
| Demand per stage | 40 | |
| Inflow seasonal mean | 30 | |
| Inflow seasonal std deviation | 10 | |
| Thermal marginal cost | 50 | |
| Deficit cost | 1000 | |
| Discount factor | 1.0 |
Demand is set above the mean inflow () 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 stays at zero throughout.
2. Stage LP for This Case
Section titled “2. Stage LP for This Case”Each stage 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:
Load balance:
Water balance:
Incoming-storage pinning (binds incoming storage to the trial value by equal column bounds; the pinned column’s reduced cost becomes the cut coefficient):
Inflow (treated as data, not a state variable; see section 3):
Bounds:
Cost-to-go variable : in the terminal stage 4 there are no cuts, and is bound to zero. As the backward pass runs, cuts of the form are added to earlier stages’ LPs. The cost-to-go is non-increasing in storage (more water never raises the cost of the remaining stages), so every cut slope 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.
3. The 0-Order Inflow Model on This Case
Section titled “3. The 0-Order Inflow Model on This Case”The inflow at every stage is sampled independently from a normal distribution with stage-seasonal mean and standard deviation:
with and for every stage in this walkthrough.
The draws across stages are independent. This is the degenerate
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 ( and
per hydro and per stage) with neither
inflow_history.parquet nor inflow_ar_coefficients.parquet: novomodelo
then applies the seasonal statistics as white noise, 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 with equal probability , giving inflows . Because the openings are identical across stages and there is no AR memory, the same three-point distribution applies at every stage.
4. Iteration 1 — Forward Pass
Section titled “4. Iteration 1 — Forward Pass”The forward pass samples one trajectory using at every stage. Under zero noise the inflow stays at the seasonal mean throughout: for .
At each stage, the LP minimises with free at zero (no cuts in iteration 1). Pure hydro dispatch dominates whenever water is available.
Stage 1 — incoming storage , inflow . Available water: . Optimal: , , . End storage . Stage cost: .
Stage 2 — , . Available: . , , . Stage cost: .
Stage 3 — , . Available: . , , . Stage cost: .
Stage 4 — , . Available: . The LP sets (all water turbined), (thermal fills the gap), , . Stage cost: .
Forward-pass summary:
| Stage | Stage cost | |||||
|---|---|---|---|---|---|---|
| 1 | 30 | 30 | 40 | 0 | 20 | 0 |
| 2 | 20 | 30 | 40 | 0 | 10 | 0 |
| 3 | 10 | 30 | 40 | 0 | 0 | 0 |
| 4 | 0 | 30 | 30 | 10 | 0 | 500 |
Upper-bound estimate from this trajectory: . This is one realisation of total cost, a statistical estimate. It falls below the iteration-1 lower bound of section 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).
5. Iteration 1 — Backward Pass
Section titled “5. Iteration 1 — Backward Pass”The backward pass walks stages . At each stage it pins the incoming storage to the forward pass’s trial point , 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 (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).
Stage 4 (terminal)
Section titled “Stage 4 (terminal)”Trial point: . Inflows under the three openings: .
| Opening | Available | |||||
|---|---|---|---|---|---|---|
| 20 | 20 | 20 | 20 | 1000 | ||
| 30 | 30 | 30 | 10 | 500 | ||
| 40 | 40 | 40 | 0 | 0 |
Storage cut coefficient , the reduced cost of the pinned incoming-storage column. By the LP envelope theorem applied to the pinned bound :
- For and (water-limited, thermal active): one extra unit of enables one extra unit of turbining, displacing one unit of thermal worth . Optimal cost falls by , so .
- For (available water exactly meets demand) the stage cost has a kink: one more unit of ends in the terminal storage , which has zero value, while one unit less needs one unit of thermal worth . Every value in is a valid subgradient, and the walkthrough’s rule gives .
| Opening | |
|---|---|
Per-opening intercepts (anchoring each cut at the trial point):
With this simplifies to : .
Single-cut aggregation (uniform probability ; see Cut Management section 3):
Cut added to stage 3’s LP:
Sanity check. At the cut gives , matching the expected stage-4 cost from the table above. At the cut gives , after which the implicit bound takes over.
Stage 3
Section titled “Stage 3”Trial point: .
The stage-3 LP now carries the cut . For each opening the optimiser balances spending on thermal now against keeping more water for the future (worth per unit via the cut).
| Opening | Available | ||||||
|---|---|---|---|---|---|---|---|
| 20 | 30 | 30 | 10 | 0 | |||
| 30 | 40 | 40 | 0 | 0 | |||
| 40 | 50 | 40 | 0 | 10 |
For the system is water-limited; the LP turbines all 30 units of available water and dispatches 10 MW of thermal, ending with and the cut binding at .
For available water exactly meets demand; no thermal, end , cut value .
For the optimiser must choose how much of the surplus to release. Decreasing raises by one (cost ) and raises by one (saves on the cut); the marginal cost of holding more is , so the optimiser pushes to its load-balance bound at , leaving and the cut value at .
Storage cut coefficients (reduced costs of the pinned incoming-storage column):
- (water-limited): one extra unit of frees one extra turbine unit, saves of thermal. .
- (available water exactly meets demand): a kink. One unit less of needs one unit of thermal (); one unit more raises and lowers the cut value by . Every value in is a valid subgradient, and the rule gives .
- (water surplus, holding storage): one extra unit of raises by one, lowers the cut value by . .
Per-opening intercepts :
| Opening | |||
|---|---|---|---|
Aggregation ():
Cut added to stage 2’s LP:
Sanity check. At the cut evaluates to , matching the probability-weighted expectation .
Stage 2
Section titled “Stage 2”Trial point: . The stage-2 LP carries the cut . A stored unit is worth at most , so every opening turbines up to demand.
| Opening | Available | ||||||||
|---|---|---|---|---|---|---|---|---|---|
| 20 | 40 | 40 | 0 | 0 | |||||
| 30 | 50 | 40 | 0 | 10 | |||||
| 40 | 60 | 40 | 0 | 20 |
is a kink (available water exactly meets demand; every value in is a valid subgradient) and the rule gives ; in and one more unit of raises along the cut. The intercepts are .
Aggregation ():
Cut added to stage 1’s LP:
At the cut evaluates to , the probability-weighted expectation .
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).
6. Iteration 1 — Lower Bound
Section titled “6. Iteration 1 — Lower Bound”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 , and takes the probability-weighted expectation of the opening objectives (the risk-neutral case of the lower bound):
where is the stage-1 optimal objective under opening with the iteration-1 cut in place.
| Opening | Available | ||||||
|---|---|---|---|---|---|---|---|
| 20 | 50 | 40 | 0 | 10 | |||
| 30 | 60 | 40 | 0 | 20 | |||
| 40 | 70 | 40 | 0 | 30 |
The lower bound rises from (before the first iteration, no cuts) to . 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.
7. Iteration 2
Section titled “7. Iteration 2”The second forward pass draws : a wet first stage, then mean inflows. Each stage LP carries its iteration-1 cut.
| Stage | Stage cost | ||||||
|---|---|---|---|---|---|---|---|
| 1 | 30 | 40 | 40 | 0 | 30 | 0 | |
| 2 | 30 | 30 | 40 | 0 | 20 | 0 | |
| 3 | 20 | 30 | 40 | 0 | 10 | 0 | |
| 4 | 10 | 30 | 40 | 0 | 0 | 0 |
The trajectory costs , 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:
| Stage | Trial point | Opening | ||||||
|---|---|---|---|---|---|---|---|---|
| 4 | 20 | 0 | ||||||
| 4 | 30 | 0 | (kink) | |||||
| 4 | 40 | 10 | ||||||
| 3 | 20 | 0 | (kink) | |||||
| 3 | 30 | 10 | (kink) | |||||
| 3 | 40 | 20 | (kink) | |||||
| 2 | 20 | 10 | ||||||
| 2 | 30 | 20 | ||||||
| 2 | 40 | 30 |
At stage 3 the available water of exactly meets demand, the end storage of sits where the two stage-3 cuts meet, and of where the second cut reaches zero, so all three openings are kinks there. Averaging with gives the iteration-2 cuts:
| Cut on stage | From trial point | At the trial point | ||
|---|---|---|---|---|
| 3 | ||||
| 2 | ||||
| 1 |
With both stage-1 cuts active, the stage-1 openings end at , and with , and (the second cut is the larger one only at ), so
8. The Cost-to-Go Across Iterations
Section titled “8. The Cost-to-Go Across Iterations”After iteration 2 each non-terminal stage holds two cuts. In this case the cost-to-go , the expected cost of stages to from the storage left at the end of stage , is a convex, piecewise-linear and non-increasing function of one variable, and each cut on stage is a line in the same variable. The stage- LP’s sees the approximation
the pointwise maximum (upper envelope) of the cut lines and the floor . Every cut lies on or below , so the upper envelope lies on or below too: it is a lower approximation, and each new cut can only raise it.
The table compares , the cost of stages 3 and 4, with the two cuts on stage 2 (trial points in iteration 1 and in iteration 2):
| Iteration-1 cut | Iteration-2 cut | |||
|---|---|---|---|---|
| 0 | ||||
| 10 | ||||
| 15 | ||||
| 20 | ||||
| 30 | ||||
| 40 |
The envelope touches at both trial points and on , where the iteration-2 cut coincides with a linear piece of , and equals for , where the floor and are both zero; it stays strictly below elsewhere, as at and . Later iterations that visit other trial points, low-storage ones in particular, add cuts that raise the envelope where it is still below .
The figure plots the table: (solid), the iteration-1 and iteration-2 cuts on stage 2 (dashed), and their upper envelope with the floor , against the storage left at the end of stage 2. The envelope meets at the two trial points (marked), on and for , where the floor and are both zero, and lies below it elsewhere. The plotted values come from a tested module that re-runs both iterations of this walkthrough.
9. Convergence on This Case
Section titled “9. Convergence on This Case”Two iterations raise the lower bound from to . The optimal expected cost of this case is : 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 equally likely inflow sequences from 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, and , 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.
10. What This Example Does Not Show
Section titled “10. What This Example Does Not Show”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 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 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 . 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.
Cross-References
Section titled “Cross-References”- 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 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