1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
//! Stage LP patch buffer and stage template builder for SDDP subproblems.
//!
//! This module provides two public facilities:
//!
//! - [`PatchBuffer`]: pre-allocates the parallel arrays consumed by
//! `SolverInterface::set_row_bounds` and `SolverInterface::set_col_bounds`
//! and fills them with scenario-dependent values before each LP solve.
//! Allocating once at training start and reusing the same buffer across all
//! iterations and stages is critical for hot-path performance: the training
//! loop calls `fill_forward_patches`, `fill_load_patches`, or
//! `fill_col_state_patches` millions of times.
//!
//! - [`build_stage_templates`]: constructs a `Vec<StageTemplate>` from a
//! loaded `System` — one template per study stage. The template encodes
//! the full structural LP in CSC format, column/row bounds, and objective
//! coefficients. It is built once at startup and shared read-only across
//! all threads.
//!
//! ## LP layout (Solver Abstraction SS2)
//!
//! ### Column layout (contiguous regions)
//!
//! ```text
//! [0, N) outgoing storage (N = n_hydros)
//! [N, N*(1+L)) AR lag variables (N*L lags, hydro-major)
//! [N*(1+L), N*(1+L) + A*K_max) anticipated_state (A*K_max slots, slot-major)
//! [N*(1+L)+A*K_max, N*(2+L)+A*K_max) z_inflow (realized inflow, free)
//! [N*(2+L)+A*K_max, N*(3+L)+A*K_max) storage_in (incoming storage; state-pinning column)
//! N*(3+L)+A*K_max theta (future cost, scalar)
//! N*(3+L)+A*K_max+1 .. decision variables:
//! hydro turbine: N*K columns
//! hydro spillage: N*K columns
//! hydro diversion: N*K columns (zero-bounded for non-diverting hydros)
//! thermal gen: T*K columns
//! line fwd flow: Lines*K columns
//! line rev flow: Lines*K columns
//! bus deficit: B*S*K columns (S = max_deficit_segments across all buses)
//! bus excess: B*K columns
//! inflow slack: N columns (sigma_inf_h, only when penalty method is active)
//! FPHA generation: N_fpha*K columns (one per FPHA hydro per block)
//! evaporation: N_evap*3 columns (evaporation_flow, f_evap_plus, f_evap_minus per evap hydro)
//! withdrawal slack: 2*N columns (under-withdrawal neg, over-withdrawal pos; one per hydro each)
//! outflow_below slack: N*K columns (min-outflow violation, one per hydro per block)
//! outflow_above slack: N*K columns (max-outflow violation, one per hydro per block)
//! turbine_below slack: N*K columns (min-turbine violation, one per hydro per block)
//! gen_below slack: N*K columns (min-generation violation, one per hydro per block)
//! NCS generation: n_active_ncs*K columns (VARIES per stage)
//! generic slack: n_generic_slack columns (VARIES per stage)
//! ```
//!
//! When `A == 0` (no anticipated thermals), the `anticipated_state` block is
//! absent and the layout collapses: `z_inflow` starts at `N*(1+L)`, `theta` at
//! `N*(3+L)`. Use [`crate::indexer::StageIndexer`] to resolve all column offsets.
//!
//! ### Row layout (contiguous regions)
//!
//! ```text
//! [0, N) z_inflow definition (equality, RHS = realized inflow)
//! [N, 2N) water balance (equality, RHS = noise innovation)
//! [2N, 2N + B*K) load balance (equality, RHS = scenario load demand)
//! 2N + B*K .. structural constraints:
//! FPHA: n_fpha_rows
//! evaporation: n_evap rows
//! min-outflow: N*K rows (one per hydro per block)
//! max-outflow: N*K rows (one per hydro per block)
//! min-turbine: N*K rows (one per hydro per block)
//! min-generation: N*K rows (one per hydro per block)
//! anticipated fishing: n_anticipated rows
//! anticipated_state_out_def: n_active_anticipated rows (stage-dependent)
//! generic: n_generic_rows (VARIES per stage)
//! ```
//!
//! The AR dynamics (noise patch target) rows are the water balance constraints
//! beginning at row `base_rows[stage]` (= `N` for a stage with N hydros). The
//! `base_rows` value returned alongside the templates encodes this offset for
//! each stage so that [`PatchBuffer`] can update the correct RHS during
//! forward-pass solves.
//!
//! ### State pinning
//!
//! State pinning lives on **incoming-state columns** rather than rows.
//! Use [`crate::indexer::StageIndexer::state_to_lp_incoming_column`] to
//! resolve the column index for state vector entry `j`. Both forward-pass
//! state pinning (`set_col_bounds(&[col], &[v], &[v])`) and backward-pass
//! cut-subgradient extraction (`view.reduced_costs[col] * col_scale[col]`)
//! use the same column index. The `storage_fixing`, `lag_fixing`, and
//! `anticipated_state_fixing` row ranges in [`crate::indexer::StageIndexer`]
//! are permanent empty sentinels (`0..0`).
//!
//! ## Patch sequence (Training Loop SS4.2a)
//!
//! Each forward-pass solve writes up to `2N + M*K` row patches and up to
//! `N*(1+L) + A*K_max` column-bound patches (where M is the number of
//! stochastic load buses and K is the block count):
//!
//! - **Row buffer** (written by `fill_forward_patches`, `fill_load_patches`,
//! `fill_z_inflow_patches`):
//!
//! ```text
//! Category 3 -- noise innovation N rows at base_rows[stage]
//! patch water_balance_row(base_row, h) = noise[h] for h in [0, N)
//!
//! Category 4 -- load balance patches M*K rows (optional, stochastic load)
//! patch load_row_start + bus_pos[i]*K + blk = load_rhs[i*K + blk]
//! for i in [0, M), blk in [0, K)
//!
//! Category 5 -- z-inflow RHS N rows at z_inflow_row_start (= 0)
//! patch z_inflow_row(h) = base_h + noise_scale_h * eta_h for h in [0, N)
//! ```
//!
//! - **Column buffer** (written by `fill_col_state_patches`):
//!
//! ```text
//! Category 1 -- incoming storage cols [storage_in.start, storage_in.end)
//! col_lower[h] == col_upper[h] == state[h] for h in [0, N)
//!
//! Category 2 -- AR lag state cols [inflow_lags.start, inflow_lags.end)
//! col_lower[N + l*N + h] == col_upper[...] == state[N + l*N + h]
//! for h in [0, N), l in [0, L)
//!
//! Category 6 -- anticipated state cols [anticipated_state.start, .end)
//! col_lower[slot] == col_upper[slot] == state[N*(1+L) + slot]
//! for slot in [0, A*K_max)
//! ```
//!
//! Each column-buffer entry `c` has `col_lower[c] == col_upper[c]` to pin the
//! variable to the state value via tight bounds. The backward pass writes only
//! the column buffer (state pinning); noise innovations come from the fixed
//! opening tree and are written to the row buffer via `fill_forward_patches`
//! with the opening-specific noise vector.
//! When `n_load_buses == 0`, Category 4 is empty and has no effect.
//!
//! ## Row index types
//!
//! All indices are stored as `usize` to match the `set_row_bounds` interface
//! signature directly; no casting is required at the call site.
//!
//! ## Worked example (SS4.2a): N = 3, L = 2, no anticipated thermals
//!
//! ```text
//! Row patches (Categories 3 and 5):
//! Patch Category Row index Value
//! 0 noise-fix 3 + 0 = 3 noise[0] (H0; base_rows[s] = N = 3)
//! 1 noise-fix 3 + 1 = 4 noise[1] (H1)
//! 2 noise-fix 3 + 2 = 5 noise[2] (H2)
//! 3 z-inflow 0 + 0 = 0 z_rhs[0] (H0; z_inflow_row_start = 0)
//! 4 z-inflow 0 + 1 = 1 z_rhs[1] (H1)
//! 5 z-inflow 0 + 2 = 2 z_rhs[2] (H2)
//!
//! Column patches (Categories 1 and 2; via fill_col_state_patches):
//! Entry Category Column index Value
//! 0 storage 12 + 0 = 12 state[0] (H0; storage_in.start = N*(2+L) = 12)
//! 1 storage 12 + 1 = 13 state[1] (H1)
//! 2 storage 12 + 2 = 14 state[2] (H2)
//! 3 lag 3 + 0 = 3 state[3] (H0 lag 0; inflow_lags.start = N = 3)
//! 4 lag 3 + 1 = 4 state[4] (H1 lag 0)
//! 5 lag 3 + 2 = 5 state[5] (H2 lag 0)
//! 6 lag 3 + 3 = 6 state[6] (H0 lag 1)
//! 7 lag 3 + 4 = 7 state[7] (H1 lag 1)
//! 8 lag 3 + 5 = 8 state[8] (H2 lag 1)
//! ```
//!
//! Total row patches: 6 = N + N. Total column patches: 9 = N*(1+L).
use ConstraintSense;
// --- Public re-exports (stable API) ---
pub use ;
pub use ;
// --- Crate-internal re-exports ---
pub use ;
// ---------------------------------------------------------------------------
// Shared constants
// ---------------------------------------------------------------------------
/// Per-hour conversion factor from m³/s to hm³.
///
/// `M3S_TO_HM3 = seconds_per_hour / m³_per_hm³ = 3600 / 1_000_000 = 0.0036`
///
/// Callers multiply by `Block::duration_hours` to get the full block
/// conversion: `volume_hm3 = flow_m3s * M3S_TO_HM3 * duration_hours`.
/// For a 30-day month (720 h): `0.0036 * 720 = 2.592`.
pub const M3S_TO_HM3: f64 = 3_600.0 / 1_000_000.0; // multiply by hours to get hm³
/// Divisor applied to all objective-function cost coefficients.
///
/// Dividing monetary costs by this factor reduces objective magnitudes
/// (e.g., from ~8.8e11 to ~8.8e8), improving LP solver numerical
/// conditioning without changing the optimization argmin. All cost-domain
/// outputs (objective values, duals, cost breakdowns) are multiplied by
/// this factor at the reporting boundary to recover original units.
pub const COST_SCALE_FACTOR: f64 = 1_000_000.0;
/// Safety margin applied symmetrically to the magnitude bound on the evaporation
/// outflow variable. The bound is `[-q_max, +q_max]` where
/// `q_max = |intercept_m3s + volume_slope_m3s_per_hm3 * v_max| * margin`. A 2x
/// margin accounts for linearization approximation error: the actual area-volume
/// curve may exceed the linear estimate near `v_max` in either direction, so the
/// same factor covers both the positive case (true evaporation underestimated at
/// high storage) and the negative case (net rainfall input underestimated at high
/// storage). A negative evaporation-outflow value represents net rainfall input
/// (inflow) rather than evaporative outflow.
pub const EVAPORATION_FLOW_SAFETY_MARGIN: f64 = 2.0;
// ---------------------------------------------------------------------------
// Shared types
// ---------------------------------------------------------------------------
/// Per-row metadata for one active generic constraint row at a single stage.
///
/// Stores the information needed by the LP builder to fill CSC matrix entries,
/// row bounds, and objective coefficients for generic constraint rows and their
/// associated slack columns. Also used by the simulation extraction pipeline
/// to map LP row/column indices back to constraint identity and block.
///
/// One entry is created per active `(constraint, block)` pair, with one
/// exception: a `block_id = None` bound whose expression is **block-independent**
/// (every term resolves to a per-stage column) collapses to a *single*
/// stage-level entry (`is_stage_level = true`) instead of one per block, since
/// the replicated rows would be identical. A `block_id = None` bound on a
/// block-level expression still generates one entry per block; a `block_id =
/// Some(k)` bound generates exactly one entry.