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
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
//! FPHA hyperplane fitting from reservoir geometry.
//!
//! Turns a hydro plant's Volume-Height-Area (VHA) curve into a set of
//! hyperplanes approximating the production function `phi(v, q, s)`. The pipeline
//! evaluates the production function on a uniform `(V, Q)` grid (at spillage = 0),
//! takes the 3-D convex hull of the resulting `(V, Q, generation)` cloud, and reads its
//! upper-envelope facets as a piecewise-linear concave outer approximation.
//!
//! # Submodule layout
//!
//! - `error` — `FphaFittingError`, the validation-error enum every fallible step returns.
//! - `geometry` — `FittingBounds` + `resolve_fitting_bounds`, the `ForebayTable`
//! VHA interpolation, and the tailrace / hydraulic-loss evaluators
//! (`evaluate_tailrace`, `evaluate_losses`, …).
//! - `production` — the head-conversion constant and `ProductionFunction`, the
//! evaluable `phi(v, q, s)` with analytical derivatives.
//! - `hull_fit` — `fit_hull_planes`, the convex-hull fitter that builds the
//! `(V, Q, generation)` cloud and extracts upper-envelope planes (the production path).
//! Also owns `RawPlane`, the four-coefficient carrier the whole pipeline threads.
//! - `grid` — the single authoritative owner of the uniform grid formula
//! (`GridParams` + `build_grid`), shared by the hull cloud, the α regression,
//! the deviation diagnostic, and the secant's representative-point scan.
//! - `alpha` — `compute_alpha_fpha` + `scale_plane_affine`, the least-squares
//! `α_FPHA` correction and the whole-affine scaling it drives.
//! - `deviation` — `compute_fit_deviation` + `FphaFitDeviation`, the fit-quality
//! measure of the final plane set against the exact production function (the
//! replacement for the retired `kappa` shrink).
//! - `secant` — `fit_gamma_s_for_planes`, the per-plane lateral-flow `γ_S` secant.
//! - `tailrace` — `TailraceFamilies` + `build_tailrace_families_map`, the exact
//! piecewise-quartic backwater-coupled tailrace evaluation.
//! - `reduction` — `reduce_planes`, the post-fit similar-hyperplane merge.
//! - `rng` — the deterministic seeded PRNG and identity hash the `Distance`
//! reduction arm samples with.
//! - `selection` — the coefficient-sign and `α_FPHA > 0` validation
//! (`validate_fitted_planes`).
//!
//! The orchestration entry point `fit_fpha_planes` and its result `FphaFitResult`
//! live here in `mod`, co-located with the re-export surface.
//!
//! The `pub(crate)` symbols this module re-exports from its submodules
//! (`FphaFittingError`, `ForebayTable`, `evaluate_losses`, `evaluate_tailrace`,
//! `TailraceSource`, `TailraceFamilies`, `build_tailrace_families_map`,
//! `FphaFitDeviation`) plus the two defined here in `mod` (`FphaFitResult`,
//! `fit_fpha_planes`) form the `crate::fpha_fitting::Symbol` surface that resolves
//! verbatim for every cross-cluster consumer. This doc names them with backtick
//! spans rather than intra-doc links: a `pub(crate)` module linking to
//! `pub(crate)` items would otherwise risk `rustdoc::private_intra_doc_links`.
use Hydro;
use ;
use crateFphaPlane;
use PlaneReductionConfig;
use ;
use ;
use resolve_fitting_bounds;
use ;
use ProductionFunction;
use reduce_planes;
use fit_gamma_s_for_planes;
use validate_fitted_planes;
pub use ;
pub use FphaFittingError;
pub use ;
pub use TailraceSource;
pub use ;
// ── Top-level fitting pipeline ────────────────────────────────────────────────
/// Combined result of the FPHA fitting pipeline.
///
/// Returned by [`fit_fpha_planes`] to expose the fitted hyperplanes alongside the
/// scalar `alpha` correction the whole affine function was scaled by, so the
/// export-row builder can recover the unscaled raw coefficients.
///
/// # Contract — `planes` carry the α-scaled whole affine function (Voice 1)
///
/// Each plane is `α·FPHA_0`: `intercept`, `gamma_v`, `gamma_q`, and `gamma_s` are
/// all `α · (raw hull coefficient)`. To recover a raw coefficient divide by
/// `alpha` (`plane.intercept / alpha`, etc.). `alpha` is the single factor every
/// coefficient was scaled by — the export row builder writes the α-scaled
/// coefficients verbatim with the IO `kappa` column pinned to `1.0`, so the
/// precomputed read-back (`intercept = gamma_0 · kappa`) round-trips `α·FPHA_0`
/// exactly. Treating the gradients as still-raw and re-applying an intercept-only
/// shrink would double-correct the export.
pub
/// Fit FPHA hyperplanes for a single hydro plant from its VHA curve geometry.
///
/// This is the top-level entry point for the computed FPHA path. It orchestrates
/// the full pipeline:
///
/// 1. **Forebay table** — build `ForebayTable` from the VHA curve rows.
/// 2. **Production function** — build `ProductionFunction` from the forebay table
/// and the hydro plant's tailrace, hydraulic loss, and efficiency models.
/// 3. **Fitting bounds** — resolve volume range and grid counts from the config.
/// 4. **Hull fit** — build the `(V, Q, generation)` production cloud (spillage = 0,
/// lateral = 0) plus the closing point, take the 3-D convex hull, and read its
/// upper-envelope facets as raw planes.
/// 5. **Alpha correction** — compute the least-squares `α_FPHA` on the spill = 0
/// grid and scale every plane's whole affine function by `α`.
/// 6. **Lateral secant** — fit the lateral-flow secant `γ_S` over
/// `lateral_flow ∈ [0, S_max]` on the α-scaled planes (smart default `lateral_flow = S`,
/// `reference lateral flow = 0`; `S_max = 2·long-term mean inflow` or the `2 × max_turbined` fallback).
/// 7. **Validation** — require `α_FPHA > 0` and the coefficient signs (including
/// the fitted `γ_S ≤ 0`).
/// 8. **Conversion** — convert each α-scaled, `γ_S`-fitted `RawPlane` to
/// `FphaPlane` (`intercept = α·gamma_0`, `γ_v` / `γ_q` = `α·gamma_*`, `γ_S`
/// the fitted secant).
///
/// The returned `Vec<FphaPlane>` is structurally identical to what the precomputed
/// path produces from `fpha_hyperplanes.parquet`: the LP builder treats both paths
/// identically.
///
/// # Errors
///
/// Any step in the pipeline can fail. All errors propagate via `?` and are
/// variants of [`FphaFittingError`]. The caller receives a descriptive error
/// that includes the hydro plant name.
///
/// # Parameters
///
/// - `forebay_rows` — VHA curve rows for the hydro plant, sorted ascending by
/// `volume_hm3` (as returned by `cobre_io::extensions::parse_hydro_geometry`).
/// - `hydro` — resolved hydro plant entity supplying physical bounds and models.
/// - `config` — FPHA fitting configuration (grid sizes, optional fitting window).
/// - `long_term_mean_inflow_m3s` — long-term mean natural inflow \[m³/s\] for the lateral-secant
/// `S_max = 2·long-term mean inflow`; `0.0` (no inflow history) selects the `2 × max_turbined`
/// fallback (see `secant::resolve_s_max`).
/// - `tailrace_source` — the resolved [`TailraceSource`]. The caller resolves it
/// per (hydro, entry): [`TailraceSource::Families`] when the plant has a
/// `tailrace_curves` table, else [`TailraceSource::Entity`] mirroring the
/// entity-level model (the inert fallback).
/// - `reduction` — the optional file-level similar-hyperplane reduction config.
/// `Some(cfg)` runs the post-fit merge of near-parallel planes after the first
/// `validate_fitted_planes` and before the `FphaPlane` conversion; `None` is a
/// literal skip — the validated `scaled` set is converted unchanged, so the
/// fit is bit-identical to a run without any reduction config.
/// - `hydro_id` — the plant's `EntityId` `i32` (stable identity). Used only by
/// the `Distance` reduction arm to seed its sampling PRNG; the `Angle` arm and
/// the `None` skip ignore it.
/// - `entry_level_bits` — the per-`SelectionMode`-entry downstream-level bits
/// (`downstream_level_m.map(f64::to_bits).unwrap_or(0)`), the same dedup-key
/// component that identifies the stage/entry. Part of the `Distance` arm's
/// stable seed identity; ignored by the `Angle` arm and the `None` skip.
/// - `collect_deviation_points` — when `true`, after the aggregate is computed
/// the per-sampled-point residuals are collected onto
/// [`FphaFitResult::deviation_points`] for the opt-in deviation-points export.
/// `false` collects nothing (the field stays empty) with zero overhead, so the
/// fit is bit-identical regardless of the flag — the collection is read-only
/// over the already-emitted planes and never feeds back into the fit.
pub