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
//! Single authoritative owner of the uniform `(V, Q)` FPHA fitting grid.
//!
//! Owns [`GridParams`] and [`build_grid`]. The grid formula is shared by the
//! `hull_fit` cloud builder, the `alpha` least-squares regression, and the
//! `secant` representative-point scan; all reach it via `super::grid::build_grid`.
//! Duplicating the formula in any caller would let the cloud grid and the
//! regression grid drift apart, silently invalidating the outer-approximation
//! guarantee.
use FittingBounds;
use ProductionFunction;
// ── Grid construction ─────────────────────────────────────────────────────────
/// Precomputed grid axis values for the fitting `(V, Q)` grid.
///
/// Computed once by [`build_grid`] and reused across all pipeline steps that
/// iterate the same grid (the `hull_fit` cloud builder, the `alpha` regression,
/// and the `secant` representative-point scan). Centralising the formula here
/// eliminates the risk of the call-sites diverging.
//
// Rationale: promoted to `pub(crate)` (with its axis fields) so it remains the
// single grid-formula owner shared across the cloud, regression, and secant
// steps. Inlining a second copy of the axis formula into any caller to avoid the
// cross-submodule call would let the cloud grid and the regression grid drift
// apart — the lint that would force this back to private is the price of keeping
// exactly one owner.
pub
/// Build the uniform `(V, Q)` grid for FPHA fitting.
///
/// Constructs two uniform axis vectors that define the grid used consistently
/// across the `hull_fit` cloud builder, the `alpha` regression, and the `secant`
/// representative-point scan.
///
/// ## Axis formulas
///
/// - **Volume**: `n_volume_points` values from `bounds.v_min` to `bounds.v_max`.
/// - **Flow**: `n_flow_points` values from `0` to `pf.max_turbined_m3s`.
/// The axis starts at `q = 0`, where production is zero (`generation = 0`): that column
/// anchors the cloud at the zero-flow origin and forms its lower closure, so
/// `hull_fit` needs no synthetic closing point.
///
/// Both axes are inclusive at both endpoints. Spillage is not a grid axis: the
/// cloud and the α regression fix `s = 0`, and the lateral secant sweeps its own
/// `S_max` sample independent of `bounds.n_spillage_points`.
//
// Rationale: promoted to `pub(crate)` so the cloud, regression, and secant steps
// call this single definition instead of each holding a private copy of the
// axis formula — see [`GridParams`] for the drift hazard a second copy creates.
pub