Skip to main content

gam_terms/basis/
measure_jet_smooth.rs

1//! Measure-jet spline smooth: multiscale local-jet-residual energy of the
2//! empirical measure (center-quadratured current implementation).
3//!
4//! The term penalizes, at every quadrature point and every scale, the failure
5//! of `f` to be locally affine *in the measure*:
6//!
7//! ```text
8//!   Q = Σ_ℓ  w_ℓ · Σ_i  mass_i · q_i(ε_ℓ)^(1−2α) · R_{i,ℓ},
9//!   w_ℓ = log_step · ε_ℓ^(−η),   η = 2s + d(2−2α),
10//! ```
11//!
12//! where `R_{i,ℓ}` is the residual quadratic form of the exact weighted
13//! local affine projection at center `i` and scale `ε_ℓ`: kernel weights
14//! `w_j = mass_j · exp(−d_ij²/(2ε_ℓ²))`, kernel mass `q_i = Σ_j w_j`, and the
15//! fit `min_b ‖Cv − Φ̃b‖²_W` over weighted-centered values
16//! `Cv = v − (uᵀv)·1` (`u = w/q`) and weighted-centered scaled features
17//! `Φ̃` (rows `(c_j − c_i)/ε`, column means removed under `u`). Rank-deficient
18//! cells use the machine-precision pseudo-inverse of `Φ̃ᵀWΦ̃/q`, so ambient
19//! affine values are projected away exactly instead of paying a ridge toll.
20//!
21//! # Contracts (each is load-bearing; tests pin them)
22//!
23//! - **Exact constant annihilation.** The constant is removed by the weighted
24//!   mean projection `C`, never ridged: `Q·1 = 0` to machine precision at
25//!   every scale, so the penalty carries NO mass term and the fit has no
26//!   prior mean to revert to. This is the no-mean-reversion contract of the
27//!   measure-jet design; ridging the constant would silently reintroduce
28//!   mean reversion.
29//! - **Exact affine projection / rank adaptation.** The slope block uses the
30//!   rank-revealing pseudo-inverse of the dimensionless local Gram
31//!   `G = Φ̃ᵀWΦ̃/q`, not a Tikhonov ridge. On a 1-D filament in ambient
32//!   dimension d the resolved tangent slope is absorbed (not penalized);
33//!   unresolved directions have no variation after weighted centering and do
34//!   not create an affine toll. The retained rank is a numerical property of
35//!   the weighted cell, not a smoothing dial.
36//! - **Mellin band.** Scales form a geometric grid from the center-spacing
37//!   floor to the half-diameter; `w_ℓ = log_step · ε_ℓ^(−η)`, with
38//!   `η = 2s + d(2−2α)`, is the fixed-order quadrature weight used by this
39//!   implementation. It keeps the advertised continuous smoothness order
40//!   `s ∈ (0, 2)` from silently changing when `α` changes.
41//! - **Density normalization.** The outer quadrature weight
42//!   `mass_i · q_i^(1−2α)` realizes `dμ(x)/q_ε(x)^(2α−1)`. On a p-dimensional
43//!   stratum with sampling density `ρ`, `q_ε ~ Cρ ε^p` and the local residual
44//!   contributes an extra `ε^{p(2−2α)}` factor. The fixed-order scale weight
45//!   cancels that factor using the available dimension parameter; without that
46//!   correction, the symbol exponent would be `2s + 2p(α−1)`.
47//! - **Frozen-quadrature replay.** The penalty and extrapolation diagnostic
48//!   depend on the FIT data through center masses, the realized band, on-web
49//!   support anchors, and penalty normalization scales. The freeze step
50//!   persists all of them ([`MeasureJetFrozenQuadrature`]) so predict-time
51//!   rebuilds replay the exact fit-time penalty instead of recomputing it from
52//!   predict rows.
53//! - **Single assembly source.** Every quadratic form this module emits —
54//!   the energy, its (s, α) jets, the per-scale spectrum — is produced by
55//!   ONE workhorse ([`assemble_weighted_forms`]) that walks the local
56//!   residual blocks exactly once per request and differs only in the
57//!   scalar weights applied per block. Criterion value and criterion
58//!   derivatives cannot drift apart (the objective↔gradient desync class is
59//!   structurally excluded).
60//! - **single-scale/multiscale opt-in (#1039/#1116).** The per-scale spectrum
61//!   and the `(α, ln τ)` ψ dials are the multiscale-mode realization, engaged
62//!   ONLY when the spec opts in (`MeasureJetBasisSpec::multiscale = true`, the
63//!   DSL `mjs(…, multiscale=true)`); see [`measure_jet_multiscale_mode`]. There
64//!   is NO center-count auto-gate: at ANY center count the default is
65//!   single-scale — one jet-energy Primary at the auto order with no energy ψ
66//!   dials. The independent function-space null-component candidate requested
67//!   by `double_penalty` is present in either mode and has its own REML λ. The
68//!   flag is persisted on the spec, so freeze→replay re-enters the same mode
69//!   verbatim.
70//!
71//! # ψ-differentiability contract (what the ψ-channel stage consumes)
72//!
73//! Mirroring the constant-curvature κ-contract (#944): centers, masses, the
74//! band are deliberately hyperparameter-FIXED at build time; the representer
75//! range ℓ is the ONE design-moving dial, and it is REML-selected by default
76//! (#1116, restored in #2761) — λ shrinks inside a span and cannot move one, so
77//! the range that decides WHICH span the representers occupy has to be chosen
78//! by the criterion, exactly as the Matérn κ is. Consequences:
79//!
80//! - **Penalty-dial design drift is identically zero**: the (s, α, τ) dials
81//!   reweight only the jet-energy penalty, never the Gaussian representer
82//!   design (`∂X/∂{s,α,τ} ≡ 0`), so those channels are penalty-only
83//!   (`is_penalty_like` auto-derives true in the outer engine's
84//!   `DirectionalHyperParam`).
85//! - **The representer range ℓ is a design-and-pullback-moving dial** (matérn's
86//!   `log_kappa` analog, #1116): `X = K(data, centers; ℓ)·z` and the center
87//!   evaluation map `E = K(centers, centers; ℓ)·z` both depend on ℓ. The
88//!   center-value forms `Q` and `H₀` are ℓ-invariant, but their coefficient
89//!   pullbacks `EᵀQE` and `EᵀH₀E` are not; exact product-rule jets are shipped
90//!   alongside the design jets. ℓ rebuilds the design per outer trial; it does
91//!   not change the frozen basis rank. FD-gated by
92//!   `psi_producer_matches_fd_length_scale`. Frozen only where a design-moving
93//!   kernel scale on covariates SHARED by two coupled blocks is an
94//!   identifiability hazard — the BMS marginal/slope pair, at its own entry
95//!   point (`freeze_measure_jet_length_scale_learning`, #1116/`a3afd17a2`) —
96//!   and where the user pins `length_scale=` outright.
97//! - **Exact (s, α) penalty jets are shipped**:
98//!   [`measure_jet_energy_form_with_jets`] returns `∂Q/∂s`, `∂²Q/∂s²`,
99//!   `∂Q/∂α`, `∂²Q/∂α²`, `∂²Q/∂s∂α` in closed form — both dials enter only
100//!   through the per-block log-weights (`∂ln w/∂s = −2 ln ε`,
101//!   `∂ln w/∂α = −2 ln q`), so the jets are reweighted re-scatters of the
102//!   SAME residual blocks, FD-gated in this module's tests.
103//!   The retained τ coordinate is inert under the exact projection, so its
104//!   derivative slots are identically zero.
105//!
106//! # Cost shape (and the upgrade ladder above it)
107//!
108//! The outer sum is coarsened per scale to a deterministic ε/2-net (the
109//! outer Riemann sum needs resolution ε, not the center-spacing floor), so
110//! the band totals ~O(m²·d) instead of O(L·m³) — the current realization of
111//! the pyramid principle that each scale interacts at its own level. This is
112//! mass-lumped quadrature of the displayed outer integral; it is first-
113//! moment exact for the cell locations and carries the usual
114//! `O(diam²/ε²)` relative scale for smooth Gaussian-weighted functionals,
115//! not an estimand-preserving identity.
116//! The long-form home for the ladder and the substrate contracts is the
117//! frame notes (`docs/measure_jet_frame.md`); its §2 moment substrate is
118//! `measure_jet_moments.rs`, its §5 extrapolation pricing
119//! `measure_jet_predict.rs`.
120
121use ndarray::{Array1, Array2, ArrayView1, ArrayView2, Axis};
122use rayon::prelude::*;
123use serde::{Deserialize, Serialize};
124
125use faer::Side;
126
127use gam_linalg::faer_ndarray::{
128    FaerEigh, FaerSvd, default_rrqr_rank_alpha, rrqr_nullspace_basis,
129};
130
131use super::{
132    AnisoBasisPsiDerivatives, AnisoPenaltyCrossProvider, BasisBuildResult, BasisError,
133    BasisMetadata, CenterStrategy, ConstructiveQuadratic, PenaltyCandidate, PenaltySource,
134    filter_penalty_candidates, normalize_penalty, normalize_penalty_cross_psi_derivative,
135    normalize_penaltywith_psi_derivatives, select_centers_by_strategy, trace_of_product,
136};
137
138/// Truncation radius of the Gaussian profile in units of the scale ε: weights
139/// beyond `3ε` are below `e^{-4.5} ≈ 1.1e-2` of the peak and are dropped from
140/// both the local fit and the `q^(1−2α)` outer weight. This is an absolute
141/// kernel-weight cutoff; using the same truncated q keeps the discrete
142/// functional self-consistent, but it is not a relative tail-error bound.
143pub(crate) const MEASURE_JET_PROFILE_CUTOFF: f64 = 3.0;
144
145/// Relative eigenvalue threshold for rank-revealing pseudo-inverses of local
146/// Gram matrices. Directions at the roundoff floor are treated as unresolved
147/// and excluded from the affine fit.
148pub(crate) const MEASURE_JET_PSEUDOINVERSE_RTOL: f64 = 64.0 * f64::EPSILON;
149
150/// Default continuous smoothness order `s` realized by the `0.0` auto
151/// sentinel. Sits mid-band in the admissible `(0, 2)` for the affine-jet
152/// (r = 2) energy: rough enough to stay pointwise-defined on filaments and
153/// sheets (`s > p/2` for intrinsic `p ≤ 2`), smooth enough to bridge gaps
154/// with attested trends.
155pub(crate) const MEASURE_JET_DEFAULT_ORDER_S: f64 = 1.5;
156
157/// Auto-band scale-count clamp: at least 3 octave-ish nodes so the energy is
158/// genuinely multiscale, at most 8 so degenerate spacing cannot explode the
159/// build.
160pub(crate) const MEASURE_JET_MIN_AUTO_SCALES: usize = 3;
161pub(crate) const MEASURE_JET_MAX_AUTO_SCALES: usize = 8;
162
163/// Representer-range multiple of the median nearest-center spacing used by the
164/// `0.0` auto sentinel.
165///
166/// **This is the SEED of the ℓ outer coordinate, not the realized range.**
167/// [`MeasureJetBasisSpec::learn_length_scale`] is on by default, so what a fit
168/// ships is whatever REML certifies from here; this constant only has to put
169/// the optimizer somewhere feasible and well-conditioned. It is a starting
170/// point in exactly the sense the Matérn `MaternLengthScale::Auto` seed is one.
171///
172/// Set to ×1: a Gaussian representer of range `ℓ = h` (the median
173/// nearest-center spacing) already overlaps its neighbors at
174/// `exp(−h²/(2ℓ²)) = exp(−1/2) ≈ 0.61`, so adjacent bumps blend smoothly while
175/// each center keeps a *distinct* response and the design Gram is far from
176/// rank-deficient. The old ×2 seed made every column `exp(−1/8) ≈ 0.88` at its
177/// neighbor, driving the Gram toward rank deficiency so the inner PIRLS / outer
178/// REML conditioning degraded and the search cycled for hundreds of seconds
179/// (the #1116 timeout). ×1 is the resolving, well-conditioned end of the range
180/// axis — the right side to *start* a search from.
181///
182/// It is emphatically NOT the right place to *stop*. #1041 changed the factor
183/// ×2 → ×1 as a replacement for the ℓ dial it was turning off, on the argument
184/// that a spacing-width kernel "fixes both at the root". Measured on
185/// `measure_jet_perf_parity` at ×1, the design's least-squares span floor
186/// against the noiseless truth is `0.152` and REML moves ℓ 7.5× longer to a
187/// floor of `1.4e-5` (#2761). No fixed multiple of the center spacing is the
188/// answer, because the answer depends on the target's smoothness relative to
189/// the center layout — which is data, not geometry.
190pub(crate) const MEASURE_JET_AUTO_LENGTH_SCALE_FACTOR: f64 = 1.0;
191
192/// Memory budget (in f64 entries) above which the multi-form assembly stops
193/// parallelizing over scales: parallel scale partials cost
194/// `L · n_forms · m²` doubles; past this budget the scales run sequentially
195/// (same numbers — the per-scale loop and the ordered sum are deterministic
196/// either way).
197pub(crate) const MEASURE_JET_PARALLEL_FORM_BUDGET_DOUBLES: usize = 1 << 26;
198
199/// Realized-design identifiability policy for the measure-jet smooth.
200/// Mirrors [`super::ConstantCurvatureIdentifiability`] (#532): the fit-time
201/// section gets the parametric orthogonalization composed onto it by the global
202/// identifiability pipeline, and the composed transform is frozen so
203/// predict-time (and per-ψ-trial) rebuilds replay it verbatim.
204#[derive(Debug, Clone, Serialize, Deserialize, Default)]
205pub enum MeasureJetIdentifiability {
206    /// Fit-time default. With the single-scale affine head, the RBF center
207    /// values are mass-orthogonalized against the affine value space and the
208    /// head passes through exactly; without a head, the representer coefficient
209    /// sum-to-zero section is used. Global parametric residualization follows.
210    #[default]
211    CenterSumToZero,
212    /// Predict-time replay: the frozen composed transform captured at fit
213    /// time. `transform.nrows()` equals representer count plus affine-head width.
214    FrozenTransform { transform: Array2<f64> },
215}
216
217/// Fit-time quadrature of the empirical measure (center masses + realized
218/// scale band), frozen onto the spec so predict-time rebuilds replay the
219/// exact fit-time penalty. Recomputing either from predict rows would
220/// silently change the penalty the coefficients were estimated under.
221#[derive(Debug, Clone, Serialize, Deserialize)]
222pub struct MeasureJetFrozenQuadrature {
223    /// Per-center masses `m_i` (nearest-center fractions of the FIT rows).
224    pub masses: Array1<f64>,
225    /// Realized geometric scale band `ε_0 < … < ε_{L−1}`.
226    pub eps_band: Vec<f64>,
227    /// Per-scale on-web support anchor
228    /// `q̄_ℓ = (Σ_i m_i q_ℓ(c_i)) / (Σ_i m_i)`.
229    pub support_means: Vec<f64>,
230    /// Frobenius scales of the emitted per-level normalized penalties. Empty in
231    /// fused mode, where the band emits one primary penalty instead.
232    pub penalty_normalization_scales: Vec<f64>,
233    /// Frobenius scales of the raw per-level forms before the arbitrary Mellin
234    /// `log_step · ε_ℓ^(-2s0)` gauge is folded in.
235    pub raw_penalty_normalization_scales: Vec<f64>,
236    /// Frobenius scale of the single fused primary penalty. `None` in per-level
237    /// mode.
238    pub fused_penalty_normalization_scale: Option<f64>,
239    /// Ambient input-measurement-error scale `σ_coord` (issue #2225): the
240    /// perpendicular off-manifold residual spread of the fit-time empirical
241    /// measure, in the frozen centers' (standardized) coordinate frame. Frozen
242    /// so the predict-time errors-in-variables variance term
243    /// `Var_input = σ_coord²·‖∇f̂‖²` uses the same input-noise scale the fit
244    /// saw. `None` when it could not be estimated (no cell spanned a tangent),
245    /// leaving `Var_input` disabled. Defaults to `None` for models persisted
246    /// before the term existed.
247    #[serde(default)]
248    pub sigma_coord: Option<f64>,
249}
250
251/// Serde default for [`MeasureJetBasisSpec::learn_length_scale`]: REML-select
252/// ℓ, the same standing the Matérn κ has. A function (not a literal) because
253/// `#[serde(default)]` on a `bool` deserializes a missing field as `false`,
254/// which would silently freeze ℓ on every spec that predates the field — the
255/// opposite of the default. See the field's own docs for the measurement.
256fn measure_jet_learn_length_scale_default() -> bool {
257    true
258}
259
260/// Measure-jet smooth configuration (`mjs(x0, …, xd)`).
261///
262/// The feature columns are ambient coordinates of data concentrated near an
263/// unknown low-dimensional (possibly stratified) set; the term learns the
264/// geometry from the empirical measure itself — centers as quadrature nodes,
265/// masses as μ-weights, local jet residuals as the roughness carrier — with
266/// no graph, mesh, or neighbor-set inside the statistical object.
267#[derive(Debug, Clone, Serialize, Deserialize)]
268pub struct MeasureJetBasisSpec {
269    /// Center/knot selection strategy (deterministic; quadrature of μ).
270    pub center_strategy: CenterStrategy,
271    /// Continuous smoothness order `s ∈ (0, 2)`; `0.0` sentinel = auto
272    /// (`MEASURE_JET_DEFAULT_ORDER_S`).
273    pub order_s: f64,
274    /// Density-normalization exponent α (outer weight `q^{1−2α}`).
275    pub alpha: f64,
276    /// Historical τ coordinate retained for frozen specs and ψ layout. The
277    /// measure-jet energy itself uses the exact weighted affine projection and
278    /// is independent of τ; the τ ψ derivatives are therefore zero.
279    pub tau0: f64,
280    /// Number of scale nodes; `0` sentinel = auto dyadic band.
281    pub num_scales: usize,
282    /// Representer (Gaussian RBF) range ℓ; `0.0` sentinel = auto
283    /// (median nearest-center spacing × `MEASURE_JET_AUTO_LENGTH_SCALE_FACTOR`).
284    pub length_scale: f64,
285    /// Add a separate function-space affine/null-component penalty alongside
286    /// the jet-energy penalty. Its strength is independently REML-selected.
287    pub double_penalty: bool,
288    /// REML-select the representer range ℓ as a design-moving outer dial,
289    /// mirroring Matérn's `log_kappa`. **Default `true`** — ℓ is a basis
290    /// coordinate of the same kind as the Matérn κ, not a smoothing parameter,
291    /// and a fitted model must choose it.
292    ///
293    /// # Why it cannot be a frozen geometric value (#2761)
294    ///
295    /// The Gaussian kernel is strictly PD for every ℓ > 0, so ℓ does NOT change
296    /// the basis rank (always `m` centers) — but it changes WHICH `m`-dim
297    /// subspace the representers span. λ can only shrink inside a span; it
298    /// cannot move one. So a mis-set ℓ is an error no smoothing parameter can
299    /// repair, and the size of that error is not small. Measured on
300    /// `measure_jet_perf_parity`'s 1-D-curve-in-3-D Gaussian fixture
301    /// (`n = 1500`, σ = 0.10, 16 centers, `p = 15`), where `span floor` is the
302    /// least-squares projection residual of the NOISELESS truth onto the
303    /// realized design's column span — the bound no λ can beat:
304    ///
305    /// ```text
306    ///   arm                       ell      edf   span floor  unpen. LS  held-out
307    ///   frozen (auto ell)      0.5144   14.684    0.152488   0.155484   0.155584
308    ///   REML-selected ell      3.8813   14.006    0.000014   0.008155   0.009642
309    ///   matern(k=16)                -   14.619    0.006077   0.011989   0.011639
310    ///   duchon(k=16)                -   15.016    0.002443   0.011308   0.010521
311    /// ```
312    ///
313    /// At the frozen range the fit is already at `edf/p = 0.98` and its held-out
314    /// RMSE *is* the span floor: unpenalized least squares on the same design
315    /// gives 0.1555, and dropping the null-component penalty moves the fourth
316    /// decimal. Freeing ℓ drops the floor by four orders and the held-out RMSE
317    /// by 16x, past both comparators, at LOWER edf — nothing is traded for it.
318    ///
319    /// # History (so a fourth flip needs new evidence)
320    ///
321    /// `299c83ffc` (#1116) introduced this dial default-ON precisely to remove
322    /// this fixture's 13x. `a3afd17a2` then found the one place it is unsafe —
323    /// a BMS fit shares ONE mjs basis between the marginal mean and the
324    /// slope surface, and a design-moving kernel scale on shared covariates
325    /// is an identifiability hazard that reached a separation runaway — and
326    /// contained it AT THE BMS ENTRY POINT with
327    /// [`crate::smooth::freeze_measure_jet_length_scale_learning`], which is
328    /// still what runs there. `b1d94d1a5` (#1041) nevertheless flipped the
329    /// GLOBAL default off, and the 13x returned as #2761. The scoped freeze is
330    /// the correct containment; the global one buys nothing it does not already
331    /// buy and costs every single-surface fit its span alignment.
332    ///
333    /// `false` freezes ℓ at the auto (or explicit) value with no outer
334    /// enrollment. The term builder selects that automatically when the user
335    /// pins `length_scale=` — an explicit range is a request, not a seed —
336    /// mirroring the Matérn `all_spatial_terms_kappa_fixed` short-circuit.
337    #[serde(default = "measure_jet_learn_length_scale_default")]
338    pub learn_length_scale: bool,
339    /// Explicit opt-in for multiscale mode: the per-scale spectral penalty
340    /// split plus the `(α, ln τ)` outer ψ dials. `false` (default) keeps the
341    /// energy in single-scale mode at ANY center count. The separate
342    /// `double_penalty` null component is available in both modes. There is no
343    /// center-count auto-gate; the user opts in via
344    /// `mjs(…, multiscale=true)`. Persisted on the spec so freeze→replay enters
345    /// the same mode.
346    #[serde(default)]
347    pub multiscale: bool,
348    /// Realized-design identifiability policy (see type docs).
349    #[serde(default)]
350    pub identifiability: MeasureJetIdentifiability,
351    /// Fit-time quadrature replay (see type docs). `None` at fit time;
352    /// `Some` on the frozen predict/ψ-trial path.
353    #[serde(default)]
354    pub frozen_quadrature: Option<MeasureJetFrozenQuadrature>,
355}
356
357impl Default for MeasureJetBasisSpec {
358    fn default() -> Self {
359        Self {
360            center_strategy: CenterStrategy::FarthestPoint { num_centers: 50 },
361            order_s: 0.0,
362            // Density-WEIGHTED Hessian energy (the module-header default): the
363            // outer weight is q^{1−2α} = q^{−1} at α = 1. The density-free
364            // variant α = 3/2 gives q^{−2}, which on a low-intrinsic-dimension
365            // stratum (data on a 1-D/2-D manifold embedded in higher ambient d)
366            // makes the local kernel mass q tiny AND spatially varying along
367            // the manifold, so q^{−2} amplifies the penalty unevenly and
368            // over-smooths the high-frequency signal there (MEASURED #1116: on
369            // the 1-D-curve-in-3-D fixture α = 3/2 left mjs ~13× worse than
370            // matérn). α = 1's q^{−1} weighting is far gentler and is the
371            // header-derived default; an explicit `alpha=` still overrides for
372            // genuinely density-free use on a full-dimensional stratum.
373            alpha: 1.0,
374            tau0: 1e-3,
375            num_scales: 0,
376            length_scale: 0.0,
377            double_penalty: true,
378            learn_length_scale: true,
379            multiscale: false,
380            identifiability: MeasureJetIdentifiability::CenterSumToZero,
381            frozen_quadrature: None,
382        }
383    }
384}
385
386/// Realized geometric scale band: `eps` ascending, `log_step` the constant
387/// log-spacing `ln(eps[ℓ+1]/eps[ℓ])` used as the Mellin quadrature weight.
388pub struct MeasureJetBand {
389    pub eps: Vec<f64>,
390    pub log_step: f64,
391}
392
393/// The energy and its exact hyperparameter jets in the live dials. `s` and
394/// `α` enter only through per-block log-weights. The retained `ln τ` slots
395/// are zero because the local fit is the exact weighted affine projection
396/// and no longer depends on τ. All forms are scattered from the SAME local
397/// residual blocks, and the ψ-channel consumes them with zero design drift.
398pub struct MeasureJetEnergyJets {
399    pub q: Array2<f64>,
400    pub dq_ds: Array2<f64>,
401    pub d2q_ds2: Array2<f64>,
402    pub dq_dalpha: Array2<f64>,
403    pub d2q_dalpha2: Array2<f64>,
404    pub d2q_ds_dalpha: Array2<f64>,
405    pub dq_dlogtau: Array2<f64>,
406    pub d2q_dlogtau2: Array2<f64>,
407    pub d2q_ds_dlogtau: Array2<f64>,
408    pub d2q_dalpha_dlogtau: Array2<f64>,
409}
410
411/// Householder vector `u` for the uniform sum-to-zero constraint: the
412/// reflection `H = I − 2uuᵀ` maps `c̄ = 1/√m·1` onto `e₁`, so columns 2..m
413/// of `H` are an orthonormal basis of `1⊥` — the same model space as the
414/// generic RRQR nullspace basis, but with O(rows·m) STRUCTURED application
415/// (`X·z = (X − 2(Xu)uᵀ) minus column 1`) instead of the O(rows·m²)
416/// constraint GEMM that the scale-smoke gate identified as the dominant
417/// build cost.
418pub(crate) fn householder_sum_to_zero_u(m: usize) -> Array1<f64> {
419    let c = 1.0 / (m as f64).sqrt();
420    let mut u = Array1::<f64>::from_elem(m, c);
421    u[0] -= 1.0;
422    let norm = u.dot(&u).sqrt();
423    u.mapv_inplace(|v| v / norm);
424    u
425}
426
427/// Materialize the Householder sum-to-zero basis `z` (m × (m−1)) — columns
428/// 2..m of `H = I − 2uuᵀ` — for the frozen-replay metadata. O(m²), built
429/// once per fit.
430pub(crate) fn householder_sum_to_zero_z(u: &Array1<f64>) -> Array2<f64> {
431    let m = u.len();
432    let mut z = Array2::<f64>::zeros((m, m - 1));
433    for j in 0..(m - 1) {
434        for i in 0..m {
435            let h = if i == j + 1 { 1.0 } else { 0.0 } - 2.0 * u[i] * u[j + 1];
436            z[(i, j)] = h;
437        }
438    }
439    z
440}
441
442pub(crate) fn symmetric_pseudoinverse(
443    a: &Array2<f64>,
444    label: &str,
445) -> Result<Array2<f64>, BasisError> {
446    let n = a.nrows();
447    if a.ncols() != n {
448        crate::bail_dim_basis!(
449            "measure-jet pseudo-inverse `{label}` needs a square matrix, got {:?}",
450            a.dim()
451        );
452    }
453    let (evals, evecs) = a.eigh(Side::Lower).map_err(|e| {
454        BasisError::InvalidInput(format!(
455            "measure-jet pseudo-inverse `{label}` eigendecomposition failed: {e}"
456        ))
457    })?;
458    let lam_max = evals.iter().fold(0.0_f64, |acc, v| acc.max((*v).max(0.0)));
459    let rank_tol = MEASURE_JET_PSEUDOINVERSE_RTOL * (n.max(1) as f64) * lam_max;
460    let mut scaled = evecs.clone();
461    for (k, mut col) in scaled.axis_iter_mut(Axis(1)).enumerate() {
462        let lam = evals[k].max(0.0);
463        let inv = if lam > rank_tol { 1.0 / lam } else { 0.0 };
464        col.mapv_inplace(|v| v * inv);
465    }
466    Ok(scaled.dot(&evecs.t()))
467}
468
469/// Rank-adapted center values of the measure-jet energy's affine null space.
470///
471/// The first column is the constant. The remaining columns are the supported
472/// ambient-linear directions returned by [`measure_jet_affine_head_transform`].
473/// Using that transform makes the basis full-column-rank even when the centers
474/// lie on a lower-dimensional affine stratum of the ambient coordinates.
475fn measure_jet_affine_value_basis(
476    centers: ArrayView2<'_, f64>,
477    masses: ArrayView1<'_, f64>,
478) -> Array2<f64> {
479    // The SAME object the design's head block is built from, evaluated at the
480    // centers. Sharing one construction is what makes "the head spans exactly
481    // the energy's null space" a property of the code rather than a comment
482    // two call sites have to keep agreeing on (#2751).
483    let lift = measure_jet_affine_head_lift(centers, masses);
484    measure_jet_affine_head_block(centers, lift.view())
485}
486
487/// Mass-metric quadratic form selecting the affine/null component of center
488/// function values:
489///
490/// `H₀ = W A (Aᵀ W A)⁺ Aᵀ W`.
491///
492/// This is a function-space object: `vᵀH₀v` is the squared mass norm of the
493/// affine projection of the center values `v`. No coefficient metric enters.
494fn affine_function_nullspace_form(
495    centers: ArrayView2<'_, f64>,
496    masses: ArrayView1<'_, f64>,
497) -> Result<Array2<f64>, BasisError> {
498    let m = centers.nrows();
499    if masses.len() != m {
500        crate::bail_dim_basis!(
501            "measure-jet affine function-space form shape mismatch: centers {:?}, masses {}",
502            centers.dim(),
503            masses.len()
504        );
505    }
506    let affine = measure_jet_affine_value_basis(centers, masses);
507    let mut weighted_affine = affine.clone();
508    for (i, mut row) in weighted_affine.outer_iter_mut().enumerate() {
509        row.mapv_inplace(|v| v * masses[i]);
510    }
511    let affine_gram = affine.t().dot(&weighted_affine);
512    let affine_gram_pinv = symmetric_pseudoinverse(&affine_gram, "affine function-space Gram")?;
513    let form = weighted_affine
514        .dot(&affine_gram_pinv)
515        .dot(&weighted_affine.t());
516    Ok((&form + &form.t()) * 0.5)
517}
518
519/// Pull a center-value quadratic form back through an evaluation map.
520fn pullback_center_form(evaluation: &Array2<f64>, form: &Array2<f64>) -> Array2<f64> {
521    let pulled = evaluation.t().dot(form).dot(evaluation);
522    (&pulled + &pulled.t()) * 0.5
523}
524
525/// Energy factor `F` of a symmetric PSD center-value form: `FᵀF = H` on `H`'s
526/// positive part, rows `√λ_k · u_kᵀ`.
527///
528/// Every eigenvalue that is positive is kept — no rank tolerance enters, so
529/// this is an exact factorization of `H⁺` rather than a truncation. `H` here is
530/// always the output of [`measure_jet_energy_form`] or the affine projector,
531/// both of which are PSD by construction, so the dropped part is roundoff.
532fn psd_energy_factor(form: &Array2<f64>, context: &str) -> Result<Array2<f64>, BasisError> {
533    let m = form.nrows();
534    if m == 0 {
535        return Ok(Array2::<f64>::zeros((0, 0)));
536    }
537    let sym = (form + &form.t()) * 0.5;
538    let (evals, evecs) = sym.eigh(Side::Lower).map_err(|e| {
539        BasisError::InvalidInput(format!(
540            "measure-jet energy factorization `{context}` eigendecomposition failed: {e}"
541        ))
542    })?;
543    let kept: Vec<usize> = evals
544        .iter()
545        .enumerate()
546        .filter_map(|(index, &value)| (value > 0.0).then_some(index))
547        .collect();
548    let mut factor = Array2::<f64>::zeros((kept.len(), m));
549    for (row, index) in kept.into_iter().enumerate() {
550        let scale = evals[index].sqrt();
551        for column in 0..m {
552            factor[[row, column]] = scale * evecs[[column, index]];
553        }
554    }
555    Ok(factor)
556}
557
558/// Pull a PSD center-value form back through an evaluation map CONSTRUCTIVELY:
559/// `EᵀHE = (F E)ᵀ (F E)` for `FᵀF = H`.
560///
561/// The dense route (`pullback_center_form` + `try_from_dense_psd`) is a false
562/// refusal waiting to happen and #2761 measured it firing: the Gaussian
563/// representers go collinear as the range ℓ grows, `E`'s condition number blows
564/// up, and the triple product loses exactly the digits that keep the smallest
565/// eigenvalue non-negative. On `measure_jet_perf_parity` the whole design then
566/// refuses to build for every `ℓ ≳ 2.8` —
567///
568/// ```text
569///   ell   2.15059  builds        reml -1279.00091366   (still descending)
570///   ell   2.79577  REFUSED  min eigenvalue -9.266e-9
571///   ell   7.98500  REFUSED  min eigenvalue -2.864e-5
572/// ```
573///
574/// — while the criterion is still descending at the last `ℓ` that builds, which
575/// is what the outer search reports as `StepSizeTooSmall after 50 attempt(s)`:
576/// its descent direction points into a region where the objective cannot be
577/// EVALUATED. `-4e-5` relative on a Frobenius-normalized matrix is cancellation,
578/// not negative curvature, and the refusal's own guidance says so ("supply the
579/// native energy factor for a PSD function penalty").
580///
581/// Going through the factor makes PSD-ness structural instead of a numerical
582/// accident, and never squares `E`'s condition number in the middle product.
583/// The module's null-component penalty already worked this way
584/// ([`affine_function_nullspace_quadratic`]); the energy Primary was the
585/// sibling that did not.
586fn constructive_pullback_center_form(
587    evaluation: &Array2<f64>,
588    form: &Array2<f64>,
589    context: &str,
590) -> Result<ConstructiveQuadratic, BasisError> {
591    let factor = psd_energy_factor(form, context)?;
592    ConstructiveQuadratic::from_energy_factor(factor.dot(evaluation), context)
593}
594
595/// Congruence of a jet onto a frame: `F (Fᵀ J F) Fᵀ`.
596///
597/// This is the exact derivative of `R(ψ) = N M(ψ) Nᵀ`, `M = Nᵀ S(ψ) N`, for a
598/// ψ-FIXED frame `N` — which is what a declared structural null frame is. An
599/// empty frame gives an exact zero, matching a rebuild that declined.
600fn restrict_jet_to_frame(jet: &Array2<f64>, frame: &Array2<f64>) -> Array2<f64> {
601    if frame.ncols() == 0 {
602        return Array2::<f64>::zeros(jet.dim());
603    }
604    let inner = frame.t().dot(jet).dot(frame);
605    let restricted = frame.dot(&inner).dot(&frame.t());
606    (&restricted + &restricted.t()) * 0.5
607}
608
609/// Frobenius scale of a constructive quadratic, with the same degenerate
610/// convention `normalize_penalty` uses: a scale at or below `1e-12` reports
611/// `1e-12` so the division can never blow up.
612fn constructive_frobenius_scale(quadratic: &ConstructiveQuadratic) -> f64 {
613    quadratic
614        .dense()
615        .iter()
616        .map(|value| value * value)
617        .sum::<f64>()
618        .sqrt()
619        .max(1e-12)
620}
621
622/// First and diagonal-second `u = ln ℓ` derivatives of `E(u)ᵀ H E(u)` for a
623/// `u`-invariant center-value form `H`.
624fn pullback_center_form_log_length_jets(
625    evaluation: &Array2<f64>,
626    evaluation_first: &Array2<f64>,
627    evaluation_second: &Array2<f64>,
628    form: &Array2<f64>,
629) -> (Array2<f64>, Array2<f64>) {
630    let h_e = form.dot(evaluation);
631    let h_e_first = form.dot(evaluation_first);
632    let h_e_second = form.dot(evaluation_second);
633    let first_raw = evaluation_first.t().dot(&h_e) + evaluation.t().dot(&h_e_first);
634    let second_raw = evaluation_second.t().dot(&h_e)
635        + evaluation.t().dot(&h_e_second)
636        + evaluation_first.t().dot(&h_e_first) * 2.0;
637    (
638        (&first_raw + &first_raw.t()) * 0.5,
639        (&second_raw + &second_raw.t()) * 0.5,
640    )
641}
642
643/// Mixed derivative `∂²(EᵀH(ψ)E)/(∂lnℓ ∂ψ)` when only `E` depends on `ℓ`.
644fn pullback_center_form_log_length_cross(
645    evaluation: &Array2<f64>,
646    evaluation_first: &Array2<f64>,
647    form_first: &Array2<f64>,
648) -> Array2<f64> {
649    let h_e = form_first.dot(evaluation);
650    let h_e_first = form_first.dot(evaluation_first);
651    let cross_raw = evaluation_first.t().dot(&h_e) + evaluation.t().dot(&h_e_first);
652    (&cross_raw + &cross_raw.t()) * 0.5
653}
654
655/// The Primary energy's structural null frame in whatever coefficient chart
656/// `z` realizes: the coefficients whose REPRESENTER block vanishes, i.e. the
657/// pure ambient-affine-head directions.
658///
659/// This is a theorem of the construction, not a measurement, and it is the
660/// reason the double-penalty topology can be ψ-invariant (#2445's mechanism,
661/// applied here for #2761):
662///
663/// * the energy annihilates ambient-affine center values EXACTLY (the module's
664///   no-mass / exact-affine-projection contract), so a coefficient whose center
665///   values are `head_cc·b_head` — pure affine — is annihilated for EVERY `ℓ`;
666/// * the single-scale gauge restricts the representer block to `null(AᵀW K_cc)`,
667///   so its center values are mass-orthogonal to the affine space. A nonzero
668///   representer part therefore cannot land in the energy's null space:
669///   `K_cc z_rbf b_rbf ∈ A ∩ A^⊥ = {0}` forces `z_rbf b_rbf = 0`.
670///
671/// So `null(Primary) = { b : (z·b)|representer rows = 0 }` exactly, in the
672/// fit-time chart AND in any composed frozen chart, because both statements are
673/// about `z` alone. Nothing here reads `ℓ`.
674///
675/// Without the declaration the topology is decided by a rank test on the
676/// pullback `kzᵀ Q kz`, whose numerical rank falls as the range grows and the
677/// representers go collinear (measured on a 1-D 50-center term: rank 47 of 48 at
678/// the auto seed, 14 of 48 at 8x it). A design-moving `ℓ` then adds or removes
679/// the double-penalty ridge between outer trials — the `incremental realizer
680/// topology changed` abort, and the `#860` penalty-count desync class.
681///
682/// Returns `None` in multiscale mode, where there is no affine head: the
683/// energy's null space is then a genuinely `ℓ`-dependent subspace of the
684/// representer block, and declaring an EMPTY frame would assert the opposite of
685/// the truth (that the Primary has no null space at all) and silently delete the
686/// null component. Measuring is the honest fallback there.
687fn measure_jet_primary_structural_null_frame(
688    z: &Array2<f64>,
689    representer_count: usize,
690    head_rank: usize,
691) -> Result<Option<Array2<f64>>, BasisError> {
692    if head_rank == 0 || representer_count == 0 || z.ncols() == 0 {
693        return Ok(None);
694    }
695    // `rrqr_nullspace_basis(B)` returns `null(Bᵀ)`, so pass the transpose of the
696    // representer rows to get the coefficient-space null vectors.
697    let representer_rows = z.slice(ndarray::s![..representer_count, ..]).to_owned();
698    let (frame, _) =
699        rrqr_nullspace_basis(&representer_rows.t().to_owned(), default_rrqr_rank_alpha())
700            .map_err(BasisError::LinalgError)?;
701    // An EMPTY frame is a declaration, not a missing one: it says the chart has
702    // absorbed every affine-head direction, so the Primary has no null space
703    // here. That is the answer in a composed frozen chart, where the global
704    // parametric orthogonalization has already taken the head — and it is
705    // exactly the chart every ψ trial rebuilds in. Returning `None` there would
706    // send the topology decision back to the rank test the declaration exists
707    // to replace, reinstating the ℓ-dependence on the one path that cannot
708    // tolerate it. `None` is reserved for "this construction does not apply"
709    // (multiscale, no head).
710    Ok(Some(frame))
711}
712
713/// Fixed-rank constructive witness for the affine/null quadratic in center-value
714/// space. Rank is decided here, where `H₀` is independent of `ℓ`, rather than
715/// after its coefficient pullback has acquired `ℓ`-dependent roundoff modes.
716fn affine_function_nullspace_center_quadratic(
717    centers: ArrayView2<'_, f64>,
718    masses: ArrayView1<'_, f64>,
719) -> Result<ConstructiveQuadratic, BasisError> {
720    ConstructiveQuadratic::try_from_dense_psd(
721        affine_function_nullspace_form(centers, masses)?,
722        "measure-jet affine center-value form",
723    )
724}
725
726fn affine_function_nullspace_quadratic(
727    evaluation: &Array2<f64>,
728    centers: ArrayView2<'_, f64>,
729    masses: ArrayView1<'_, f64>,
730) -> Result<ConstructiveQuadratic, BasisError> {
731    if evaluation.nrows() != centers.nrows() {
732        crate::bail_dim_basis!(
733            "measure-jet affine function-space penalty shape mismatch: evaluation {:?}, centers {:?}",
734            evaluation.dim(),
735            centers.dim()
736        );
737    }
738    let center_quadratic = affine_function_nullspace_center_quadratic(centers, masses)?;
739    ConstructiveQuadratic::from_energy_factor(
740        center_quadratic.factor().dot(evaluation),
741        "measure-jet affine/null coefficient penalty",
742    )
743}
744
745/// Pairwise squared distances `‖a_i − b_j‖²` via the GEMM identity
746/// `‖a − b‖² = ‖a‖² + ‖b‖² − 2·aᵀb`: one (n×d)·(d×m) matrix product carries
747/// every FMA at tile speed instead of n·m scalar distance loops — the
748/// machine-native form of this kernel, and the module's ONLY distance
749/// source (representer design, support curve, and the center-pair geometry:
750/// band floor, median spacing, ε/2-net, neighbor cutoffs). The cancellation
751/// error near-coincident points pay is O(ε_f64·‖x‖²) ABSOLUTE, harmless
752/// under a Gaussian profile (the kernel is flat at d ≈ 0); clamped at zero
753/// so roundoff cannot emit tiny negatives (the a = b diagonal therefore
754/// lands at roundoff scale, not an exact 0 — no caller pins it).
755pub(crate) fn pairwise_sq_dists(a: ArrayView2<'_, f64>, b: ArrayView2<'_, f64>) -> Array2<f64> {
756    let an: Vec<f64> = a.outer_iter().map(|r| r.dot(&r)).collect();
757    let bn: Vec<f64> = b.outer_iter().map(|r| r.dot(&r)).collect();
758    let mut g = a.dot(&b.t());
759    g.axis_iter_mut(Axis(0))
760        .into_par_iter()
761        .enumerate()
762        .for_each(|(i, mut row)| {
763            for (j, v) in row.iter_mut().enumerate() {
764                *v = (an[i] + bn[j] - 2.0 * *v).max(0.0);
765            }
766        });
767    g
768}
769
770/// Row-block size for streaming GEMM passes that must not materialize the
771/// full n×m distance matrix (nearest-node assignment): 64Ki rows × m ≤ a
772/// few hundred MB of transient per block, GEMM-speed throughout.
773pub(crate) const MEASURE_JET_ASSIGN_BLOCK_ROWS: usize = 65_536;
774
775pub(crate) fn validate_finite_points(
776    points: ArrayView2<'_, f64>,
777    what: &str,
778) -> Result<(), BasisError> {
779    for (i, row) in points.outer_iter().enumerate() {
780        if row.iter().any(|v| !v.is_finite()) {
781            crate::bail_invalid_basis!("measure-jet {what} row {i} has a non-finite coordinate");
782        }
783    }
784    Ok(())
785}
786
787/// Median nearest-OTHER-center distance — the resolution floor of the center
788/// quadrature, used for the band floor and the auto representer range.
789pub(crate) fn median_nearest_center_spacing(dist2: &Array2<f64>) -> Result<f64, BasisError> {
790    let m = dist2.nrows();
791    if m < 2 {
792        return Err(BasisError::InsufficientColumnsForConstraint { found: m });
793    }
794    let mut nearest: Vec<f64> = Vec::with_capacity(m);
795    for i in 0..m {
796        let mut best = f64::INFINITY;
797        for j in 0..m {
798            if j != i && dist2[(i, j)] < best {
799                best = dist2[(i, j)];
800            }
801        }
802        nearest.push(best.sqrt());
803    }
804    nearest.sort_by(|a, b| a.partial_cmp(b).expect("finite center spacings"));
805    let median = nearest[nearest.len() / 2];
806    if !(median.is_finite() && median > 0.0) {
807        crate::bail_invalid_basis!(
808            "measure-jet centers are degenerate (median nearest-center spacing = {median}); \
809             duplicate centers cannot carry a scale band"
810        );
811    }
812    Ok(median)
813}
814
815/// Make the representer coefficient section a BASIS of its span at the realized
816/// range, not merely a spanning set (gam#2750).
817///
818/// ## What goes wrong without this
819///
820/// A Gaussian kernel's spectrum decays super-exponentially in `ℓ/spacing`, so
821/// the center evaluation map `E = K_cc · Z_rbf` — through which BOTH the design
822/// and every penalty pullback pass — loses conditioning fast as the representer
823/// range grows. The raw section is chosen for head-orthogonality alone and
824/// keeps every direction, so the shipped chart carries directions the criterion
825/// cannot resolve, and the criterion's own Occam term is the first casualty:
826///
827/// ```text
828///   ell     cond(S)     d log|S|+/d ln ell:  analytic       FD        gap
829///   1.21    1.2e10                          -109.843    -109.829    -0.014
830///   1.57    3.0e11                          -107.924    -107.919    -0.005
831///   2.04    3.7e13                           -97.252     -96.331    -0.922
832///   2.65    3.8e13                           -51.836     -64.765   +12.930
833///   3.45    6.5e12  (rank 14 -> 13)          -26.621     -40.633   +14.012
834/// ```
835///
836/// (16 centers on the `measure_jet_perf_parity` geometry, chart frozen so the
837/// finite difference and the analytic jet differentiate the same object; the
838/// jet itself agrees with a central difference of the shipped penalty to
839/// `1e-8` throughout, so the producer is exact and it is `log|S|₊` that stops
840/// being a function.) Once `cond(S)` reaches the rank-classification floor the
841/// kept spectrum bottoms out in roundoff: `log|S|₊` is then a sum over
842/// directions whose logs are noise, `tr(S⁺Ṡ)` divides by them, and the outer
843/// search is handed a direction that is not a descent direction of its own
844/// objective. That is the wall the `ln ℓ` coordinate has been hitting.
845///
846/// ## The section
847///
848/// Whiten against the section's own center evaluation map,
849/// `E = K_cc Z = U Σ Vᵀ`: take `Z ← Z · V · diag(1/max(σ, √ε·‖K_cc‖))`, so the
850/// realized section satisfies `EᵀE = I` wherever `E` is resolvable and is
851/// *damped rather than deleted* below that. Two questions, two bars, and they
852/// are not the same question — but they share one anchor, and the anchor is
853/// `‖K_cc‖₂`, the norm of the operator whose product formed `E`, never `E`'s
854/// own largest singular value (which collapses; see the table at the bars):
855///
856/// * **Does the direction exist?** `σ > ε·‖K_cc‖·max(dim)` — the backward-error
857///   bar of forming `E = K_cc·Z`. Below it there is no direction, only the
858///   roundoff of that product.
859/// * **How far may it be amplified?** `1/σ` is also the factor by which
860///   direction `i` amplifies the design's own roundoff, and the criterion
861///   squares the design into `XᵀWX`, so the amplification survives that Gram
862///   with significant digits exactly when `ε·(‖K_cc‖/σ_i)² < 1`. Hence the
863///   scaling — not the membership — is floored at `√ε·‖K_cc‖`.
864///
865/// **Damping instead of deleting is what makes the width `ℓ`-invariant.** A
866/// span is invariant under ANY invertible diagonal rescaling, so a damped
867/// direction contributes exactly the same column space as an undamped one; it
868/// simply enters the design with a small norm instead of an amplified one, and
869/// carries its own roundoff in at that same small norm. The realized chart
870/// therefore keeps every direction `E` has, at every `ℓ`, while
871/// `cond(E·W) ≤ √ε·cond(E)` is bounded by construction.
872///
873/// * The rescaling is a pure change of coefficient chart. The profiled
874///   criterion is invariant under an invertible reparameterization (`X → XT`,
875///   `S → TᵀST` moves `log|XᵀWX + λS|` and `log|λS|₊` by the same
876///   `2 ln|det T|`), so it changes no estimate — only the arithmetic.
877/// * **The whitening removes the squaring from the PENALTY, exactly.** With
878///   `EᵀE = I` the energy pullback is `S = UᵀQU` with `U` orthonormal, whose
879///   spectrum is bounded by `Q`'s; and `Q` is `ℓ`-INVARIANT (a form on center
880///   VALUES), so `cond(S)` stops being a function of the range at all.
881///   Measured, re-realizing the chart at each `ℓ` on the #2761 fixture:
882///   `cond(E) = 1.0`, `cond(S₊) = 21.1`, `log|S|₊ = 0.800` from `1×` to `16×`
883///   the seed range. So "the energy pullback squares `E`" — the reason the
884///   previous cut gave for being where it was — does not survive its own
885///   remedy.
886/// * **What the whitening does NOT remove is the squaring in `XᵀWX`.** The
887///   chart lives at the CENTERS; the design lives at the DATA, and `1/σ_i` is
888///   also the factor by which direction `i` amplifies the design's own
889///   roundoff. The criterion squares the design, so a retained direction
890///   survives with significant digits exactly when `ε·(σ_max/σ_i)² < 1`. That
891///   is where the half-mantissa belongs, and it is one half-mantissa, not two.
892/// * **The previous cut spent the half-mantissa twice, on an already-squared
893///   quantity — and spent it by DELETING.** It whitened against `G = EᵀE` and
894///   cut at `√ε·λ_max(G)`; since `λ = σ²` that is `σ > ε^{1/4}·σ_max`, i.e.
895///   `cond(E) ≤ ε^{-1/4} ≈ 8·10³`. Measured on the #2761 fixture at 16 centers,
896///   that deleted most of the span at the ranges REML actively selects:
897///
898/// ```text
899///   ℓ/ℓ_seed  cond(E)   ε^{1/4} DELETE: p  span floor    √ε DAMP: p  span floor
900///      1      3.0e+01         12            6.11e-2          12       6.11e-2
901///      2      2.8e+04         11            2.43e-2          12       1.94e-2
902///      4      4.2e+07          8            1.50e-2          12       2.10e-3
903///      8      9.1e+09          6            1.67e-2          12       1.81e-4   <- 92x
904///     16      2.7e+11          4            8.92e-2          12       3.54e-5
905/// ```
906///
907///   `span floor` is the least-squares residual RMSE of the NOISELESS truth on
908///   the realized design's own column span — the bound no `λ` can beat, since
909///   `λ` shrinks inside a span and never moves one. Note the `ε^{1/4}` floor
910///   going UP past `4×`: that chart is worse than the seed range it was meant
911///   to improve on. The damped column keeps the whole span (`1.81e-4` at `8×`
912///   reproduces an 80-digit projection of the same span to every digit) at a
913///   width that does not move with the dial, which a DELETING bar cannot do at
914///   any threshold — `12,11,8,6,4` for `ε^{1/4}` and `12,12,12,11,10` even for
915///   `√ε`.
916///
917/// Realized per cold build from `K_cc(ℓ)`, so the chart tracks the dial it is a
918/// chart for; a frozen-quadrature replay reuses the composed transform verbatim.
919fn condition_representer_section(
920    k_cc: &Array2<f64>,
921    z_rbf: &Array2<f64>,
922) -> Result<Array2<f64>, BasisError> {
923    if z_rbf.ncols() == 0 {
924        return Ok(z_rbf.clone());
925    }
926    let evaluation = k_cc.dot(z_rbf);
927    let (_, singular, right) = evaluation.svd(false, true).map_err(BasisError::LinalgError)?;
928    let leading = singular.iter().copied().fold(0.0_f64, f64::max);
929    if !(leading.is_finite() && leading > 0.0) {
930        return Ok(z_rbf.clone());
931    }
932    // `right` is `Vᵀ`: row `i` is `σ_i`'s right singular vector, in the
933    // coefficient coordinates of `z_rbf`.
934    let right = right.ok_or_else(|| {
935        BasisError::LinalgError(gam_linalg::faer_ndarray::FaerLinalgError::SvdNoConvergence {
936            context: "measure-jet representer section: right singular vectors were not returned",
937        })
938    })?;
939    // Both bars are anchored on `‖K_cc‖₂`, NOT on `σ_max(E)`. `E = K_cc·Z` with
940    // orthonormal `Z`, so the computed `E` carries a backward error
941    // `O(ε·‖K_cc‖₂)` whatever `E`'s own size turns out to be — and `σ_max(E)`
942    // is not a fixed fraction of `‖K_cc‖₂`: it COLLAPSES as `ℓ` grows, because
943    // the constraint that makes `Z` head-orthogonal is exactly what annihilates
944    // the flat limit `K_cc → 𝟙𝟙ᵀ`. Measured on the 1-D sweep fixture (50
945    // centers, standardized `x`):
946    //
947    // ```text
948    //   ℓ      ‖K_cc‖₂  σ_max(E)  σ_max/‖K‖  σ_min(E)
949    //   0.05     2.4     2.29e+0   9.6e-1     5.5e-2
950    //   0.55    17.2     8.79e+0   5.1e-1     3.2e-16   <- σ_min is the roundoff
951    //   6.09    48.4     1.49e-2   3.1e-4     4.5e-17      floor, and it is FLAT
952    //  49.73    50.0     3.48e-6   7.0e-8     3.5e-17
953    // ```
954    //
955    // A bar relative to `σ_max(E)` therefore falls BELOW that flat roundoff
956    // floor once the range is long, and admits the floor itself as signal: at
957    // `ℓ = 11` a `ε·σ_max·dim` bar is `1.5e-17` against entries of `1.7e-17`,
958    // so all 48 directions "pass" and the chart hands the fit 48 columns of
959    // pure rounding noise. Those columns fit anything, so the criterion has a
960    // spurious minimum out there — measured at `V = −447.8` on this fixture's
961    // sweep case 1 against `≈ −44` in the honest region, which is precisely
962    // where its outer search was terminating.
963    let dimension_factor = evaluation.nrows().max(evaluation.ncols()) as f64;
964    let (_, kernel_singular, _) = k_cc.svd(false, false).map_err(BasisError::LinalgError)?;
965    let anchor = kernel_singular
966        .iter()
967        .copied()
968        .fold(0.0_f64, f64::max)
969        .max(leading);
970    // Membership: the backward-error bar of the product that formed `E`. Below
971    // it there is no direction, only the roundoff of `K_cc·Z`.
972    let existence = anchor * f64::EPSILON * dimension_factor;
973    // Amplification: ONE half-mantissa, spent ONCE. `1/σ_i` is the factor by
974    // which direction `i` lifts the design's own roundoff — which is set by
975    // `‖K_xc‖ ∼ ‖K_cc‖`, not by `σ_max(E)` — and the criterion squares the
976    // design into `XᵀWX`, so the lift survives that Gram with significant
977    // digits exactly when `ε·(‖K_cc‖/σ_i)² < 1`. Below `√ε·‖K_cc‖` the
978    // direction is DAMPED to that scaling rather than dropped: the span is
979    // unchanged (any invertible rescaling spans the same columns) and the
980    // roundoff comes in at the damped norm instead of an amplified one.
981    //
982    // This also reproduces the range bracket's own physics for free. As `ℓ`
983    // passes the node diameter, `σ_max(E)` falls under the floor and the WHOLE
984    // representer block damps smoothly toward zero, leaving the affine head —
985    // which is exactly `MeasureJetRangeBracket::ceiling`'s statement that past
986    // there "the block is numerically one function plus the affine head and
987    // there is no distinct model past it", now realized by the arithmetic
988    // rather than asserted next to it.
989    let amplification_floor = anchor * f64::EPSILON.sqrt();
990    // Visibility: a damped direction enters the energy pullback
991    // `S = (E·W)ᵀQ(E·W)` with squared weight `(σ_i/floor)²`, so if that weight
992    // drops below the canonical penalty-spectrum rank cutoff the direction is
993    // classified UNPENALIZED — an accidentally free design direction, which is
994    // the opposite of conservative. It also makes `log|S|₊` a step function of
995    // `ℓ`: measured on the 1-D sweep fixture, the primary's nullity flapping by
996    // one moved the profiled criterion by `8.5` at fixed `λ`, which is what a
997    // `ln ℓ` line search cannot cross.
998    //
999    // So the chart's retention bar is at least as strict as the penalty's own:
1000    // keep direction `i` only while `(σ_i/floor)² > tol`, with `tol` the same
1001    // `spectral_tolerance` convention (#1425's single classifier) every other
1002    // penalty-spectrum consumer reads. No second constant, and the two
1003    // decisions can no longer disagree about which directions are penalized.
1004    let rank_tolerance =
1005        z_rbf.ncols().max(1) as f64 * super::bspline_build::SPECTRAL_RANK_RELATIVE_TOLERANCE;
1006    let visibility = amplification_floor * rank_tolerance.sqrt();
1007    let retention = existence.max(visibility);
1008    let kept: Vec<usize> = (0..singular.len())
1009        .filter(|&i| singular[i] > retention)
1010        .collect();
1011    let kept = if kept.is_empty() {
1012        // Every representer direction is below the resolvable floor. Keep the
1013        // single strongest one rather than emitting an empty block: a term with
1014        // no representer columns is a different model, and that decision
1015        // belongs to the range screen, not to a conditioning step.
1016        vec![(0..singular.len()).fold(0usize, |best, i| {
1017            if singular[i] > singular[best] { i } else { best }
1018        })]
1019    } else {
1020        kept
1021    };
1022    let mut transform = Array2::<f64>::zeros((z_rbf.ncols(), kept.len()));
1023    for (column, &index) in kept.iter().enumerate() {
1024        let inverse = singular[index]
1025            .max(amplification_floor)
1026            .max(f64::MIN_POSITIVE)
1027            .recip();
1028        // Sign gauge: a singular vector is defined up to sign, and the sign a
1029        // decomposition happens to return is not a property of the geometry.
1030        // Pin it on the entry of largest magnitude so the realized chart is
1031        // reproducible across faer revisions and platforms.
1032        let mut pivot = 0usize;
1033        for row in 1..z_rbf.ncols() {
1034            if right[(index, row)].abs() > right[(index, pivot)].abs() {
1035                pivot = row;
1036            }
1037        }
1038        let sign = if right[(index, pivot)] < 0.0 { -1.0 } else { 1.0 };
1039        for row in 0..z_rbf.ncols() {
1040            transform[(row, column)] = sign * right[(index, row)] * inverse;
1041        }
1042    }
1043    Ok(z_rbf.dot(&transform))
1044}
1045
1046/// Axis-aligned bounding-box diagonal of a point set — the deterministic
1047/// diameter proxy the scale band and the range bracket both measure the
1048/// configuration's extent with. `O(m·d)` and permutation-invariant, unlike a
1049/// max-pairwise-distance scan.
1050pub(crate) fn bounding_box_diagonal(points: ArrayView2<'_, f64>) -> f64 {
1051    let mut diag2 = 0.0_f64;
1052    for k in 0..points.ncols() {
1053        let col = points.column(k);
1054        let mut lo = f64::INFINITY;
1055        let mut hi = f64::NEG_INFINITY;
1056        for &v in col.iter() {
1057            lo = lo.min(v);
1058            hi = hi.max(v);
1059        }
1060        if lo.is_finite() && hi.is_finite() {
1061            diag2 += (hi - lo) * (hi - lo);
1062        }
1063    }
1064    diag2.sqrt()
1065}
1066
1067/// The deterministic bracket the representer range `ℓ` is SCREENED over before
1068/// the outer ψ search refines it, in the STANDARDIZED frame the basis is
1069/// realized in (gam#2750).
1070///
1071/// ## Why a bracket exists at all
1072///
1073/// `ℓ` is a design-moving coordinate: it decides WHICH span the representers
1074/// occupy, and the outer search reaches it by local descent from the seed. The
1075/// profiled criterion in `ln ℓ` is not unimodal — as `ℓ` grows past a few
1076/// center spacings the Gaussian columns become collinear, the rank-revealing
1077/// identifiability section drops columns, and the criterion steps. Measured on
1078/// `measure_jet_formula_fit_robustness_sweep` seed 1 (n = 200, one sine cycle):
1079/// a local minimum at the auto range `ℓ = 0.020` (`V = −234.5`), a barrier at
1080/// `ℓ = 0.035` (`V = −231.1`), and the GLOBAL minimum at `ℓ = 0.80`
1081/// (`V = −256.3`) — 21.7 log units deeper, with held-out RMSE `0.0084` against
1082/// `0.0175`, i.e. the same basis fitting 2.1× better and beating `tp` on both
1083/// the criterion and the truth instead of losing on both. A local descent
1084/// seeded inside the first basin cannot cross that barrier, and the frozen
1085/// coefficient chart the ψ trials rebuild in stops being evaluable ~1.6× past
1086/// the seed, so the search terminates essentially where it started. The λ that
1087/// comes back is then a faithful readout of a range nothing could move — which
1088/// is the "1-D fits select a too-large λ" this bracket exists to end.
1089///
1090/// ## Why THIS bracket
1091///
1092/// The nodes are the term's own realized scale band, verbatim. That is not a
1093/// coincidence of convenience: the energy's `ε` and the representer range `ℓ`
1094/// are the same physical quantity — a length in the chart — and the band is
1095/// already derived, not chosen: its floor is the median nearest-node spacing
1096/// (below it neighbouring representers stop overlapping and the design is a
1097/// bump-per-node indicator with no partition of unity) and its ceiling is half
1098/// the node bounding-box diagonal, at the band's own auto-clamped resolution.
1099/// So the screen introduces no length, no count and no step of its own.
1100///
1101/// ## Where a walk past the top node stops (#2761)
1102///
1103/// [`MeasureJetRangeBracket::feasibility_ceiling`] — the range at which the
1104/// closest node pair stops being distinguishable in the chart's own arithmetic,
1105/// [`measure_jet_range_feasibility_ceiling`]. It is the SAME wall
1106/// [`measure_jet_ln_range_window`] gives the outer search, and it is the same
1107/// wall for the same reason: a stopping rule may not be tighter than the model.
1108///
1109/// It used to be [`MeasureJetRangeBracket::node_diameter`], on the argument
1110/// that at `ℓ` that long every pair of representers overlaps at `≥ exp(−1/2)`
1111/// so "there is no distinct model past it". That argument is measurably wrong,
1112/// and the tree said so in two places before this: `measure_jet_ln_range_window`
1113/// records that *"the profiled criterion genuinely prefers a range AT or ABOVE
1114/// the node diameter"* on three fixtures, and
1115/// `the_search_window_reaches_past_where_the_screen_stops_walking` pins the
1116/// search window as strictly wider. Those reconcile only while something else
1117/// keeps searching past the stopping rule. On a term whose `ℓ` dial is FROZEN —
1118/// the BMS marginal/slope pair, or any `learn_length_scale=false` — nothing
1119/// does, and the stopping rule becomes the wall.
1120///
1121/// Measured on the #1041 parity fixture (`m = 10`, extent `[2.671, 2.726]`):
1122/// band `[1.08074, 1.43607, 1.90823]`, `log_step = 0.284265`, node diameter
1123/// `3.81645`. The screen's chosen range for the marginal surface was `3.36930`
1124/// — walk node 2 to every printed digit, with walk node 3 at `4.47708` past the
1125/// diameter. The walk pushes a node and only then breaks if it failed to
1126/// improve, so an argmin that IS the last pushed node improved: the walk left
1127/// through the ceiling test with the criterion still descending.
1128///
1129/// **What raising the stop was actually worth here, measured after the change,
1130/// because it is less than the shape of the defect suggests.** The walk now
1131/// scores `4.47708`, which does NOT improve — so the criterion has an interior
1132/// optimum on this fixture and the old ceiling happened to cut just past it.
1133/// What the extra node buys is the PARABOLIC REFINEMENT, which cannot fire on
1134/// an argmin that is the last element: with a neighbour on both sides the
1135/// refinement lands at `ℓ = 3.10543` with a better criterion value, and
1136/// held-out marginal RMSE goes `0.04185 → 0.04179`. A rule that cannot be
1137/// stepped past also cannot be refined at, and that is the part of the cost
1138/// that was invisible.
1139///
1140/// The larger held-out number on the same sweep — `0.03788` at `ℓ = 68.5`,
1141/// where the block still carries `edf = 7.47` and is not degenerate — is NOT
1142/// what this change recovers, and attributing it to the ceiling would be wrong:
1143/// the criterion does not want to go there. That gap is a statement about the
1144/// screening CRITERION (a profiled Gaussian REML of the term alone against a
1145/// binary response) disagreeing with held-out truth at long ranges, and it
1146/// belongs to whoever takes that question next.
1147///
1148/// Raising the stop is safe by the walk's own rule, which only continues while
1149/// the criterion improves: on the gam#2750 fixture, where the criterion drops
1150/// from `−256.3` to `−198.5` just past the diameter, the walk stops on the
1151/// first non-improving node exactly as it does today. The ceiling only ever
1152/// binds where the criterion is still descending — which is precisely the case
1153/// where stopping is wrong.
1154#[derive(Clone, Debug)]
1155pub struct MeasureJetRangeBracket {
1156    /// Geometric grid of candidate ranges, ascending. The realized scale band.
1157    pub nodes: Vec<f64>,
1158    /// The band's own log step, so an endpoint walk keeps its resolution.
1159    pub log_step: f64,
1160    /// The node bounding-box diagonal. A geometric fact about the cloud,
1161    /// reported because the band's own ceiling is half of it; NOT a stopping
1162    /// rule (see the type docs).
1163    pub node_diameter: f64,
1164    /// Hard upper end for any walk past the top node: the feasibility wall
1165    /// [`measure_jet_range_feasibility_ceiling`], the same one the outer
1166    /// search's [`measure_jet_ln_range_window`] stops at.
1167    pub feasibility_ceiling: f64,
1168}
1169
1170/// The range at which a node pair separated by `spacing` stops being
1171/// DISTINGUISHABLE from a coincident one in `f64`.
1172///
1173/// `exp(−spacing²/2ℓ²)` has come within `√ε` of 1 at
1174/// `ℓ = spacing/√(2√ε)`; past it `K_cc` is the all-ones matrix to working
1175/// precision and the gauge annihilates exactly that (the affine span, constant
1176/// included), so no distinct model survives. `√ε` is the chart's own bar — the
1177/// same half-mantissa `condition_representer_section` spends, and for the same
1178/// reason: it is the point past which a direction cannot survive being squared
1179/// into a Gram and inverted back out.
1180///
1181/// ONE definition, used by both the outer search's window
1182/// ([`measure_jet_ln_range_window`]) and the response screen's walk stop
1183/// ([`MeasureJetRangeBracket::feasibility_ceiling`]), so the two cannot drift
1184/// into disagreeing about where the model ends (#2761).
1185pub fn measure_jet_range_feasibility_ceiling(spacing: f64) -> f64 {
1186    spacing / (2.0 * f64::EPSILON.sqrt()).sqrt()
1187}
1188
1189/// Realize [`MeasureJetRangeBracket`] for a fresh (unfrozen) measure-jet spec.
1190///
1191/// `data` must already be in the standardized frame the basis is built in, and
1192/// the spec must be the FRESH one (no frozen quadrature): the bracket is a
1193/// seeding device and a frozen term has nothing left to seed.
1194pub fn measure_jet_range_bracket(
1195    data: ArrayView2<'_, f64>,
1196    spec: &MeasureJetBasisSpec,
1197) -> Result<MeasureJetRangeBracket, BasisError> {
1198    if spec.frozen_quadrature.is_some() {
1199        crate::bail_invalid_basis!(
1200            "measure-jet range bracket is a seeding device; a frozen-quadrature spec has no seed left to choose"
1201        );
1202    }
1203    if data.ncols() == 0 {
1204        crate::bail_invalid_basis!("measure-jet range bracket needs at least one feature column");
1205    }
1206    validate_finite_points(data, "data")?;
1207    let seed_centers = select_centers_by_strategy(data, &spec.center_strategy)?;
1208    if seed_centers.nrows() < 3 {
1209        return Err(BasisError::InsufficientColumnsForConstraint {
1210            found: seed_centers.nrows(),
1211        });
1212    }
1213    let (nodes, _masses) = measure_jet_quadrature_nodes(data, seed_centers.view())?;
1214    let band = measure_jet_band(nodes.view(), spec.num_scales)?;
1215    // The band floor IS the median nearest-node spacing, so the feasibility
1216    // wall is read off the bracket's own first node rather than remeasured.
1217    let feasibility_ceiling = measure_jet_range_feasibility_ceiling(band.eps[0]);
1218    Ok(MeasureJetRangeBracket {
1219        nodes: band.eps,
1220        log_step: band.log_step,
1221        node_diameter: bounding_box_diagonal(nodes.view()),
1222        feasibility_ceiling,
1223    })
1224}
1225
1226/// The `ln ℓ` SEARCH WINDOW for the design-moving representer range, in the
1227/// frame `spec.length_scale` is expressed in (gam#2750).
1228///
1229/// ## Why this is not an absolute interval
1230///
1231/// `ℓ` is a LENGTH in the chart the basis is realized in, so a window for it is
1232/// a statement about the node cloud, not about `f64`. Both ends here are the
1233/// same measured length — the median nearest-node spacing `s`, which is also
1234/// [`realized_measure_jet_length_scale`]'s auto value and
1235/// [`MeasureJetBand::eps`]'s floor — read at the two ranges where the kernel
1236/// stops saying anything about the pair it separates:
1237///
1238/// * **floor `ℓ = s`.** Neighbouring representers overlap at exactly
1239///   `exp(−1/2)`. Below it they stop overlapping, the design degenerates from a
1240///   partition of unity into a bump-per-node indicator, and rows between nodes
1241///   fall outside every representer's support.
1242/// * **ceiling `ℓ = s/√(2√ε)`.** The same neighbouring pair's kernel value
1243///   `exp(−s²/2ℓ²)` has come within `√ε` of 1, so the pair is no longer
1244///   DISTINGUISHABLE from a coincident one in the arithmetic the chart is built
1245///   in. Past it `K_cc` is the all-ones matrix to working precision, and the
1246///   gauge annihilates exactly that (the affine span, constant included), so
1247///   there is no distinct model left. `√ε` is the chart's own bar — the same
1248///   half-mantissa `condition_representer_section` spends, and for the same
1249///   reason: it is the point past which a direction cannot survive being
1250///   squared into a Gram and inverted back out.
1251///
1252/// The window is therefore `[ln s, ln s − ½ln(2√ε)]`: it TRANSLATES with the
1253/// chart (both ends are proportional to a measured length, so an isotropic
1254/// rescale by `c` shifts them both by `ln c`) and its WIDTH is `8.664`, a pure
1255/// function of `f64::EPSILON` rather than a number anybody picked.
1256///
1257/// ## What this deliberately is NOT
1258///
1259/// It is **not** [`MeasureJetRangeBracket::node_diameter`], the node bounding-box
1260/// diagonal. That is where the response SCREEN stops walking, which is a
1261/// stopping rule for a search over nodes, not a wall in the model: measured on
1262/// three fixtures (`measure_jet_formula_fit_robustness_sweep` seed 1,
1263/// `measure_jet_web_quality`, and the two probes that score them), the profiled
1264/// criterion genuinely prefers a range AT or ABOVE the node diameter, and a box
1265/// that stopped there railed the outer search and refused the fit. A long range
1266/// is a legitimate model — as `ℓ` grows the gauge-quotiented representer span
1267/// tends to a polynomial one, which is exactly the right basis for a smooth
1268/// target — so the upper end has to be a feasibility statement and nothing
1269/// weaker.
1270pub fn measure_jet_ln_range_window(
1271    data: ArrayView2<'_, f64>,
1272    spec: &MeasureJetBasisSpec,
1273) -> Result<(f64, f64), BasisError> {
1274    let spacing = match (&spec.center_strategy, &spec.frozen_quadrature) {
1275        (CenterStrategy::UserProvided(_), Some(frozen)) if !frozen.eps_band.is_empty() => {
1276            frozen.eps_band[0]
1277        }
1278        _ => measure_jet_range_bracket(data, spec)?.nodes[0],
1279    };
1280    if !(spacing.is_finite() && spacing > 0.0) {
1281        crate::bail_invalid_basis!(
1282            "measure-jet ln-range window is degenerate: the node cloud reports a nearest-node \
1283             spacing of {spacing}, so it has no range scale to search over"
1284        );
1285    }
1286    // The range at which `1 - exp(-s^2/2l^2)` reaches the chart's half-mantissa
1287    // bar, from the single definition the screen's walk stop also reads.
1288    let ceiling = measure_jet_range_feasibility_ceiling(spacing);
1289    Ok((spacing.ln(), ceiling.ln()))
1290}
1291
1292/// Build the realized geometric scale band from the center set: floor at the
1293/// median nearest-center spacing (below it the quadrature resolves nothing),
1294/// ceiling at half the bounding-box diagonal (a deterministic diameter-scale
1295/// cap; local fits remain center-weighted and distinct there).
1296/// `num_scales == 0` requests the auto count `clamp(⌈log2(ε_max/ε_min)⌉ + 1,
1297/// 3, 8)`; a degenerate band (ceiling ≤ floor) collapses to the single floor
1298/// scale with `log_step = ln 2`.
1299pub fn measure_jet_band(
1300    centers: ArrayView2<'_, f64>,
1301    num_scales: usize,
1302) -> Result<MeasureJetBand, BasisError> {
1303    validate_finite_points(centers, "centers")?;
1304    let dist2 = pairwise_sq_dists(centers, centers);
1305    let eps_min = median_nearest_center_spacing(&dist2)?;
1306    // Half the bounding-box diagonal: a cheap, deterministic diameter proxy.
1307    let eps_max = 0.5 * bounding_box_diagonal(centers);
1308    if !(eps_max.is_finite() && eps_max > eps_min) {
1309        return Ok(MeasureJetBand {
1310            eps: vec![eps_min],
1311            log_step: std::f64::consts::LN_2,
1312        });
1313    }
1314    let auto = ((eps_max / eps_min).log2().ceil() as usize + 1)
1315        .clamp(MEASURE_JET_MIN_AUTO_SCALES, MEASURE_JET_MAX_AUTO_SCALES);
1316    let count = if num_scales == 0 { auto } else { num_scales };
1317    if count == 1 {
1318        return Ok(MeasureJetBand {
1319            eps: vec![eps_min],
1320            log_step: std::f64::consts::LN_2,
1321        });
1322    }
1323    let ratio = (eps_max / eps_min).powf(1.0 / (count as f64 - 1.0));
1324    let mut eps = Vec::with_capacity(count);
1325    let mut e = eps_min;
1326    for _ in 0..count {
1327        eps.push(e);
1328        e *= ratio;
1329    }
1330    Ok(MeasureJetBand {
1331        eps,
1332        log_step: ratio.ln(),
1333    })
1334}
1335
1336/// First-moment-exact quadrature of the empirical measure on the cell partition
1337/// induced by the seed centers: nearest-center assignment (deterministic
1338/// tie-break: lowest center index) yields per-cell masses, and each non-empty
1339/// cell's quadrature node is its mass-weighted barycenter. Empty cells keep
1340/// their seed coordinates with zero mass (the assembly skips them; their
1341/// representer columns remain valid).
1342pub fn measure_jet_quadrature_nodes(
1343    data: ArrayView2<'_, f64>,
1344    centers: ArrayView2<'_, f64>,
1345) -> Result<(Array2<f64>, Array1<f64>), BasisError> {
1346    if data.ncols() != centers.ncols() {
1347        crate::bail_dim_basis!(
1348            "measure-jet mass assignment dimension mismatch: data d={} centers d={}",
1349            data.ncols(),
1350            centers.ncols()
1351        );
1352    }
1353    validate_finite_points(data, "data")?;
1354    validate_finite_points(centers, "centers")?;
1355    let n = data.nrows();
1356    let m = centers.nrows();
1357    let d = centers.ncols();
1358    if n == 0 || m == 0 {
1359        crate::bail_invalid_basis!("measure-jet mass assignment needs nonempty data and centers");
1360    }
1361    // Nearest-node assignment in streamed GEMM blocks: argmin_j ‖x−c_j‖² =
1362    // argmin_j (‖c_j‖² − 2·xᵀc_j), so each block is one (rows×d)·(d×m)
1363    // product plus a row-wise argmin — tile-speed FMAs, O(block·m) transient
1364    // memory, deterministic ties to the lowest center index.
1365    let cn: Vec<f64> = centers.outer_iter().map(|r| r.dot(&r)).collect();
1366    let assignments: Vec<usize> = (0..n)
1367        .step_by(MEASURE_JET_ASSIGN_BLOCK_ROWS)
1368        .flat_map(|start| {
1369            let end = (start + MEASURE_JET_ASSIGN_BLOCK_ROWS).min(n);
1370            let g = data.slice(ndarray::s![start..end, ..]).dot(&centers.t());
1371            let block: Vec<usize> = g
1372                .axis_iter(Axis(0))
1373                .into_par_iter()
1374                .map(|row| {
1375                    let mut best_j = 0usize;
1376                    let mut best = f64::INFINITY;
1377                    for (j, &gij) in row.iter().enumerate() {
1378                        let s = cn[j] - 2.0 * gij;
1379                        if s < best {
1380                            best = s;
1381                            best_j = j;
1382                        }
1383                    }
1384                    best_j
1385                })
1386                .collect();
1387            block
1388        })
1389        .collect();
1390    let mut masses = Array1::<f64>::zeros(m);
1391    let mut nodes = centers.to_owned();
1392    let mut sums = Array2::<f64>::zeros((m, d));
1393    let unit = 1.0 / n as f64;
1394    for (i, &j) in assignments.iter().enumerate() {
1395        masses[j] += unit;
1396        for k in 0..d {
1397            sums[(j, k)] += data[(i, k)];
1398        }
1399    }
1400    // Cell barycenters: the first moment of μ on each cell. These are the
1401    // realized nodes for first-moment-exact lumping.
1402    let mut barycenter = sums;
1403    for j in 0..m {
1404        let count = masses[j] * n as f64;
1405        if count > 0.0 {
1406            for k in 0..d {
1407                barycenter[(j, k)] /= count;
1408                nodes[(j, k)] = barycenter[(j, k)];
1409            }
1410        }
1411    }
1412    Ok((nodes, masses))
1413}
1414
1415/// THE single assembly source: walk every (scale, outer-net center) local
1416/// residual block exactly once and scatter it into `n_forms` accumulators
1417/// with caller-chosen scalar weights. The energy, its (s, α) jets, and the
1418/// per-scale spectrum are all this routine with different weight closures,
1419/// so a value/derivative desync is structurally impossible.
1420///
1421/// Per block the closure receives `(scale_idx, eps, q, base)` where `q` is
1422/// the truncated kernel sum used by the local residual and `base`
1423/// is the fully-assembled outer weight
1424/// `log_step · ε^(−η) · net_mass_i · q^(1−2α)`, with
1425/// `η = 2s + d(2−2α)` for the available dimension parameter, and writes, per requested
1426/// form, one weight triple `[w_R, w_2, w_3]`. Only `w_R` is live:
1427/// `R = CᵀWC − B·G⁺·Bᵀ/q`, with `G⁺` the rank-revealing pseudo-inverse.
1428/// The extra slots are retained for the ψ layout and receive zero local
1429/// channels because τ no longer changes the energy.
1430///
1431/// The outer sum over centers is coarsened per scale to a deterministic
1432/// ε/2-net with nearest-member mass aggregation (the outer Riemann sum needs
1433/// resolution ε, not the center-spacing floor), so each scale's cost sits at
1434/// its own level and the band totals ~O(m²·d) instead of O(L·m³). The inner
1435/// (local-fit) quadrature always uses the full center set, so the local
1436/// residual identities (exact constant annihilation, PSD) are untouched.
1437pub(crate) fn assemble_weighted_forms<F>(
1438    centers: ArrayView2<'_, f64>,
1439    masses: ArrayView1<'_, f64>,
1440    band: &MeasureJetBand,
1441    order_s: f64,
1442    alpha: f64,
1443    tau0: f64,
1444    n_forms: usize,
1445    channels: usize,
1446    weights: &F,
1447) -> Result<Vec<Array2<f64>>, BasisError>
1448where
1449    F: Fn(usize, f64, f64, f64, &mut [[f64; 3]]) + Sync,
1450{
1451    let m = centers.nrows();
1452    let d = centers.ncols();
1453    if n_forms == 0 || !(1..=3).contains(&channels) {
1454        crate::bail_invalid_basis!(
1455            "measure-jet assembly needs at least one output form and 1..=3 block channels"
1456        );
1457    }
1458    if masses.len() != m {
1459        crate::bail_dim_basis!(
1460            "measure-jet energy mass/center mismatch: {} masses for {} centers",
1461            masses.len(),
1462            m
1463        );
1464    }
1465    if band.eps.is_empty() || band.eps.iter().any(|e| !(e.is_finite() && *e > 0.0)) {
1466        crate::bail_invalid_basis!("measure-jet energy needs a nonempty positive scale band");
1467    }
1468    if !(order_s.is_finite() && order_s > 0.0 && order_s < 2.0) {
1469        crate::bail_invalid_basis!(
1470            "measure-jet order s must lie in (0, 2) for the affine-jet energy; got {order_s}"
1471        );
1472    }
1473    if !(alpha.is_finite() && tau0.is_finite() && tau0 >= 0.0) {
1474        crate::bail_invalid_basis!(
1475            "measure-jet energy needs finite alpha and finite tau0 >= 0; got alpha={alpha}, tau0={tau0}"
1476        );
1477    }
1478    if masses.iter().any(|v| !(v.is_finite() && *v >= 0.0)) {
1479        crate::bail_invalid_basis!("measure-jet energy needs finite nonnegative center masses");
1480    }
1481    let dist2 = pairwise_sq_dists(centers, centers);
1482
1483    // One block of `n_forms` m×m accumulators per scale. Each scale's center
1484    // loop is sequential and the cross-scale sum below runs in band order,
1485    // so the result is bit-deterministic whether or not the scales
1486    // themselves run in parallel.
1487    let assemble_scale = |scale_idx: usize, eps: f64| -> Result<Vec<Array2<f64>>, BasisError> {
1488        let mut out: Vec<Array2<f64>> =
1489            (0..n_forms).map(|_| Array2::<f64>::zeros((m, m))).collect();
1490        let cutoff2 = (MEASURE_JET_PROFILE_CUTOFF * eps) * (MEASURE_JET_PROFILE_CUTOFF * eps);
1491        let inv_two_eps2 = 1.0 / (2.0 * eps * eps);
1492        let eta = 2.0 * order_s + (d as f64) * (2.0 - 2.0 * alpha);
1493        let scale_weight = band.log_step * eps.powf(-eta);
1494        // Outer-quadrature coarsening: greedy ε/2-net over the centers in
1495        // fixed index order (deterministic), with every center's mass
1496        // aggregated to its nearest net member (lowest-index tie break).
1497        let net_radius2 = 0.25 * eps * eps;
1498        let mut outer: Vec<usize> = Vec::new();
1499        for i in 0..m {
1500            if masses[i] <= 0.0 {
1501                continue;
1502            }
1503            let covered = outer.iter().any(|&o| dist2[(i, o)] <= net_radius2);
1504            if !covered {
1505                outer.push(i);
1506            }
1507        }
1508        let mut net_mass = vec![0.0_f64; m];
1509        for i in 0..m {
1510            if masses[i] <= 0.0 {
1511                continue;
1512            }
1513            let mut best = f64::INFINITY;
1514            let mut best_o = usize::MAX;
1515            for &o in &outer {
1516                if dist2[(i, o)] < best {
1517                    best = dist2[(i, o)];
1518                    best_o = o;
1519                }
1520            }
1521            if best_o != usize::MAX {
1522                net_mass[best_o] += masses[i];
1523            }
1524        }
1525        let mut wbuf = vec![[0.0_f64; 3]; n_forms];
1526        for &i in &outer {
1527            // Local neighbor set (always includes i itself).
1528            let mut idx: Vec<usize> = Vec::new();
1529            for j in 0..m {
1530                if dist2[(i, j)] <= cutoff2 {
1531                    idx.push(j);
1532                }
1533            }
1534            let ml = idx.len();
1535            // Kernel weights and mass.
1536            let mut w = Array1::<f64>::zeros(ml);
1537            let mut q = 0.0_f64;
1538            for (a, &j) in idx.iter().enumerate() {
1539                let wj = masses[j] * (-dist2[(i, j)] * inv_two_eps2).exp();
1540                w[a] = wj;
1541                q += wj;
1542            }
1543            if !(q > 0.0) {
1544                continue;
1545            }
1546            // Scaled local features Φ (ml × d) and weighted column means a.
1547            let mut phi = Array2::<f64>::zeros((ml, d));
1548            for (a, &j) in idx.iter().enumerate() {
1549                for k in 0..d {
1550                    phi[(a, k)] = (centers[(j, k)] - centers[(i, k)]) / eps;
1551                }
1552            }
1553            let a_mean = phi.t().dot(&w) / q;
1554            // B = WΦ − w·aᵀ and G = (ΦᵀWΦ)/q − a·aᵀ.
1555            let mut wphi = phi.clone();
1556            for (a, mut row) in wphi.outer_iter_mut().enumerate() {
1557                row.mapv_inplace(|v| v * w[a]);
1558            }
1559            let mut b = wphi.clone();
1560            for (a, mut row) in b.outer_iter_mut().enumerate() {
1561                for k in 0..d {
1562                    row[k] -= w[a] * a_mean[k];
1563                }
1564            }
1565            let mut g = phi.t().dot(&wphi);
1566            g.mapv_inplace(|v| v / q);
1567            for r in 0..d {
1568                for c in 0..d {
1569                    g[(r, c)] -= a_mean[r] * a_mean[c];
1570                }
1571            }
1572            let g_pinv = symmetric_pseudoinverse(&g, "local affine Gram")?;
1573            let bm = b.dot(&g_pinv);
1574            let base = scale_weight * net_mass[i] * q.powf(1.0 - 2.0 * alpha);
1575            weights(scale_idx, eps, q, base, &mut wbuf);
1576            // Scatter-add Σ_k wbuf[k]·R into each form. The τ channels are
1577            // zero because the exact projection is τ-independent.
1578            for (a, &ja) in idx.iter().enumerate() {
1579                let bma = bm.row(a);
1580                for (c, &jc) in idx.iter().enumerate() {
1581                    let b_c = b.row(c);
1582                    let mut val_r = -w[a] * w[c] / q - bma.dot(&b_c) / q;
1583                    if a == c {
1584                        val_r += w[a];
1585                    }
1586                    for (k, out_k) in out.iter_mut().enumerate() {
1587                        let wk = wbuf[k];
1588                        out_k[(ja, jc)] += wk[0] * val_r;
1589                    }
1590                }
1591            }
1592        }
1593        Ok(out)
1594    };
1595
1596    let n_scales = band.eps.len();
1597    let parallel_ok = m
1598        .saturating_mul(m)
1599        .saturating_mul(n_scales)
1600        .saturating_mul(n_forms)
1601        <= MEASURE_JET_PARALLEL_FORM_BUDGET_DOUBLES;
1602    let per_scale: Vec<Vec<Array2<f64>>> = if parallel_ok {
1603        band.eps
1604            .par_iter()
1605            .enumerate()
1606            .map(|(scale_idx, &eps)| assemble_scale(scale_idx, eps))
1607            .collect::<Result<Vec<_>, BasisError>>()?
1608    } else {
1609        band.eps
1610            .iter()
1611            .enumerate()
1612            .map(|(scale_idx, &eps)| assemble_scale(scale_idx, eps))
1613            .collect::<Result<Vec<_>, BasisError>>()?
1614    };
1615
1616    let mut totals: Vec<Array2<f64>> = (0..n_forms).map(|_| Array2::<f64>::zeros((m, m))).collect();
1617    for scale_forms in per_scale {
1618        for (total, part) in totals.iter_mut().zip(scale_forms) {
1619            *total += &part;
1620        }
1621    }
1622    // Numerical symmetrization (every analytic form here is symmetric).
1623    Ok(totals.into_iter().map(|t| (&t + &t.t()) * 0.5).collect())
1624}
1625
1626/// The multiscale jet-residual energy `Q` (m × m, symmetric PSD) on the
1627/// center set. See the module docs for the formula and contracts; the local
1628/// residual form is assembled through the closed-form identities
1629///
1630/// ```text
1631///   CᵀWC          = W − w·wᵀ/q,
1632///   B = CᵀWΦ̃     = WΦ − w·aᵀ          (a = Φᵀw/q),
1633///   G = Φ̃ᵀWΦ̃/q  = (ΦᵀWΦ)/q − a·aᵀ,
1634///   R_loc         = CᵀWC − B·G⁺·Bᵀ/q,
1635/// ```
1636///
1637/// with `G⁺` realized through the symmetric eigendecomposition and a
1638/// machine-precision rank cutoff. One walk of `assemble_weighted_forms`
1639/// with the unit weight.
1640pub fn measure_jet_energy_form(
1641    centers: ArrayView2<'_, f64>,
1642    masses: ArrayView1<'_, f64>,
1643    band: &MeasureJetBand,
1644    order_s: f64,
1645    alpha: f64,
1646    tau0: f64,
1647) -> Result<Array2<f64>, BasisError> {
1648    let mut forms = assemble_weighted_forms(
1649        centers,
1650        masses,
1651        band,
1652        order_s,
1653        alpha,
1654        tau0,
1655        1,
1656        1,
1657        &|_, _, _, base, out: &mut [[f64; 3]]| out[0] = [base, 0.0, 0.0],
1658    )?;
1659    let q = forms.swap_remove(0);
1660    // The energy `Q = Σ wᵢ Rᵢ` is a nonnegative combination of analytically
1661    // PSD local residual forms, so it is PSD in exact arithmetic. The affine
1662    // span is annihilated to machine zero, where roundoff in the per-block
1663    // pseudo-inverse and the centering cancellation leaves the smallest
1664    // eigenvalue at ±ε_mach·‖Q‖. Project onto the PSD cone (floor negative
1665    // eigenvalues at 0) so `vᵀQv ≥ 0` holds exactly for every `v`, including
1666    // the affine directions the energy must annihilate.
1667    project_symmetric_psd(q, "measure-jet energy form")
1668}
1669
1670/// Project a symmetric matrix onto the PSD cone by flooring its negative
1671/// eigenvalues at 0. Only sub-machine-precision negative eigenvalues are
1672/// expected here (the form is analytically PSD); a meaningfully negative
1673/// eigenvalue would indicate an assembly bug, so it is floored but the
1674/// reconstruction otherwise preserves the spectrum exactly.
1675pub(crate) fn project_symmetric_psd(
1676    a: Array2<f64>,
1677    label: &str,
1678) -> Result<Array2<f64>, BasisError> {
1679    let n = a.nrows();
1680    if n == 0 {
1681        return Ok(a);
1682    }
1683    let (evals, evecs) = a.eigh(Side::Lower).map_err(|e| {
1684        BasisError::InvalidInput(format!(
1685            "measure-jet PSD projection `{label}` eigendecomposition failed: {e}"
1686        ))
1687    })?;
1688    if evals.iter().all(|&lam| lam >= 0.0) {
1689        return Ok(a);
1690    }
1691    let mut scaled = evecs.clone();
1692    for (k, mut col) in scaled.axis_iter_mut(Axis(1)).enumerate() {
1693        let lam = evals[k].max(0.0);
1694        col.mapv_inplace(|v| v * lam);
1695    }
1696    let psd = scaled.dot(&evecs.t());
1697    Ok((&psd + &psd.t()) * 0.5)
1698}
1699
1700/// The per-scale energy forms `Q_ℓ` (each m × m, symmetric PSD), with
1701/// `Σ_ℓ Q_ℓ = Q` to the PSD-projection floor (same blocks, one-hot weights).
1702/// These are the spectral-split carriers: emitted as separate penalty
1703/// candidates they let the multi-penalty REML engine learn per-level amplitudes
1704/// λ_ℓ directly — scale adaptivity at ρ-speed with no rebuild and no new
1705/// optimizer code.
1706///
1707/// Each level is projected onto the PSD cone for the same reason the fused
1708/// [`measure_jet_energy_form`] is, and the projection is load-bearing HERE in a
1709/// way it is not there — see the note at the return.
1710pub fn measure_jet_energy_forms_per_scale(
1711    centers: ArrayView2<'_, f64>,
1712    masses: ArrayView1<'_, f64>,
1713    band: &MeasureJetBand,
1714    order_s: f64,
1715    alpha: f64,
1716    tau0: f64,
1717) -> Result<Vec<Array2<f64>>, BasisError> {
1718    let n_scales = band.eps.len();
1719    let forms = assemble_weighted_forms(
1720        centers,
1721        masses,
1722        band,
1723        order_s,
1724        alpha,
1725        tau0,
1726        n_scales,
1727        1,
1728        &|scale_idx, _, _, base, out: &mut [[f64; 3]]| {
1729            for (k, slot) in out.iter_mut().enumerate() {
1730                *slot = if k == scale_idx {
1731                    [base, 0.0, 0.0]
1732                } else {
1733                    [0.0, 0.0, 0.0]
1734                };
1735            }
1736        },
1737    )?;
1738    // PSD cone projection, per level. Every `Q_ℓ` is a NONNEGATIVE combination
1739    // of the same analytically-PSD local residual blocks the fused energy sums,
1740    // so it is PSD in exact arithmetic and only the per-block pseudo-inverse and
1741    // centering cancellation put a ±ε_mach·‖Q_ℓ‖ negative in the spectrum —
1742    // the identical situation `measure_jet_energy_form` floors on the cone.
1743    //
1744    // Skipping it here was NOT symmetric with the fused path, because the fused
1745    // path normalizes ONE matrix while the builder normalizes EVERY LEVEL BY ITS
1746    // OWN Frobenius scale. A level whose detail energy is numerically dead
1747    // carries only that roundoff, and dividing roundoff by its own tiny norm
1748    // rescales it to unit norm: a ±ε_mach relative negative becomes an O(1)
1749    // absolute one. `ConstructiveQuadratic::try_from_dense_psd` then rejects the
1750    // candidate and the whole multiscale BUILD fails — measured as
1751    // `IndefinitePenalty { context: "measure-jet scale penalty",
1752    // min_eigenvalue: -0.3039, tolerance: 1.486e-8 }`, where the tolerance is
1753    // √ε_mach against a max |λ| of ~1, i.e. the certified matrix is already
1754    // unit-normalized and the negative is 2e7× tolerance. No real detail
1755    // spectrum is 30% negative; that is normalized roundoff.
1756    //
1757    // Flooring restores the invariant the signature documents, leaves a dead
1758    // level as an exact-zero candidate for `filter_penalty_candidates`/REML to
1759    // deselect rather than a fatal build error, and preserves `Σ_ℓ Q_ℓ = Q` to
1760    // the same machine-precision floor the fused projection already accepts. It
1761    // also stops `measure_jet_scale_spectrum` from being able to report a
1762    // NEGATIVE detail energy `vᵀQ_ℓv`.
1763    forms
1764        .into_iter()
1765        .enumerate()
1766        .map(|(level, q_l)| {
1767            project_symmetric_psd(q_l, &format!("measure-jet per-scale energy form {level}"))
1768        })
1769        .collect()
1770}
1771
1772/// The support diagnostic `ε ↦ q_ε(x★)`: kernel mass of the (frozen) center
1773/// quadrature seen from each query point at every band scale (n_query × L).
1774/// A query ON the web sees its strand's mass already at fine scales; a query
1775/// OFF the web accumulates mass only once ε reaches its distance to the
1776/// support. This is the on-web-ness statistic shipped alongside predictions
1777/// — smooth, multiresolution, derived from the measure with no neighbor
1778/// sets.
1779pub fn measure_jet_support_curve(
1780    queries: ArrayView2<'_, f64>,
1781    centers: ArrayView2<'_, f64>,
1782    masses: ArrayView1<'_, f64>,
1783    eps_band: &[f64],
1784) -> Result<Array2<f64>, BasisError> {
1785    if queries.ncols() != centers.ncols() {
1786        crate::bail_dim_basis!(
1787            "measure-jet support curve dimension mismatch: queries d={} centers d={}",
1788            queries.ncols(),
1789            centers.ncols()
1790        );
1791    }
1792    if masses.len() != centers.nrows() {
1793        crate::bail_dim_basis!(
1794            "measure-jet support curve mass/center mismatch: {} masses for {} centers",
1795            masses.len(),
1796            centers.nrows()
1797        );
1798    }
1799    if eps_band.is_empty() || eps_band.iter().any(|e| !(e.is_finite() && *e > 0.0)) {
1800        crate::bail_invalid_basis!("measure-jet support curve needs a nonempty positive band");
1801    }
1802    validate_finite_points(queries, "queries")?;
1803    validate_finite_points(centers, "centers")?;
1804    let nq = queries.nrows();
1805    let nl = eps_band.len();
1806    // Distances once (GEMM), then every band scale reads the same d² row —
1807    // an L-fold saving over per-scale distance recomputation.
1808    let d2 = pairwise_sq_dists(queries, centers);
1809    let mut out = Array2::<f64>::zeros((nq, nl));
1810    out.axis_iter_mut(Axis(0))
1811        .into_par_iter()
1812        .enumerate()
1813        .for_each(|(qi, mut row)| {
1814            let d2_row = d2.row(qi);
1815            for (li, &eps) in eps_band.iter().enumerate() {
1816                let inv_two_eps2 = 1.0 / (2.0 * eps * eps);
1817                let mut acc = 0.0_f64;
1818                for (j, &dd) in d2_row.iter().enumerate() {
1819                    acc += masses[j] * (-dd * inv_two_eps2).exp();
1820                }
1821                row[li] = acc;
1822            }
1823        });
1824    Ok(out)
1825}
1826
1827pub(crate) fn measure_jet_support_means(
1828    centers: ArrayView2<'_, f64>,
1829    masses: ArrayView1<'_, f64>,
1830    eps_band: &[f64],
1831) -> Result<Vec<f64>, BasisError> {
1832    let total_mass = masses.sum();
1833    if !(total_mass.is_finite() && total_mass > 0.0) {
1834        crate::bail_invalid_basis!(
1835            "measure-jet support means need positive finite total mass; got {total_mass}"
1836        );
1837    }
1838    let support = measure_jet_support_curve(centers, centers, masses, eps_band)?;
1839    let mut means = vec![0.0_f64; eps_band.len()];
1840    for (i, row) in support.rows().into_iter().enumerate() {
1841        let mass = masses[i];
1842        for (mean, &q) in means.iter_mut().zip(row.iter()) {
1843            *mean += mass * q;
1844        }
1845    }
1846    for mean in &mut means {
1847        *mean /= total_mass;
1848        if !(*mean).is_finite() || *mean <= 0.0 {
1849            crate::bail_invalid_basis!(
1850                "measure-jet support mean must be positive and finite; got {mean}"
1851            );
1852        }
1853    }
1854    Ok(means)
1855}
1856
1857/// Gaussian representer features `exp(−‖x − c‖²/(2ℓ²))` (n × m).
1858pub fn measure_jet_design_matrix(
1859    data: ArrayView2<'_, f64>,
1860    centers: ArrayView2<'_, f64>,
1861    length_scale: f64,
1862) -> Result<Array2<f64>, BasisError> {
1863    if data.ncols() != centers.ncols() {
1864        crate::bail_dim_basis!(
1865            "measure-jet design dimension mismatch: data d={} centers d={}",
1866            data.ncols(),
1867            centers.ncols()
1868        );
1869    }
1870    if !(length_scale.is_finite() && length_scale > 0.0) {
1871        crate::bail_invalid_basis!(
1872            "measure-jet design needs a positive finite length_scale; got {length_scale}"
1873        );
1874    }
1875    validate_finite_points(data, "data")?;
1876    validate_finite_points(centers, "centers")?;
1877    let inv_two_l2 = 1.0 / (2.0 * length_scale * length_scale);
1878    // One GEMM for every distance, then the Gaussian applied in place — the
1879    // n×m allocation IS the output, no transient copy.
1880    let mut out = pairwise_sq_dists(data, centers);
1881    out.axis_iter_mut(Axis(0))
1882        .into_par_iter()
1883        .for_each(|mut row| {
1884            row.mapv_inplace(|d2| (-d2 * inv_two_l2).exp());
1885        });
1886    Ok(out)
1887}
1888
1889/// Exact first and diagonal-second derivatives of the Gaussian representer
1890/// design with respect to `u = ln ℓ`.
1891fn measure_jet_design_log_length_jets(
1892    data: ArrayView2<'_, f64>,
1893    centers: ArrayView2<'_, f64>,
1894    length_scale: f64,
1895) -> Result<(Array2<f64>, Array2<f64>), BasisError> {
1896    let kernel = measure_jet_design_matrix(data, centers, length_scale)?;
1897    let squared_distances = pairwise_sq_dists(data, centers);
1898    let inv_l2 = 1.0 / (length_scale * length_scale);
1899    let mut first = kernel.clone();
1900    let mut second = kernel;
1901    for ((first_value, second_value), &distance_squared) in first
1902        .iter_mut()
1903        .zip(second.iter_mut())
1904        .zip(squared_distances.iter())
1905    {
1906        let a = distance_squared * inv_l2;
1907        let kernel_value = *first_value;
1908        *first_value = kernel_value * a;
1909        *second_value = kernel_value * (a * a - 2.0 * a);
1910    }
1911    Ok((first, second))
1912}
1913
1914/// Rank-revealing ambient-linear head lift `T` (d × head_rank) for the
1915/// extrapolation null space (#1845).
1916///
1917/// The measure-jet energy annihilates ambient-affine functions EXACTLY (the
1918/// no-mass contract), so the affine functions are the penalty's null space —
1919/// the directions the fit is free to extend across a training gap. But the
1920/// Gaussian representer design cannot REPRESENT a global affine function off
1921/// its support: a finite sum of decaying bumps reverts to the parametric
1922/// backbone away from the centers, so in a gap the fit collapses toward the
1923/// training mean instead of carrying the flank-attested trend. Completing the
1924/// smoothing-spline structure, the builder appends this ambient-linear null
1925/// space to the design as an UNPENALIZED head (the `{x_1..x_d}` head the frame
1926/// notes §1 pin as the property the representer basis lacked).
1927///
1928/// The head is data-derived and magic-free. Ambient coordinates of data on a
1929/// low intrinsic-dimension stratum are rank-deficient as linear trends, so the
1930/// coordinate columns are orthonormalized on the centers and the
1931/// numerically-degenerate directions dropped. Working in the mean-CENTERED
1932/// coordinate columns (the mass-weighted mean is the intercept's, not the
1933/// head's) makes the rank test measure the genuine spread of the centers along
1934/// each direction rather than its offset; the relative floor
1935/// `MEASURE_JET_PSEUDOINVERSE_RTOL` is the module's own numerical rank
1936/// tolerance (the same one the local Gram pseudo-inverses use). The returned
1937/// `T` satisfies `linear_head(points) = points · T` (the mean-centering only
1938/// informs the keep/drop decision). `T` is a deterministic function of the
1939/// frozen centers + masses, so the frozen replay path reconstructs the
1940/// identical head with no persisted state.
1941///
1942/// This is the LINEAR half of the null space. The realized head block is the
1943/// whole affine null space `[1 | points·T]`; build it through
1944/// [`measure_jet_affine_head_lift`] + [`measure_jet_affine_head_block`], which
1945/// is what the design, the gauge and the null-component penalty all use. A
1946/// linear-only head is a defect, not an economy: the global parametric
1947/// orthogonalization removes ONE design direction, and if the term's null space
1948/// has no constant to give up, the direction it takes comes out of the null
1949/// space itself, leaving `d − 1` free linear directions instead of `d` (#2751).
1950pub fn measure_jet_affine_head_transform(
1951    centers: ArrayView2<'_, f64>,
1952    masses: ArrayView1<'_, f64>,
1953) -> Array2<f64> {
1954    let m = centers.nrows();
1955    let d = centers.ncols();
1956    let total_mass = masses.sum();
1957    // Mass inner product on center values.
1958    let mdot = |u: &Array1<f64>, v: &Array1<f64>| -> f64 {
1959        let mut acc = 0.0;
1960        for i in 0..m {
1961            acc += masses[i] * u[i] * v[i];
1962        }
1963        acc
1964    };
1965    // Mean-centered coordinate columns: the mass-weighted mean is removed so the
1966    // residual mass-norm is the genuine spread of the centers along a direction,
1967    // not dominated by the coordinate's offset (which the intercept owns).
1968    let cols: Vec<Array1<f64>> = (0..d)
1969        .map(|k| {
1970            let col = centers.column(k).to_owned();
1971            let mean = if total_mass > 0.0 {
1972                mdot(&col, &Array1::ones(m)) / total_mass
1973            } else {
1974                0.0
1975            };
1976            col.mapv(|x| x - mean)
1977        })
1978        .collect();
1979    // Relative numerical rank floor from the centered coordinate-column scale.
1980    let max_norm = cols
1981        .iter()
1982        .fold(0.0_f64, |acc, c| acc.max(mdot(c, c).sqrt()));
1983    let drop_below =
1984        (MEASURE_JET_PSEUDOINVERSE_RTOL * (d.max(1) as f64) * max_norm).max(f64::MIN_POSITIVE);
1985    // Mass-weighted modified Gram–Schmidt on the centered columns; `t`
1986    // accumulates the lift in the ORIGINAL coordinate basis, so every kept head
1987    // column is `points · t_r` (up to the intercept-owned constant).
1988    let mut q_cols: Vec<Array1<f64>> = Vec::new();
1989    let mut t_cols: Vec<Array1<f64>> = Vec::new();
1990    for k in 0..d {
1991        let mut v = cols[k].clone();
1992        let mut t = Array1::<f64>::zeros(d);
1993        t[k] = 1.0;
1994        for (q, tq) in q_cols.iter().zip(t_cols.iter()) {
1995            let proj = mdot(q, &v);
1996            v.scaled_add(-proj, q);
1997            t.scaled_add(-proj, tq);
1998        }
1999        let norm = mdot(&v, &v).sqrt();
2000        if norm > drop_below {
2001            v.mapv_inplace(|x| x / norm);
2002            t.mapv_inplace(|x| x / norm);
2003            q_cols.push(v);
2004            t_cols.push(t);
2005        }
2006    }
2007    let head_rank = t_cols.len();
2008    let mut t_mat = Array2::<f64>::zeros((d, head_rank));
2009    for (r, t) in t_cols.into_iter().enumerate() {
2010        t_mat.column_mut(r).assign(&t);
2011    }
2012    t_mat
2013}
2014
2015/// Affine head lift `T_aff` (`(d+1) × (1 + head_rank)`) acting on the augmented
2016/// point rows `[1 | x]`: column 0 is the constant, the rest are the supported
2017/// ambient-linear directions of [`measure_jet_affine_head_transform`].
2018///
2019/// This — not the linear lift alone — is the energy's null space. The energy
2020/// annihilates every AFFINE function of the centers exactly, constant included
2021/// (`affine_function_nullspace_form` projects onto exactly this span), so the
2022/// design block that carries the null space has to span the same thing.
2023///
2024/// The constant column looks redundant against the model intercept and is not.
2025/// The term-collection chokepoint residualizes every measure-jet design against
2026/// the parametric block and reparameterizes to `Z = null(1ᵀX)`, which removes
2027/// exactly one coefficient direction. The null space of the constrained penalty
2028/// is `{γ : Zγ ∈ null(S)}`, so that removal is charged to the null space unless
2029/// the null space contains the constraint's own direction. With a linear-only
2030/// head the term's null space is `span{x·T}`, the constant is nowhere in it,
2031/// and the centering deletes a LINEAR direction: on a 2-D fixture the surviving
2032/// direction is the accidental one with zero data-mean, and every REML fit that
2033/// selects a large energy λ collapses onto it (#2751, measured at Pearson
2034/// 0.705 = |cos 45°| against a planted `x1` plane). With the constant present
2035/// the centering consumes the constant — which the intercept re-supplies —
2036/// and all `head_rank` linear directions stay free. That is exactly how the
2037/// thin-plate/Duchon null space `{1, x_1..x_d}` behaves at the same chokepoint.
2038pub fn measure_jet_affine_head_lift(
2039    centers: ArrayView2<'_, f64>,
2040    masses: ArrayView1<'_, f64>,
2041) -> Array2<f64> {
2042    let linear = measure_jet_affine_head_transform(centers, masses);
2043    let d = centers.ncols();
2044    let mut lift = Array2::<f64>::zeros((d + 1, linear.ncols() + 1));
2045    lift[(0, 0)] = 1.0;
2046    lift.slice_mut(ndarray::s![1.., 1..]).assign(&linear);
2047    lift
2048}
2049
2050/// Realize the affine head block `[1 | points] · T_aff` for the lift returned
2051/// by [`measure_jet_affine_head_lift`]. A zero-column lift (multiscale mode,
2052/// which carries no head) yields a zero-column block.
2053pub fn measure_jet_affine_head_block(
2054    points: ArrayView2<'_, f64>,
2055    lift: ArrayView2<'_, f64>,
2056) -> Array2<f64> {
2057    let n = points.nrows();
2058    let width = lift.ncols();
2059    if width == 0 {
2060        return Array2::<f64>::zeros((n, 0));
2061    }
2062    let d = points.ncols();
2063    assert_eq!(
2064        lift.nrows(),
2065        d + 1,
2066        "affine head lift must have d+1 rows for d ambient coordinates"
2067    );
2068    let mut augmented = Array2::<f64>::ones((n, d + 1));
2069    augmented.slice_mut(ndarray::s![.., 1..]).assign(&points);
2070    augmented.dot(&lift)
2071}
2072
2073/// Resolve the realized representer range ℓ. An explicit positive
2074/// `spec_length_scale` is used verbatim; the `0.0` sentinel auto-initializes
2075/// from the median nearest-center spacing (one spacing width: neighbors
2076/// overlap at exp(−1/2) ≈ 0.61, smooth blend without collinearity).
2077pub fn realized_measure_jet_length_scale(
2078    centers: ArrayView2<'_, f64>,
2079    spec_length_scale: f64,
2080) -> Result<f64, BasisError> {
2081    if spec_length_scale.is_finite() && spec_length_scale > 0.0 {
2082        return Ok(spec_length_scale);
2083    }
2084    if spec_length_scale != 0.0 {
2085        crate::bail_invalid_basis!(
2086            "measure-jet length_scale must be positive (or 0.0 for auto); got {spec_length_scale}"
2087        );
2088    }
2089    let dist2 = pairwise_sq_dists(centers, centers);
2090    let spacing = median_nearest_center_spacing(&dist2)?;
2091    Ok(MEASURE_JET_AUTO_LENGTH_SCALE_FACTOR * spacing)
2092}
2093
2094/// The realized, ψ-FIXED geometry shared by the basis builder and the
2095/// ψ-derivative producer — ONE realization source, so the penalty the fit
2096/// uses and the penalty the ψ-channel differentiates can never drift apart
2097/// (the #901 desync class, excluded structurally).
2098pub(crate) struct RealizedMeasureJetGeometry {
2099    pub(crate) centers: Array2<f64>,
2100    pub(crate) masses: Array1<f64>,
2101    pub(crate) eps_band: Vec<f64>,
2102    pub(crate) log_step: f64,
2103    pub(crate) length_scale: f64,
2104    /// Assembly order for the energy weights: the realized default in
2105    /// per-level mode (absorbed per candidate by normalization), the
2106    /// explicit value in fused mode.
2107    pub(crate) order_s_eval: f64,
2108    /// Spectral-split mode marker (`order_s == 0.0` sentinel).
2109    pub(crate) per_level: bool,
2110    pub(crate) z: Array2<f64>,
2111    pub(crate) coefficient_gauge: gam_problem::Gauge,
2112    pub(crate) kz: Array2<f64>,
2113    /// Affine head lift `T_aff` ((d+1) × head_width): the energy's null space
2114    /// appended to the representer design (#1845), constant included (#2751).
2115    /// The head columns evaluate as `[1 | points] · T_aff`; empty
2116    /// (`(d+1) × 0`) in multiscale mode, which carries no head. Deterministic
2117    /// in the frozen centers + masses, so predict-time replay rebuilds it
2118    /// verbatim.
2119    pub(crate) head_lift: Array2<f64>,
2120}
2121
2122pub(crate) fn realize_measure_jet_geometry(
2123    data: ArrayView2<'_, f64>,
2124    spec: &MeasureJetBasisSpec,
2125) -> Result<RealizedMeasureJetGeometry, BasisError> {
2126    if data.ncols() == 0 {
2127        crate::bail_invalid_basis!("measure-jet smooth needs at least one feature column");
2128    }
2129    validate_finite_points(data, "data")?;
2130    let seed_centers = select_centers_by_strategy(data, &spec.center_strategy)?;
2131    let m = seed_centers.nrows();
2132    if m < 3 {
2133        return Err(BasisError::InsufficientColumnsForConstraint { found: m });
2134    }
2135    let order_s = if spec.order_s == 0.0 {
2136        MEASURE_JET_DEFAULT_ORDER_S
2137    } else {
2138        spec.order_s
2139    };
2140    // Quadrature realization. Fit path: the realized nodes are the cell
2141    // BARYCENTERS of the seed partition (first-moment-exact lumping of μ —
2142    // see `measure_jet_quadrature_nodes`), so the metadata's `centers` are
2143    // already the realized nodes and the frozen path (predict / ψ-trial,
2144    // `CenterStrategy::UserProvided`) replays them verbatim with the frozen
2145    // masses, band, support anchors, and normalization scales.
2146    let (centers, masses, eps_band, log_step) = match &spec.frozen_quadrature {
2147        Some(frozen) => {
2148            if frozen.masses.len() != m {
2149                crate::bail_dim_basis!(
2150                    "frozen measure-jet quadrature mismatch: {} masses for {} centers",
2151                    frozen.masses.len(),
2152                    m
2153                );
2154            }
2155            if frozen.eps_band.is_empty() {
2156                crate::bail_invalid_basis!("frozen measure-jet quadrature has an empty band");
2157            }
2158            let log_step = if frozen.eps_band.len() >= 2 {
2159                (frozen.eps_band[1] / frozen.eps_band[0]).ln()
2160            } else {
2161                std::f64::consts::LN_2
2162            };
2163            (
2164                seed_centers,
2165                frozen.masses.clone(),
2166                frozen.eps_band.clone(),
2167                log_step,
2168            )
2169        }
2170        None => {
2171            let (nodes, masses) = measure_jet_quadrature_nodes(data, seed_centers.view())?;
2172            let band = measure_jet_band(nodes.view(), spec.num_scales)?;
2173            (nodes, masses, band.eps, band.log_step)
2174        }
2175    };
2176    let length_scale = realized_measure_jet_length_scale(centers.view(), spec.length_scale)?;
2177    // Affine extrapolation head (#1845): the raw center space becomes
2178    // `[ m Gaussian representers | head_width affine columns ]`. The head
2179    // carries the penalty's affine null space explicitly — constant included
2180    // (#2751) — so the fit no longer reverts to the parametric backbone (the
2181    // training mean) across an unsupported gap, and so the collection's
2182    // parametric orthogonalization has the constant to consume instead of a
2183    // linear direction.
2184    // The extrapolation head is the single-scale (fused) gap-bridge path. In
2185    // multiscale mode the per-scale spectral penalties carry their own
2186    // structure and the design stays the pure representer basis (the per-level
2187    // replay + width contracts pin `m − 1` columns), so the head is added only
2188    // when the term is single-scale.
2189    let head_lift = if spec.multiscale {
2190        Array2::<f64>::zeros((centers.ncols() + 1, 0))
2191    } else {
2192        measure_jet_affine_head_lift(centers.view(), masses.view())
2193    };
2194    let head_width = head_lift.ncols();
2195    let m_aug = m + head_width;
2196    let k_cc = measure_jet_design_matrix(centers.view(), centers.view(), length_scale)?;
2197    let head_cc = measure_jet_affine_head_block(centers.view(), head_lift.view());
2198    // Realized-design constraint transform. In single-scale mode the explicit
2199    // affine head and Gaussian representers can otherwise carry the same affine
2200    // CENTER values in two different ways. That is a genuine gauge redundancy,
2201    // not a reason to ridge either coefficient block. At fit time remove it
2202    // exactly by restricting the RBF center values to the mass-orthogonal
2203    // complement of the supported affine space:
2204    //
2205    //   C = A^T W K_cc,       Z_rbf = null(C).
2206    //
2207    // The head then passes through as an identity block. The frozen composed
2208    // `z · z_parametric` is replayed verbatim at prediction/ψ trials (#532), so
2209    // the rank-revealed section never changes after fit-time realization. In
2210    // multiscale mode there is no explicit head, hence no affine duplication;
2211    // retain the existing representer sum-to-zero section there.
2212    let (z, coefficient_gauge) = match &spec.identifiability {
2213        MeasureJetIdentifiability::FrozenTransform { transform } => {
2214            if transform.nrows() != m_aug {
2215                crate::bail_dim_basis!(
2216                    "frozen measure-jet identifiability transform mismatch: {} representers + {} head columns but transform has {} rows",
2217                    m,
2218                    head_width,
2219                    transform.nrows()
2220                );
2221            }
2222            (
2223                transform.clone(),
2224                gam_problem::Gauge::from_block_transforms(&[transform.clone()]),
2225            )
2226        }
2227        MeasureJetIdentifiability::CenterSumToZero => {
2228            let z_rbf = if head_width > 0 {
2229                // `head_cc` IS the affine value basis A at the centers, by
2230                // construction (both come from `measure_jet_affine_head_lift`),
2231                // so the gauge constrains the representers against exactly the
2232                // span the head carries.
2233                let mut weighted_affine = head_cc.clone();
2234                for (i, mut row) in weighted_affine.outer_iter_mut().enumerate() {
2235                    row.mapv_inplace(|v| v * masses[i]);
2236                }
2237                // `rrqr_nullspace_basis(B)` returns null(B^T). Here
2238                // `B = K_cc^T W A = C^T`, hence the returned columns span
2239                // null(C), exactly the required RBF coefficient section.
2240                let constraint_cross = k_cc.t().dot(&weighted_affine);
2241                rrqr_nullspace_basis(&constraint_cross, default_rrqr_rank_alpha())
2242                    .map_err(BasisError::LinalgError)?
2243                    .0
2244            } else {
2245                let u = householder_sum_to_zero_u(m);
2246                householder_sum_to_zero_z(&u)
2247            };
2248            let z_rbf = condition_representer_section(&k_cc, &z_rbf)?;
2249            let rbf_rank = z_rbf.ncols();
2250            let mut z_block = Array2::<f64>::zeros((m_aug, rbf_rank + head_width));
2251            z_block
2252                .slice_mut(ndarray::s![..m, ..rbf_rank])
2253                .assign(&z_rbf);
2254            for r in 0..head_width {
2255                z_block[(m + r, rbf_rank + r)] = 1.0;
2256            }
2257            (
2258                z_block.clone(),
2259                gam_problem::Gauge::from_block_transforms(&[z_block]),
2260            )
2261        }
2262    };
2263    // Augmented raw center matrix `[K(centers, centers) | A]`, so the
2264    // restricted `kz` maps constrained coefficients to center nodal values for
2265    // BOTH the representers and the head; the energy annihilates the head block
2266    // (affine) to machine precision, so it stays the unpenalized null space.
2267    let mut k_aug = Array2::<f64>::zeros((m, m_aug));
2268    k_aug.slice_mut(ndarray::s![.., ..m]).assign(&k_cc);
2269    if head_width > 0 {
2270        k_aug.slice_mut(ndarray::s![.., m..]).assign(&head_cc);
2271    }
2272    let kz = coefficient_gauge.restrict_design(&k_aug);
2273    Ok(RealizedMeasureJetGeometry {
2274        centers,
2275        masses,
2276        eps_band,
2277        log_step,
2278        length_scale,
2279        order_s_eval: order_s,
2280        // Multiscale (per-scale spectral) energy is an EXPLICIT opt-in (#1116):
2281        // one Primary energy at any center count unless the spec asks for the
2282        // scale split. The independent null-component candidate is orthogonal
2283        // to this mode decision. No center-count auto-gate.
2284        per_level: spec.multiscale,
2285        z,
2286        coefficient_gauge,
2287        kz,
2288        head_lift,
2289    })
2290}
2291
2292/// Estimate the ambient input-measurement-error scale `σ_coord` — the
2293/// perpendicular off-manifold residual spread of the empirical measure — for
2294/// the errors-in-variables predictive-variance term `Var_input = ∇f̂ᵀΣ_x∇f̂`,
2295/// `Σ_x = σ_coord²·I` (issue #2225).
2296///
2297/// The measure-jet models data concentrated near an unknown low-intrinsic-
2298/// dimension set sampled with isotropic ambient coordinate noise. In a
2299/// neighborhood the set is locally affine, so the noise lives in the ambient
2300/// directions ORTHOGONAL to the local tangent — exactly the smallest principal
2301/// directions of the local data covariance. This is the standard local-PCA
2302/// noise floor: for each center's nearest-assignment cell with enough points to
2303/// span a tangent (`≥ d + 1`, the linear-algebra rank requirement — not a tuned
2304/// knob), the smallest eigenvalue of the cell-local covariance estimates the
2305/// perpendicular variance `σ_coord²`; averaging over cells (weighted by the
2306/// cell count) pools the estimate. No response values, no smoothing dial, and
2307/// no magic constant enter — it is a pure function of the ambient point cloud
2308/// and the frozen centers, in the centers' (standardized) coordinate frame.
2309///
2310/// Returns `None` when no cell can span a tangent (e.g. `d`-dimensional data
2311/// with fewer than `d + 1` points per cell, or a full-dimensional stratum with
2312/// no separable perpendicular direction) — the caller then leaves `Var_input`
2313/// disabled rather than invent a scale.
2314pub fn measure_jet_input_noise_scale(
2315    data: ArrayView2<'_, f64>,
2316    centers: ArrayView2<'_, f64>,
2317) -> Result<Option<f64>, BasisError> {
2318    let d = data.ncols();
2319    let m = centers.nrows();
2320    if d == 0 || m == 0 || data.nrows() == 0 {
2321        return Ok(None);
2322    }
2323    if centers.ncols() != d {
2324        crate::bail_dim_basis!(
2325            "measure-jet input-noise estimate: data d={d} disagrees with centers d={}",
2326            centers.ncols()
2327        );
2328    }
2329    validate_finite_points(data, "data")?;
2330    validate_finite_points(centers, "centers")?;
2331    // Nearest-center assignment (the same rule that lumps the quadrature
2332    // masses): the squared-distance Gram, argmin per row.
2333    let sq = pairwise_sq_dists(data, centers);
2334    let mut members: Vec<Vec<usize>> = vec![Vec::new(); m];
2335    for (j, row) in sq.axis_iter(Axis(0)).enumerate() {
2336        let mut best = 0usize;
2337        let mut best_d = f64::INFINITY;
2338        for (i, &dij) in row.iter().enumerate() {
2339            if dij < best_d {
2340                best_d = dij;
2341                best = i;
2342            }
2343        }
2344        members[best].push(j);
2345    }
2346    let mut weighted_sum = 0.0_f64;
2347    let mut weight = 0.0_f64;
2348    for cell in &members {
2349        let n_i = cell.len();
2350        // A cell needs at least d + 1 points to define a full-rank local
2351        // covariance; otherwise its smallest eigenvalue is a spurious zero.
2352        if n_i < d + 1 {
2353            continue;
2354        }
2355        // Cell-local mean and covariance in ambient coordinates.
2356        let mut mean = Array1::<f64>::zeros(d);
2357        for &j in cell {
2358            mean += &data.row(j);
2359        }
2360        mean /= n_i as f64;
2361        let mut cov = Array2::<f64>::zeros((d, d));
2362        for &j in cell {
2363            let mut centered = data.row(j).to_owned();
2364            centered -= &mean;
2365            for a in 0..d {
2366                for b in 0..d {
2367                    cov[(a, b)] += centered[a] * centered[b];
2368                }
2369            }
2370        }
2371        cov /= n_i as f64;
2372        // Symmetrize against accumulation asymmetry, then read the smallest
2373        // eigenvalue = the perpendicular (noise) principal variance.
2374        let cov_sym = (&cov + &cov.t()) * 0.5;
2375        let (evals, _) = cov_sym.eigh(Side::Lower).map_err(|e| {
2376            BasisError::InvalidInput(format!(
2377                "measure-jet input-noise estimate: local covariance eigendecomposition failed: {e}"
2378            ))
2379        })?;
2380        let smallest = evals
2381            .iter()
2382            .copied()
2383            .fold(f64::INFINITY, |acc, v| acc.min(v))
2384            .max(0.0);
2385        if smallest.is_finite() {
2386            weighted_sum += n_i as f64 * smallest;
2387            weight += n_i as f64;
2388        }
2389    }
2390    if weight <= 0.0 {
2391        return Ok(None);
2392    }
2393    let sigma2 = weighted_sum / weight;
2394    if !(sigma2.is_finite() && sigma2 > 0.0) {
2395        return Ok(None);
2396    }
2397    Ok(Some(sigma2.sqrt()))
2398}
2399
2400/// Whether a measure-jet spec runs in multiscale mode (per-scale spectral
2401/// energies + `(α, ln τ)` ψ dials). The separate `double_penalty`
2402/// affine/null-component candidate is available in both modes. This is the
2403/// single source of truth shared by the builder and outer enrollment predicates,
2404/// so the energy layout and ψ dimension cannot disagree. Multiscale is an
2405/// explicit opt-in (`spec.multiscale`); there is no center-count auto-gate
2406/// (#1116).
2407pub fn measure_jet_multiscale_mode(spec: &MeasureJetBasisSpec) -> bool {
2408    spec.multiscale
2409}
2410
2411/// Build the measure-jet smooth: Gaussian representer design `K(data,
2412/// centers)·z`, multiscale jet-residual penalty (one candidate per scale in
2413/// spectral mode, one Primary in pinned-order mode), an optional separate
2414/// function-space null-component candidate, and the replayable
2415/// [`BasisMetadata::MeasureJet`]. The geometry comes from the
2416/// empirical measure (centers + masses + band) through the shared
2417/// realization helper — the same source the ψ-derivative producer uses.
2418pub fn build_measure_jet_basis(
2419    data: ArrayView2<'_, f64>,
2420    spec: &MeasureJetBasisSpec,
2421) -> Result<BasisBuildResult, BasisError> {
2422    let RealizedMeasureJetGeometry {
2423        centers,
2424        masses,
2425        eps_band,
2426        log_step,
2427        length_scale,
2428        order_s_eval: order_s,
2429        per_level,
2430        z,
2431        coefficient_gauge,
2432        kz,
2433        head_lift,
2434    } = realize_measure_jet_geometry(data, spec)?;
2435    let band = MeasureJetBand {
2436        eps: eps_band.clone(),
2437        log_step,
2438    };
2439    let m = centers.nrows();
2440    let head_width = head_lift.ncols();
2441    let m_aug = m + head_width;
2442    // Augmented raw design `[K(data, centers) | [1 | data]·T_aff]` (#1845): the
2443    // head columns are the AFFINE extrapolation basis, which is the energy's
2444    // whole null space (#2751). The gauge restricts BOTH blocks together, so
2445    // the frozen composed transform replays the head verbatim at predict time.
2446    let kernel_design = measure_jet_design_matrix(data, centers.view(), length_scale)?;
2447    let mut raw_design = Array2::<f64>::zeros((data.nrows(), m_aug));
2448    raw_design
2449        .slice_mut(ndarray::s![.., ..m])
2450        .assign(&kernel_design);
2451    if head_width > 0 {
2452        let head_design = measure_jet_affine_head_block(data, head_lift.view());
2453        raw_design
2454            .slice_mut(ndarray::s![.., m..])
2455            .assign(&head_design);
2456    }
2457    let constrained_design = coefficient_gauge.restrict_design(&raw_design);
2458    let design = gam_linalg::matrix::DesignMatrix::Dense(
2459        gam_linalg::matrix::DenseDesignMatrix::from(constrained_design),
2460    );
2461    let support_means = measure_jet_support_means(centers.view(), masses.view(), &eps_band)?;
2462    // Spectral/geometric split. With the auto order sentinel (order_s == 0.0)
2463    // the term emits one candidate PER scale: the multi-penalty REML engine
2464    // then learns the level amplitudes λ_ℓ directly — scale adaptivity at
2465    // ρ-speed, dead scales REML-deselected (the Duchon-ARD pattern) — and the
2466    // fitted order is read off the spectrum (ŝ = −½ · slope of ln λ̂_ℓ on
2467    // ln ε_ℓ) instead of being optimized. An explicit s > 0 pins the Mellin
2468    // weights and fuses the band into one candidate. The Mellin prefactor
2469    // ε^(−η)·log_step inside each per-scale form is absorbed by the
2470    // per-candidate Frobenius normalization, so REML owns the amplitudes
2471    // outright. The sentinel itself is persisted in the metadata as the mode
2472    // marker: a replay MUST re-enter the same mode or the penalty count
2473    // desyncs (the gam#860 trap class).
2474    let mut candidates = Vec::new();
2475    let mut penalty_normalization_scales = Vec::new();
2476    let mut raw_penalty_normalization_scales = Vec::new();
2477    let mut fused_penalty_normalization_scale = None;
2478    if per_level {
2479        let forms = measure_jet_energy_forms_per_scale(
2480            centers.view(),
2481            masses.view(),
2482            &band,
2483            order_s,
2484            spec.alpha,
2485            spec.tau0,
2486        )?;
2487        for (level, q_l) in forms.into_iter().enumerate() {
2488            // Constructive pullback, not a dense triple product: the per-scale
2489            // form is PSD by construction and so is its pullback, and only the
2490            // arithmetic can lose that (#2761).
2491            let s_l = constructive_pullback_center_form(&kz, &q_l, "measure-jet scale penalty")?;
2492            let c_l = constructive_frobenius_scale(&s_l);
2493            let intrinsic_dim = centers.ncols() as f64;
2494            let eta = 2.0 * order_s + intrinsic_dim * (2.0 - 2.0 * spec.alpha);
2495            let scale_weight = log_step * eps_band[level].powf(-eta);
2496            penalty_normalization_scales.push(c_l);
2497            raw_penalty_normalization_scales.push(c_l / scale_weight);
2498            candidates.push(PenaltyCandidate {
2499                matrix: s_l.scaled(1.0 / c_l, "normalized measure-jet scale penalty")?,
2500                source: PenaltySource::Other(format!("measure_jet_scale_{level}")),
2501                normalization_scale: c_l,
2502                kronecker_factors: None,
2503                op: None,
2504            });
2505        }
2506    } else {
2507        let q_form = measure_jet_energy_form(
2508            centers.view(),
2509            masses.view(),
2510            &band,
2511            order_s,
2512            spec.alpha,
2513            spec.tau0,
2514        )?;
2515        // The Primary is exactly the jet-energy functional pulled back through
2516        // the center evaluation map. It is independent of `double_penalty`:
2517        // statistical selection is a distinct REML component below, never a
2518        // fixed coefficient toll fused into this estimand.
2519        let penalty =
2520            constructive_pullback_center_form(&kz, &q_form, "measure-jet primary penalty")?;
2521        let c_primary = constructive_frobenius_scale(&penalty);
2522        fused_penalty_normalization_scale = Some(c_primary);
2523        // Declare the energy's structural null frame on the shipped Primary
2524        // (#2761, the #2445 mechanism): the affine head is null by theorem, and
2525        // the pullback's NUMERICAL rank falls as the representer range grows, so
2526        // a rank test on the shipped matrix would let a design-moving ℓ decide
2527        // the double-penalty topology between outer trials.
2528        let mut primary =
2529            penalty.scaled(1.0 / c_primary, "normalized measure-jet primary penalty")?;
2530        if let Some(frame) = measure_jet_primary_structural_null_frame(&z, m, head_width)? {
2531            primary = primary.with_structural_null_frame(
2532                frame,
2533                "measure-jet primary structural null declaration",
2534            )?;
2535        }
2536        candidates.push(PenaltyCandidate {
2537            matrix: primary,
2538            source: PenaltySource::Primary,
2539            normalization_scale: c_primary,
2540            kronecker_factors: None,
2541            op: None,
2542        });
2543    }
2544    // Explicit null recovery is a genuine statistical component: penalize the
2545    // affine/null FUNCTION projection under the empirical-measure mass metric,
2546    // and let REML select its strength independently in both modes. This is the
2547    // standard double-penalty decomposition (roughness + null component); no
2548    // coefficient identity and no hard-coded mixture changes the Primary.
2549    if spec.double_penalty {
2550        let null_penalty = affine_function_nullspace_quadratic(&kz, centers.view(), masses.view())?;
2551        let (_, c_null) = normalize_penalty(null_penalty.dense());
2552        candidates.push(PenaltyCandidate {
2553            matrix: null_penalty
2554                .scaled(1.0 / c_null, "normalized measure-jet null-function penalty")?,
2555            source: PenaltySource::DoublePenaltyNullspace,
2556            normalization_scale: c_null,
2557            kronecker_factors: None,
2558            op: None,
2559        });
2560        // Decide the ridge's fate in THIS chart, the way the term-collection
2561        // chokepoint decides it in its own (#2433's repair, which periodic
2562        // Duchon already carries verbatim, extended here for #2761).
2563        //
2564        // The collection applies its global gauge and then rebuilds the ridge
2565        // from `null(Primary_constrained)`; a chart that has taken the last
2566        // structural null direction leaves nothing for the ridge to shrink and
2567        // the collection drops it. A frozen composed chart — which is what
2568        // every outer ψ trial and every predict-time replay rebuilds in — is
2569        // exactly such a chart for a 1-D measure-jet term, where the parametric
2570        // orthogonalization absorbs the whole affine head. Emitting the raw
2571        // ridge there produces a LOCAL topology of 2 against the collection's
2572        // cached 1, and the incremental realizer aborts the outer search with
2573        // `topology changed ... active_penalties=2, cached_penalties=1`.
2574        //
2575        // Running the same rebuild locally makes the two layers agree by
2576        // construction instead of by coincidence. In the cold chart the head is
2577        // still present, the rebuild keeps the ridge, and this is a no-op on
2578        // the shipped topology.
2579        let primary_physical = candidates
2580            .iter()
2581            .find(|candidate| matches!(candidate.source, PenaltySource::Primary))
2582            .map(|candidate| {
2583                candidate.matrix.scaled(
2584                    candidate.normalization_scale,
2585                    "physical measure-jet primary",
2586                )
2587            })
2588            .transpose()?;
2589        if let Some(primary_physical) = primary_physical {
2590            let width = primary_physical.nrows();
2591            for candidate in &mut candidates {
2592                if !matches!(candidate.source, PenaltySource::DoublePenaltyNullspace) {
2593                    continue;
2594                }
2595                let ridge_physical = candidate.matrix.scaled(
2596                    candidate.normalization_scale,
2597                    "physical measure-jet null-function penalty",
2598                )?;
2599                match super::rebuild_metric_consistent_ridge(&primary_physical, &ridge_physical)? {
2600                    Some(rebuilt) => {
2601                        let normalized = super::normalize_constructive_penalty_candidate(
2602                            rebuilt,
2603                            PenaltySource::DoublePenaltyNullspace,
2604                        )?;
2605                        candidate.matrix = normalized.matrix;
2606                        candidate.normalization_scale = normalized.normalization_scale;
2607                    }
2608                    None => {
2609                        candidate.matrix = ConstructiveQuadratic::zero(width);
2610                        candidate.normalization_scale = 1.0;
2611                    }
2612                }
2613                candidate.kronecker_factors = None;
2614                candidate.op = None;
2615            }
2616        }
2617    }
2618    let filtered = filter_penalty_candidates(candidates)?;
2619    // #2225: compute the errors-in-variables input-noise scale while `centers`
2620    // is still owned; it is moved into the metadata `centers` field below.
2621    let sigma_coord = measure_jet_input_noise_scale(data, centers.view())?;
2622    Ok(BasisBuildResult {
2623        design,
2624        affine_offset: None,
2625        active_penalties: filtered.active,
2626        dropped_penalties: filtered.dropped,
2627        metadata: BasisMetadata::MeasureJet {
2628            centers,
2629            input_scale: crate::IsotropicScale::ONE,
2630            // The realized range from `realize_measure_jet_geometry`, in the
2631            // same frame as `centers` and `eps_band`.  Unlike the other three
2632            // Euclidean families the term-collection wrapper does NOT restore
2633            // an original-units value over this, so the standardized tag
2634            // survives to every consumer (#2636).
2635            length_scale: crate::StandardizedUnits::new(length_scale),
2636            eps_band,
2637            // The SPEC's order field, sentinel included: 0.0 marks per-level
2638            // (spectral) mode and must replay as per-level — persisting the
2639            // realized default here would silently flip the rebuild into
2640            // fused mode and desync the penalty count.
2641            order_s: spec.order_s,
2642            alpha: spec.alpha,
2643            tau0: spec.tau0,
2644            masses,
2645            support_means,
2646            penalty_normalization_scales,
2647            raw_penalty_normalization_scales,
2648            fused_penalty_normalization_scale,
2649            constraint_transform: Some(z),
2650            // Perpendicular off-manifold residual scale of the fit rows in the
2651            // centers' frame — the errors-in-variables input-noise scale (#2225).
2652            sigma_coord,
2653        },
2654        kronecker_factored: None,
2655        joint_null_rotation: None,
2656    })
2657}
2658
2659/// Exact ψ-jets of the REALIZED measure-jet penalty candidates, adapted to
2660/// the anisotropic group-ψ carrier the spatial optimizer consumes.
2661///
2662/// Coordinates (the layout contract for the registration arm):
2663/// - per-level (spectral) mode: `[ln ℓ?, α, ln τ]` — order is absorbed by the
2664///   REML-learned scale amplitudes; `ln τ` is retained as an inert coordinate;
2665/// - single-scale mode: `[ln ℓ?]`, because its energy dials are fixed.
2666///
2667/// Only `ln ℓ` moves the design. It also moves every coefficient-space penalty
2668/// pullback through the center evaluation map `E(ℓ)`; `(α, ln τ)` move only the
2669/// per-scale center-value forms. Exact diagonal and mixed product-rule jets are
2670/// emitted before Frobenius normalization.
2671/// Penalty derivatives are routed through the SAME constrained Frobenius
2672/// normalization as the fit-time candidates
2673/// (`normalize_penaltywith_psi_derivatives` + the cross rule), so criterion
2674/// value and criterion derivative share one normalization — the #901 lesson
2675/// made structural. The function-space null candidate has nonzero `ln ℓ` jets
2676/// and zero `(α, ln τ)` jets. The per-candidate layout follows the builder's
2677/// ORIGINAL order (scale candidates or Primary, then null component); consumers
2678/// align to the FITTED penalty list via
2679/// `ActivePenaltyInfo.original_index` when the candidate filter dropped
2680/// any.
2681pub fn build_measure_jet_basis_psi_derivatives(
2682    data: ArrayView2<'_, f64>,
2683    spec: &MeasureJetBasisSpec,
2684) -> Result<AnisoBasisPsiDerivatives, BasisError> {
2685    if !(spec.tau0.is_finite() && spec.tau0 > 0.0) {
2686        crate::bail_invalid_basis!(
2687            "measure-jet ψ derivatives need tau0 > 0 because the retained τ coordinate is ln τ; got {}",
2688            spec.tau0
2689        );
2690    }
2691    let geom = realize_measure_jet_geometry(data, spec)?;
2692    let band = MeasureJetBand {
2693        eps: geom.eps_band.clone(),
2694        log_step: geom.log_step,
2695    };
2696    let n = data.nrows();
2697    let p = geom.kz.ncols();
2698    let m = geom.centers.nrows();
2699    let m_aug = m + geom.head_lift.ncols();
2700
2701    struct LengthScaleJets {
2702        evaluation_first: Array2<f64>,
2703        evaluation_second: Array2<f64>,
2704        design_first: Array2<f64>,
2705        design_second: Array2<f64>,
2706    }
2707
2708    // The Gaussian representer range moves both the FIT design and the center
2709    // evaluation map `E = [K_cc | A_head] Z`. The affine head is ℓ-invariant,
2710    // so its raw derivative columns are exactly zero before applying the frozen
2711    // Gauge section. Keeping `Z` frozen is the replay contract: rank/gauge
2712    // realization happens once at fit time, then every ψ trial differentiates
2713    // the same coefficient chart.
2714    let length_scale_jets = if spec.learn_length_scale {
2715        let (dk_data, d2k_data) =
2716            measure_jet_design_log_length_jets(data, geom.centers.view(), geom.length_scale)?;
2717        let mut dk_data_aug = Array2::<f64>::zeros((n, m_aug));
2718        let mut d2k_data_aug = Array2::<f64>::zeros((n, m_aug));
2719        dk_data_aug.slice_mut(ndarray::s![.., ..m]).assign(&dk_data);
2720        d2k_data_aug
2721            .slice_mut(ndarray::s![.., ..m])
2722            .assign(&d2k_data);
2723
2724        let (dk_centers, d2k_centers) = measure_jet_design_log_length_jets(
2725            geom.centers.view(),
2726            geom.centers.view(),
2727            geom.length_scale,
2728        )?;
2729        let mut dk_centers_aug = Array2::<f64>::zeros((m, m_aug));
2730        let mut d2k_centers_aug = Array2::<f64>::zeros((m, m_aug));
2731        dk_centers_aug
2732            .slice_mut(ndarray::s![.., ..m])
2733            .assign(&dk_centers);
2734        d2k_centers_aug
2735            .slice_mut(ndarray::s![.., ..m])
2736            .assign(&d2k_centers);
2737
2738        Some(LengthScaleJets {
2739            evaluation_first: geom.coefficient_gauge.restrict_design(&dk_centers_aug),
2740            evaluation_second: geom.coefficient_gauge.restrict_design(&d2k_centers_aug),
2741            design_first: geom.coefficient_gauge.restrict_design(&dk_data_aug),
2742            design_second: geom.coefficient_gauge.restrict_design(&d2k_data_aug),
2743        })
2744    } else {
2745        None
2746    };
2747
2748    let coord_offset = usize::from(length_scale_jets.is_some());
2749    let n_coords = coord_offset + if geom.per_level { 2 } else { 0 };
2750    let pairs: Vec<(usize, usize)> = (0..n_coords)
2751        .flat_map(|a| ((a + 1)..n_coords).map(move |b| (a, b)))
2752        .collect();
2753    let zero_p = || Array2::<f64>::zeros((p, p));
2754
2755    struct RawPenaltyJets {
2756        value: Array2<f64>,
2757        first: Vec<Array2<f64>>,
2758        second_diag: Vec<Array2<f64>>,
2759        cross: Vec<Array2<f64>>,
2760    }
2761
2762    let sandwich = |form: &Array2<f64>| pullback_center_form(&geom.kz, form);
2763    let length_diag = |form: &Array2<f64>| {
2764        let jets = length_scale_jets
2765            .as_ref()
2766            .expect("length-scale form jets require an enrolled length coordinate");
2767        pullback_center_form_log_length_jets(
2768            &geom.kz,
2769            &jets.evaluation_first,
2770            &jets.evaluation_second,
2771            form,
2772        )
2773    };
2774    let length_cross = |form_first: &Array2<f64>| {
2775        let jets = length_scale_jets
2776            .as_ref()
2777            .expect("length-scale cross jets require an enrolled length coordinate");
2778        pullback_center_form_log_length_cross(&geom.kz, &jets.evaluation_first, form_first)
2779    };
2780
2781    // Raw (pre-normalization) value + exact jet stacks per ORIGINAL candidate.
2782    // Coordinate order is `[lnℓ?, α, lnτ]` in multiscale mode and `[lnℓ?]`
2783    // in single-scale mode. Candidate order exactly mirrors the value builder:
2784    // scale candidates or Primary first, then the optional null-component
2785    // candidate. Active filtering aligns through `ActivePenaltyInfo::original_index`.
2786    // The single-scale Primary, when there is one. `None` in per-level mode,
2787    // which emits scale candidates instead and therefore never rebuilds the
2788    // null component.
2789    let mut single_scale_primary: Option<ConstructiveQuadratic> = None;
2790    let mut raw: Vec<RawPenaltyJets> = if geom.per_level {
2791        let l_count = band.eps.len();
2792        // Six forms per scale: value, ∂α, ∂α², and zero τ slots — same
2793        // blocks, one walk (single-source rule).
2794        let forms = assemble_weighted_forms(
2795            geom.centers.view(),
2796            geom.masses.view(),
2797            &band,
2798            geom.order_s_eval,
2799            spec.alpha,
2800            spec.tau0,
2801            6 * l_count,
2802            3,
2803            &|scale_idx, eps: f64, q: f64, base: f64, out: &mut [[f64; 3]]| {
2804                for slot in out.iter_mut() {
2805                    *slot = [0.0, 0.0, 0.0];
2806                }
2807                let intrinsic_dim = geom.centers.ncols() as f64;
2808                let ga = 2.0 * intrinsic_dim * eps.ln() - 2.0 * q.max(f64::MIN_POSITIVE).ln();
2809                let k0 = 6 * scale_idx;
2810                out[k0] = [base, 0.0, 0.0];
2811                out[k0 + 1] = [ga * base, 0.0, 0.0];
2812                out[k0 + 2] = [ga * ga * base, 0.0, 0.0];
2813                out[k0 + 3] = [0.0, 0.0, 0.0];
2814                out[k0 + 4] = [0.0, 0.0, 0.0];
2815                out[k0 + 5] = [0.0, 0.0, 0.0];
2816            },
2817        )?;
2818        let alpha_coord = coord_offset;
2819        let tau_coord = coord_offset + 1;
2820        let mut raw = Vec::with_capacity(l_count + usize::from(spec.double_penalty));
2821        for level in 0..l_count {
2822            let chunk = &forms[6 * level..6 * level + 6];
2823            let mut first: Vec<Array2<f64>> = (0..n_coords).map(|_| zero_p()).collect();
2824            let mut second_diag: Vec<Array2<f64>> = (0..n_coords).map(|_| zero_p()).collect();
2825            first[alpha_coord] = sandwich(&chunk[1]);
2826            first[tau_coord] = sandwich(&chunk[3]);
2827            second_diag[alpha_coord] = sandwich(&chunk[2]);
2828            second_diag[tau_coord] = sandwich(&chunk[4]);
2829            if coord_offset == 1 {
2830                let (ell_first, ell_second) = length_diag(&chunk[0]);
2831                first[0] = ell_first;
2832                second_diag[0] = ell_second;
2833            }
2834            let mut cross: Vec<Array2<f64>> = (0..pairs.len()).map(|_| zero_p()).collect();
2835            for (pair_idx, &(a, b)) in pairs.iter().enumerate() {
2836                cross[pair_idx] = if coord_offset == 1 && a == 0 && b == alpha_coord {
2837                    length_cross(&chunk[1])
2838                } else if coord_offset == 1 && a == 0 && b == tau_coord {
2839                    length_cross(&chunk[3])
2840                } else if a == alpha_coord && b == tau_coord {
2841                    sandwich(&chunk[5])
2842                } else {
2843                    zero_p()
2844                };
2845            }
2846            raw.push(RawPenaltyJets {
2847                value: sandwich(&chunk[0]),
2848                first,
2849                second_diag,
2850                cross,
2851            });
2852        }
2853        raw
2854    } else {
2855        // Single-scale mode enrolls no `(s, α, lnτ)` penalty dials. It still
2856        // emits the pure Primary and, when requested, a separate REML null
2857        // component; an opt-in `lnℓ` coordinate differentiates both pullbacks.
2858        let q_form = measure_jet_energy_form(
2859            geom.centers.view(),
2860            geom.masses.view(),
2861            &band,
2862            geom.order_s_eval,
2863            spec.alpha,
2864            spec.tau0,
2865        )?;
2866        let mut first: Vec<Array2<f64>> = (0..n_coords).map(|_| zero_p()).collect();
2867        let mut second_diag: Vec<Array2<f64>> = (0..n_coords).map(|_| zero_p()).collect();
2868        if coord_offset == 1 {
2869            let (ell_first, ell_second) = length_diag(&q_form);
2870            first[0] = ell_first;
2871            second_diag[0] = ell_second;
2872        }
2873        // Keep the Primary the builder would emit: the null component's shipped
2874        // matrix is a REBUILD off it, so the producer needs the same object to
2875        // differentiate the same thing (see below).
2876        single_scale_primary = Some(constructive_pullback_center_form(
2877            &geom.kz,
2878            &q_form,
2879            "measure-jet primary penalty",
2880        )?);
2881        if let (Some(primary), Some(frame)) = (
2882            single_scale_primary.as_mut(),
2883            measure_jet_primary_structural_null_frame(&geom.z, m, geom.head_lift.ncols())?,
2884        ) {
2885            *primary = primary.clone().with_structural_null_frame(
2886                frame,
2887                "measure-jet primary structural null declaration",
2888            )?;
2889        }
2890        vec![RawPenaltyJets {
2891            value: sandwich(&q_form),
2892            first,
2893            second_diag,
2894            cross: Vec::new(),
2895        }]
2896    };
2897
2898    if spec.double_penalty {
2899        let null_center =
2900            affine_function_nullspace_center_quadratic(geom.centers.view(), geom.masses.view())?;
2901        let null_form = null_center.dense();
2902        let mut first: Vec<Array2<f64>> = (0..n_coords).map(|_| zero_p()).collect();
2903        let mut second_diag: Vec<Array2<f64>> = (0..n_coords).map(|_| zero_p()).collect();
2904        if coord_offset == 1 {
2905            let (ell_first, ell_second) = length_diag(null_form);
2906            first[0] = ell_first;
2907            second_diag[0] = ell_second;
2908        }
2909        let mut value = sandwich(null_form);
2910        // The builder does NOT ship this raw pullback when a Primary exists: it
2911        // ships `rebuild_metric_consistent_ridge`'s output, `R = N M Nᵀ` with
2912        // `M = Nᵀ (EᵀH₀E) N` and `N` the Primary's declared structural null
2913        // frame. Differentiating the raw pullback instead is an
2914        // objective↔gradient desync on the `ln ℓ` coordinate, and #2761
2915        // measured it as the WHOLE of that coordinate's gradient error:
2916        //
2917        //   arm                       analytic     Ridders FD     rel
2918        //   double_penalty = true    -1.124278e1  -1.149171e1   2.2e-2
2919        //   double_penalty = false   -1.1255398e1 -1.1255398e1   9e-10
2920        //
2921        // with the total's per-atom breakdown putting it in `logdet_S`
2922        // (−0.2369) and `fixed_beta` (+0.4858). λ_null being tiny does not
2923        // shrink it: on the directions the Primary annihilates, `S_λ` IS
2924        // `λ_null·S_null`, so `λ_null` cancels out of
2925        // `tr(S_λ⁺ ∂S_λ/∂ψ)` and a wrong `∂S_null/∂ψ` lands at full size.
2926        //
2927        // The exact jets of the rebuilt object are `N (Nᵀ ∂S_raw N) Nᵀ`, since
2928        // `N` is ψ-fixed by construction (it is a declaration, not a rank
2929        // test). They are numerically ZERO here — `N`'s columns carry no
2930        // representer coefficients, so `E·N` is ℓ-invariant and `M` cannot
2931        // move — but computing them rather than asserting them keeps the
2932        // producer correct if a future frame does move.
2933        if let Some(primary) = single_scale_primary.as_ref() {
2934            let ridge_physical = ConstructiveQuadratic::from_energy_factor(
2935                null_center.factor().dot(&geom.kz),
2936                "measure-jet affine/null coefficient penalty",
2937            )?;
2938            match super::rebuild_metric_consistent_ridge(primary, &ridge_physical)? {
2939                Some(rebuilt) => {
2940                    let frame = primary
2941                        .structural_null_frame()
2942                        .cloned()
2943                        .unwrap_or_else(|| Array2::<f64>::zeros((p, 0)));
2944                    for coord in 0..n_coords {
2945                        first[coord] = restrict_jet_to_frame(&first[coord], &frame);
2946                        second_diag[coord] = restrict_jet_to_frame(&second_diag[coord], &frame);
2947                    }
2948                    value = rebuilt.dense().clone();
2949                }
2950                None => {
2951                    // The rebuild declined, so the builder ships an exact zero
2952                    // and the candidate is dropped. A dropped candidate has no
2953                    // derivative.
2954                    for coord in 0..n_coords {
2955                        first[coord] = zero_p();
2956                        second_diag[coord] = zero_p();
2957                    }
2958                    value = zero_p();
2959                }
2960            }
2961        }
2962        raw.push(RawPenaltyJets {
2963            value,
2964            first,
2965            second_diag,
2966            // H₀ is independent of α and τ; its only moving object is E(ℓ),
2967            // so every mixed coordinate derivative is zero.
2968            cross: (0..pairs.len()).map(|_| zero_p()).collect(),
2969        });
2970    }
2971
2972    let n_cands = raw.len();
2973    let mut penalties_first: Vec<Vec<Array2<f64>>> =
2974        (0..n_coords).map(|_| Vec::with_capacity(n_cands)).collect();
2975    let mut penalties_second_diag: Vec<Vec<Array2<f64>>> =
2976        (0..n_coords).map(|_| Vec::with_capacity(n_cands)).collect();
2977    // Cross matrices per pair per candidate, precomputed eagerly (the
2978    // candidate count is the band length, not the data size) and served
2979    // through the on-demand provider.
2980    let mut crosses: Vec<Vec<Array2<f64>>> = (0..pairs.len()).map(|_| Vec::new()).collect();
2981    for candidate in &raw {
2982        let s_raw = &candidate.value;
2983        // ONE Frobenius scale per candidate, fixed up front from `s_raw`
2984        // alone: c anchors the value and every derivative of this candidate.
2985        // `normalize_penaltywith_psi_derivatives` recomputes the identical c
2986        // per coordinate (same trace_of_product + sqrt on the same `s_raw`),
2987        // and its degenerate convention is mirrored here: ‖S‖_F ≤ 1e-12 (or
2988        // non-finite) reports scale 1.0 — the value passes through unscaled,
2989        // and the cross helper receives that same 1.0, never a collapsed
2990        // near-zero scale.
2991        let fro = trace_of_product(s_raw, s_raw).sqrt();
2992        let c = if fro.is_finite() && fro > 1e-12 {
2993            fro
2994        } else {
2995            1.0
2996        };
2997        for coord in 0..n_coords {
2998            let (_, s_first, s_second, _) = normalize_penaltywith_psi_derivatives(
2999                s_raw,
3000                &candidate.first[coord],
3001                &candidate.second_diag[coord],
3002            );
3003            penalties_first[coord].push(s_first);
3004            penalties_second_diag[coord].push(s_second);
3005        }
3006        for (pair_idx, &(a, b)) in pairs.iter().enumerate() {
3007            let cross_raw_mat = normalize_penalty_cross_psi_derivative(
3008                s_raw,
3009                &candidate.first[a],
3010                &candidate.first[b],
3011                &candidate.cross[pair_idx],
3012                c,
3013            );
3014            crosses[pair_idx].push(cross_raw_mat);
3015        }
3016    }
3017
3018    let pair_index: Vec<((usize, usize), Vec<Array2<f64>>)> =
3019        pairs.iter().copied().zip(crosses.into_iter()).collect();
3020    let provider = AnisoPenaltyCrossProvider::new(move |a, b| {
3021        pair_index
3022            .iter()
3023            .find(|((pa, pb), _)| (*pa, *pb) == (a, b) || (*pa, *pb) == (b, a))
3024            .map(|(_, mats)| mats.clone())
3025            .ok_or_else(|| {
3026                BasisError::InvalidInput(format!(
3027                    "measure-jet ψ cross derivative requested for unknown pair ({a}, {b})"
3028                ))
3029            })
3030    });
3031    let mut design_first: Vec<Array2<f64>> = (0..n_coords)
3032        .map(|_| Array2::<f64>::zeros((n, p)))
3033        .collect();
3034    let mut design_second_diag: Vec<Array2<f64>> = (0..n_coords)
3035        .map(|_| Array2::<f64>::zeros((n, p)))
3036        .collect();
3037    if let Some(jets) = &length_scale_jets {
3038        design_first[0] = jets.design_first.clone();
3039        design_second_diag[0] = jets.design_second.clone();
3040    }
3041    Ok(AnisoBasisPsiDerivatives {
3042        design_first,
3043        design_second_diag,
3044        design_second_cross: Vec::new(),
3045        design_second_cross_pairs: Vec::new(),
3046        penalties_first,
3047        penalties_second_diag,
3048        penalties_cross_pairs: pairs,
3049        penalties_cross_provider: Some(provider),
3050        implicit_operator: None,
3051    })
3052}
3053
3054#[cfg(test)]
3055mod tests {
3056    use super::*;
3057
3058    /// Deterministic Box–Muller standard normal from a 64-bit LCG state — a
3059    /// self-contained noise generator (no external RNG dependency).
3060    fn lcg_normal(state: &mut u64) -> f64 {
3061        let mut next = || {
3062            *state = state
3063                .wrapping_mul(6364136223846793005)
3064                .wrapping_add(1442695040888963407);
3065            // Top 53 bits → uniform (0, 1).
3066            (((*state >> 11) as f64) + 0.5) / (1u64 << 53) as f64
3067        };
3068        let u1 = next();
3069        let u2 = next();
3070        (-2.0 * u1.ln()).sqrt() * (std::f64::consts::TAU * u2).cos()
3071    }
3072
3073    /// The perpendicular off-manifold residual estimator recovers a KNOWN
3074    /// ambient noise scale on a 1-D manifold (a line) embedded in 2-D: points
3075    /// sampled along the tangent with isotropic-perpendicular Gaussian noise of
3076    /// scale σ, centers spaced along the line. The local-PCA smallest-eigenvalue
3077    /// floor must return ≈ σ (#2225).
3078    #[test]
3079    pub(crate) fn input_noise_scale_recovers_known_perpendicular_sigma() {
3080        // Line direction (unit) and its perpendicular in 2-D.
3081        let tang = [1.0 / 5f64.sqrt(), 2.0 / 5f64.sqrt()];
3082        let perp = [2.0 / 5f64.sqrt(), -1.0 / 5f64.sqrt()];
3083        let sigma = 0.05_f64;
3084        let n = 600usize;
3085        let mut state = 0x1234_5678_9abc_def0u64;
3086        let mut data = Array2::<f64>::zeros((n, 2));
3087        for j in 0..n {
3088            // Tangential coordinate marches deterministically over [0, 3].
3089            let t = 3.0 * (j as f64) / (n as f64 - 1.0);
3090            let noise = sigma * lcg_normal(&mut state);
3091            for a in 0..2 {
3092                data[(j, a)] = t * tang[a] + noise * perp[a];
3093            }
3094        }
3095        // Centers along the line (on the noiseless manifold): plenty of points
3096        // per cell to span the tangent.
3097        let n_centers = 8usize;
3098        let mut centers = Array2::<f64>::zeros((n_centers, 2));
3099        for i in 0..n_centers {
3100            let t = 3.0 * (i as f64 + 0.5) / (n_centers as f64);
3101            for a in 0..2 {
3102                centers[(i, a)] = t * tang[a];
3103            }
3104        }
3105        let est = measure_jet_input_noise_scale(data.view(), centers.view())
3106            .expect("estimate ok")
3107            .expect("noise scale present");
3108        // Sample smallest-eigenvalue floor is mildly downward-biased; require it
3109        // within 40% of the truth (central estimate, not a tuned tolerance).
3110        assert!(
3111            (est - sigma).abs() <= 0.4 * sigma,
3112            "estimated σ_coord {est} far from true {sigma}"
3113        );
3114    }
3115
3116    /// Too few points per cell (cannot span a d-dim tangent) ⇒ no estimate,
3117    /// so the caller leaves Var_input disabled rather than invent a scale.
3118    #[test]
3119    pub(crate) fn input_noise_scale_none_when_cells_too_small() {
3120        let data = array![[0.0, 0.0], [1.0, 2.0], [2.0, 4.0]];
3121        let centers = array![[0.0, 0.0], [1.0, 2.0], [2.0, 4.0]];
3122        // Each point is its own nearest center (1 point per cell < d + 1 = 3).
3123        assert!(
3124            measure_jet_input_noise_scale(data.view(), centers.view())
3125                .expect("estimate ok")
3126                .is_none()
3127        );
3128    }
3129
3130    pub(crate) fn two_cluster_centers() -> (ndarray::Array2<f64>, ndarray::Array1<f64>) {
3131        let centers = array![
3132            [0.00, 0.00],
3133            [0.31, 0.05],
3134            [0.58, -0.07],
3135            [0.93, 0.11],
3136            [1.22, 0.02],
3137            [1.49, -0.04],
3138            [3.10, 2.00],
3139            [3.42, 2.13],
3140            [3.71, 1.91],
3141            [4.05, 2.07],
3142            [4.33, 1.96],
3143            [4.61, 2.12],
3144        ];
3145        let m = centers.nrows();
3146        let masses = ndarray::Array1::<f64>::from_elem(m, 1.0 / m as f64);
3147        (centers, masses)
3148    }
3149    use ndarray::array;
3150
3151    pub(crate) fn band_for(centers: &Array2<f64>) -> MeasureJetBand {
3152        measure_jet_band(centers.view(), 0).expect("band")
3153    }
3154
3155    /// The no-mass contract: constants must be annihilated to machine
3156    /// precision at every scale (the constant is projected, never ridged).
3157    #[test]
3158    pub(crate) fn energy_form_annihilates_constants_exactly() {
3159        let (centers, masses) = two_cluster_centers();
3160        let band = band_for(&centers);
3161        let q = measure_jet_energy_form(centers.view(), masses.view(), &band, 1.5, 1.0, 1e-3)
3162            .expect("energy form");
3163        let m = q.nrows();
3164        let ones = Array1::<f64>::ones(m);
3165        let qv = q.dot(&ones);
3166        let scale = q.iter().fold(0.0_f64, |acc, v| acc.max(v.abs()));
3167        assert!(scale > 0.0, "energy form is identically zero");
3168        for (i, v) in qv.iter().enumerate() {
3169            assert!(
3170                v.abs() <= 1e-12 * scale,
3171                "Q·1 leak at row {i}: {v:.3e} vs scale {scale:.3e}"
3172            );
3173        }
3174        let vqv = ones.dot(&qv);
3175        assert!(
3176            vqv.abs() <= 1e-12 * scale,
3177            "constant carries energy: 1ᵀQ1 = {vqv:.3e}"
3178        );
3179    }
3180
3181    /// The default local projection annihilates ambient affine functions
3182    /// exactly; τ is retained for ψ layout but no longer adds an affine toll.
3183    #[test]
3184    pub(crate) fn energy_form_annihilates_affine_at_default_tau() {
3185        let (centers, masses) = two_cluster_centers();
3186        let band = band_for(&centers);
3187        let m = centers.nrows();
3188        // Affine values v = 0.7 + 1.3·x − 0.4·y, and a rough ±1 checkerboard.
3189        let mut affine = Array1::<f64>::zeros(m);
3190        let mut rough = Array1::<f64>::zeros(m);
3191        for i in 0..m {
3192            affine[i] = 0.7 + 1.3 * centers[(i, 0)] - 0.4 * centers[(i, 1)];
3193            rough[i] = if i % 2 == 0 { 1.0 } else { -1.0 };
3194        }
3195        let q = measure_jet_energy_form(centers.view(), masses.view(), &band, 1.5, 1.0, 1e-3)
3196            .expect("energy form");
3197        let e_affine = affine.dot(&q.dot(&affine));
3198        let e_rough = rough.dot(&q.dot(&rough));
3199        assert!(e_rough > 0.0, "rough vector must pay energy");
3200        assert!(
3201            e_affine.abs() <= 1e-12 * e_rough,
3202            "default affine energy {e_affine:.3e} vs rough {e_rough:.3e}"
3203        );
3204    }
3205
3206    /// PSD: the energy is a sum of weighted least-squares residuals.
3207    #[test]
3208    pub(crate) fn energy_form_is_psd() {
3209        let (centers, masses) = two_cluster_centers();
3210        let band = band_for(&centers);
3211        let q = measure_jet_energy_form(centers.view(), masses.view(), &band, 1.5, 1.0, 1e-3)
3212            .expect("energy form");
3213        let m = q.nrows();
3214        for trial in 0..5usize {
3215            let v = Array1::<f64>::from_shape_fn(m, |i| {
3216                ((i * 7 + trial * 13) % 11) as f64 / 11.0 - 0.5
3217            });
3218            let e = v.dot(&q.dot(&v));
3219            assert!(e >= -1e-10, "vᵀQv = {e:.3e} < 0 on trial {trial}");
3220        }
3221    }
3222
3223    /// A 1-D filament embedded in 2-D: high-frequency center values along the
3224    /// strand pay strictly more energy than a slow trend.
3225    #[test]
3226    pub(crate) fn rough_vector_pays_more_than_smooth() {
3227        let m = 24usize;
3228        let centers = Array2::<f64>::from_shape_fn((m, 2), |(i, k)| {
3229            let t = i as f64 / (m as f64 - 1.0);
3230            if k == 0 {
3231                t * 4.0
3232            } else {
3233                0.3 * (t * 4.0).sin()
3234            }
3235        });
3236        let masses = Array1::<f64>::from_elem(m, 1.0 / m as f64);
3237        let band = band_for(&centers);
3238        let q = measure_jet_energy_form(centers.view(), masses.view(), &band, 1.5, 1.0, 1e-3)
3239            .expect("energy form");
3240        let slow = Array1::<f64>::from_shape_fn(m, |i| (i as f64 / (m as f64 - 1.0)).powi(2));
3241        let fast = Array1::<f64>::from_shape_fn(m, |i| if i % 2 == 0 { 0.5 } else { -0.5 });
3242        let e_slow = slow.dot(&q.dot(&slow));
3243        let e_fast = fast.dot(&q.dot(&fast));
3244        assert!(
3245            e_fast > 10.0 * e_slow,
3246            "alternating values must pay >> a slow trend: fast {e_fast:.3e} vs slow {e_slow:.3e}"
3247        );
3248    }
3249
3250    /// The support curve separates on-web from off-web queries at fine
3251    /// scales and grows monotonically in ε for any query.
3252    #[test]
3253    pub(crate) fn support_curve_separates_on_web_from_off_web() {
3254        let m = 24usize;
3255        let centers = Array2::<f64>::from_shape_fn((m, 2), |(i, k)| {
3256            let t = i as f64 / (m as f64 - 1.0);
3257            if k == 0 { t * 4.0 } else { 0.0 }
3258        });
3259        let masses = Array1::<f64>::from_elem(m, 1.0 / m as f64);
3260        let band = band_for(&centers);
3261        let queries = array![[2.0, 0.0], [2.0, 1.5]];
3262        let curves =
3263            measure_jet_support_curve(queries.view(), centers.view(), masses.view(), &band.eps)
3264                .expect("support curve");
3265        // On-web sees strictly more mass than off-web at the finest scale.
3266        assert!(
3267            curves[(0, 0)] > 10.0 * curves[(1, 0)],
3268            "fine-scale support must separate web from void: on {:.3e} vs off {:.3e}",
3269            curves[(0, 0)],
3270            curves[(1, 0)]
3271        );
3272        // Kernel mass is monotone in ε for every query.
3273        for qi in 0..2 {
3274            for li in 1..band.eps.len() {
3275                assert!(
3276                    curves[(qi, li)] >= curves[(qi, li - 1)] - 1e-15,
3277                    "support curve must be monotone in scale (query {qi}, level {li})"
3278                );
3279            }
3280        }
3281    }
3282
3283    /// The default is single-scale mode at ANY center count: one Primary
3284    /// jet-energy candidate plus the independently REML-selected affine/null
3285    /// component requested by the default `double_penalty`. Multiscale (the
3286    /// per-scale spectral split + ψ dials) is an EXPLICIT opt-in
3287    /// (`spec.multiscale`, the DSL `mjs(…, multiscale=true)`) — there is no
3288    /// center-count auto-gate (#1116). `measure_jet_multiscale_mode` is the
3289    /// single source for this decision.
3290    #[test]
3291    pub(crate) fn default_stays_single_scale_until_multiscale_opt_in() {
3292        let n = 200usize;
3293        let data = Array2::<f64>::from_shape_fn((n, 2), |(i, k)| {
3294            let t = i as f64 / (n as f64 - 1.0);
3295            if k == 0 {
3296                t * 3.0
3297            } else {
3298                0.4 * (t * 3.0).sin()
3299            }
3300        });
3301        // Default (multiscale = false) stays single-scale even at a LARGE center
3302        // count that, under the deleted auto-gate, would have flipped to
3303        // multiscale: one pure Primary plus one function-space null component.
3304        let single = MeasureJetBasisSpec {
3305            center_strategy: CenterStrategy::FarthestPoint { num_centers: 80 },
3306            ..MeasureJetBasisSpec::default()
3307        };
3308        assert!(
3309            !measure_jet_multiscale_mode(&single),
3310            "default must resolve to single-scale at any center count"
3311        );
3312        let built_single =
3313            build_measure_jet_basis(data.view(), &single).expect("single-scale build");
3314        assert_eq!(
3315            built_single.active_penalties.len(),
3316            2,
3317            "single-scale double-penalty mode emits Primary + affine/null component"
3318        );
3319        assert!(matches!(
3320            built_single.active_penalties[0].info.source,
3321            PenaltySource::Primary
3322        ));
3323        assert!(matches!(
3324            built_single.active_penalties[1].info.source,
3325            PenaltySource::DoublePenaltyNullspace
3326        ));
3327        // The explicit opt-in flips to multiscale at the SAME center count: the
3328        // per-scale spectral split (several candidates) plus the same explicit
3329        // null-component candidate, strictly more candidates than single-scale.
3330        let multi = MeasureJetBasisSpec {
3331            center_strategy: CenterStrategy::FarthestPoint { num_centers: 80 },
3332            multiscale: true,
3333            ..MeasureJetBasisSpec::default()
3334        };
3335        assert!(
3336            measure_jet_multiscale_mode(&multi),
3337            "multiscale=true must resolve to multiscale mode"
3338        );
3339        let built_multi = build_measure_jet_basis(data.view(), &multi).expect("multiscale build");
3340        assert!(
3341            built_multi.active_penalties.len() > built_single.active_penalties.len(),
3342            "multiscale mode emits the per-scale spectral split plus null selection, got {} (vs single-scale {})",
3343            built_multi.active_penalties.len(),
3344            built_single.active_penalties.len()
3345        );
3346    }
3347
3348    /// An explicit order pins the Mellin weights and fuses the band into a
3349    /// single Primary candidate. Disabling explicit null recovery leaves exactly
3350    /// that candidate; enabling it must never alter the Primary itself.
3351    #[test]
3352    pub(crate) fn fused_mode_without_double_penalty_emits_single_primary_candidate() {
3353        let n = 40usize;
3354        let data = Array2::<f64>::from_shape_fn((n, 2), |(i, k)| {
3355            let t = i as f64 / (n as f64 - 1.0);
3356            if k == 0 {
3357                t * 3.0
3358            } else {
3359                0.4 * (t * 3.0).sin()
3360            }
3361        });
3362        let spec = MeasureJetBasisSpec {
3363            center_strategy: CenterStrategy::FarthestPoint { num_centers: 14 },
3364            order_s: 1.3,
3365            double_penalty: false,
3366            ..MeasureJetBasisSpec::default()
3367        };
3368        let built = build_measure_jet_basis(data.view(), &spec).expect("fused build");
3369        assert_eq!(
3370            built.active_penalties.len(),
3371            1,
3372            "single-scale mode without null recovery emits exactly one Primary"
3373        );
3374        assert!(matches!(
3375            built.active_penalties[0].info.source,
3376            PenaltySource::Primary
3377        ));
3378        let BasisMetadata::MeasureJet { order_s, .. } = &built.metadata else {
3379            panic!("measure-jet build must return MeasureJet metadata");
3380        };
3381        assert_eq!(*order_s, 1.3, "explicit order must persist verbatim");
3382    }
3383
3384    /// The single-scale affine head is a gauge-fixed decomposition, not a
3385    /// coefficient ridge: RBF center values are exactly mass-orthogonal to the
3386    /// supported affine space, and replacing those directions with the head
3387    /// keeps the RAW chart exactly `m` wide. The collection's parametric
3388    /// orthogonalization then removes the head's constant, landing the FIT
3389    /// chart at `m - 1` — the width this test asserted directly before #2751,
3390    /// when the head omitted the constant and the centering took a linear
3391    /// direction instead.
3392    #[test]
3393    pub(crate) fn single_scale_affine_head_gauge_annihilates_center_cross() {
3394        let n = 90usize;
3395        let data = Array2::<f64>::from_shape_fn((n, 2), |(i, k)| {
3396            let t = i as f64 / (n as f64 - 1.0);
3397            if k == 0 {
3398                3.0 * t
3399            } else {
3400                (2.0 * std::f64::consts::PI * t).sin() + 0.2 * t
3401            }
3402        });
3403        let spec = MeasureJetBasisSpec {
3404            center_strategy: CenterStrategy::FarthestPoint { num_centers: 18 },
3405            double_penalty: false,
3406            multiscale: false,
3407            ..MeasureJetBasisSpec::default()
3408        };
3409        let geom = realize_measure_jet_geometry(data.view(), &spec).expect("realized geometry");
3410        let m = geom.centers.nrows();
3411        let head_width = geom.head_lift.ncols();
3412        assert!(head_width > 0, "fixture must realize an affine head");
3413        assert_eq!(
3414            geom.z.ncols(),
3415            m,
3416            "the affine head replaces the RBF block's affine directions one for one: the RAW \
3417             chart is exactly m wide (m - head_width representers + head_width head columns). \
3418             The collection's parametric orthogonalization then removes the constant, landing \
3419             the FIT chart at m - 1 (#2751)"
3420        );
3421        let rbf_rank = m - head_width;
3422        let z_rbf = geom.z.slice(ndarray::s![..m, ..rbf_rank]).to_owned();
3423        let k_cc =
3424            measure_jet_design_matrix(geom.centers.view(), geom.centers.view(), geom.length_scale)
3425                .expect("center kernel");
3426        let affine = measure_jet_affine_value_basis(geom.centers.view(), geom.masses.view());
3427        assert_eq!(affine.ncols(), head_width);
3428        let mut weighted_affine = affine.clone();
3429        for (i, mut row) in weighted_affine.outer_iter_mut().enumerate() {
3430            row.mapv_inplace(|v| v * geom.masses[i]);
3431        }
3432        let constraint_cross = k_cc.t().dot(&weighted_affine);
3433        let residual = constraint_cross.t().dot(&z_rbf);
3434        let scale = constraint_cross
3435            .iter()
3436            .fold(1.0_f64, |acc, value| acc.max(value.abs()));
3437        assert!(
3438            residual.iter().all(|value| value.abs() <= 1e-10 * scale),
3439            "A^T W Kcc Z_rbf must vanish; max residual {:.3e}",
3440            residual
3441                .iter()
3442                .fold(0.0_f64, |acc, value| acc.max(value.abs()))
3443        );
3444    }
3445
3446    /// `double_penalty` adds a distinct evidence-selected component and cannot
3447    /// mutate the jet-energy estimand carried by Primary.
3448    #[test]
3449    pub(crate) fn double_penalty_leaves_primary_matrix_unchanged() {
3450        let n = 64usize;
3451        let data = Array2::<f64>::from_shape_fn((n, 2), |(i, k)| {
3452            let t = i as f64 / (n as f64 - 1.0);
3453            if k == 0 { 2.5 * t } else { (4.0 * t).cos() }
3454        });
3455        let base = MeasureJetBasisSpec {
3456            center_strategy: CenterStrategy::FarthestPoint { num_centers: 16 },
3457            order_s: 1.25,
3458            double_penalty: false,
3459            ..MeasureJetBasisSpec::default()
3460        };
3461        let without = build_measure_jet_basis(data.view(), &base).expect("primary-only build");
3462        let with = build_measure_jet_basis(
3463            data.view(),
3464            &MeasureJetBasisSpec {
3465                double_penalty: true,
3466                ..base.clone()
3467            },
3468        )
3469        .expect("double-penalty build");
3470        assert_eq!(without.active_penalties.len(), 1);
3471        assert_eq!(with.active_penalties.len(), 2);
3472        assert!(matches!(
3473            without.active_penalties[0].info.source,
3474            PenaltySource::Primary
3475        ));
3476        assert!(matches!(
3477            with.active_penalties[0].info.source,
3478            PenaltySource::Primary
3479        ));
3480        assert!(matches!(
3481            with.active_penalties[1].info.source,
3482            PenaltySource::DoublePenaltyNullspace
3483        ));
3484        assert!(
3485            without.active_penalties[0]
3486                .matrix
3487                .iter()
3488                .zip(with.active_penalties[0].matrix.iter())
3489                .all(|(a, b)| (a - b).abs() <= 1e-13),
3490            "turning on null recovery must not modify Primary"
3491        );
3492    }
3493
3494    /// The Householder basis must be orthonormal with sum-to-zero columns.
3495    #[test]
3496    pub(crate) fn householder_sum_to_zero_basis_is_orthonormal() {
3497        let m = 9usize;
3498        let u = householder_sum_to_zero_u(m);
3499        let z = householder_sum_to_zero_z(&u);
3500        for j in 0..(m - 1) {
3501            let col_j = z.column(j);
3502            assert!(col_j.sum().abs() <= 1e-12, "column {j} must sum to zero");
3503            for j2 in j..(m - 1) {
3504                let dot = col_j.dot(&z.column(j2));
3505                let want = if j == j2 { 1.0 } else { 0.0 };
3506                assert!(
3507                    (dot - want).abs() <= 1e-12,
3508                    "orthonormality failure at ({j}, {j2}): {dot}"
3509                );
3510            }
3511        }
3512    }
3513
3514    /// Frozen-geometry fixture shared by the ψ-producer FD gates: build
3515    /// once, pin everything (nodes, masses, band, transform, realized ℓ),
3516    /// and return the pinned spec so dial-perturbed rebuilds move ONLY the
3517    /// dials — the per-trial contract the optimizer relies on.
3518    pub(crate) fn frozen_spec_fixture(
3519        order_s: f64,
3520        multiscale: bool,
3521    ) -> (Array2<f64>, MeasureJetBasisSpec) {
3522        // Multiscale (per-scale + ψ) mode is the explicit opt-in (#1116); the
3523        // per-level fixture passes `multiscale = true`, the fused fixture
3524        // `false`. A large center count is kept so the multiscale spectrum is
3525        // identifiable when opted in.
3526        let n = 140usize;
3527        let data = Array2::<f64>::from_shape_fn((n, 2), |(i, k)| {
3528            let t = i as f64 / (n as f64 - 1.0);
3529            if k == 0 {
3530                t * 3.0
3531            } else {
3532                0.5 * (t * 3.0).cos() + if i % 9 == 0 { 0.8 } else { 0.0 }
3533            }
3534        });
3535        let spec = MeasureJetBasisSpec {
3536            center_strategy: CenterStrategy::FarthestPoint { num_centers: 70 },
3537            order_s,
3538            multiscale,
3539            // These fixtures gate the PENALTY-dial derivatives; freeze ℓ so the
3540            // coordinate layout is exactly the penalty dials (the design-moving
3541            // ℓ dial has its own FD gate, `psi_producer_matches_fd_length_scale`).
3542            learn_length_scale: false,
3543            ..MeasureJetBasisSpec::default()
3544        };
3545        let first = build_measure_jet_basis(data.view(), &spec).expect("fixture build");
3546        let BasisMetadata::MeasureJet {
3547            centers,
3548            length_scale,
3549            eps_band,
3550            masses,
3551            support_means,
3552            penalty_normalization_scales,
3553            raw_penalty_normalization_scales,
3554            fused_penalty_normalization_scale,
3555            constraint_transform,
3556            ..
3557        } = &first.metadata
3558        else {
3559            panic!("measure-jet build must return MeasureJet metadata");
3560        };
3561        let frozen = MeasureJetBasisSpec {
3562            center_strategy: CenterStrategy::UserProvided(centers.clone()),
3563            order_s,
3564            alpha: spec.alpha,
3565            tau0: spec.tau0,
3566            num_scales: eps_band.len(),
3567            // MeasureJet freezes its range STANDARDIZED and replays it
3568            // verbatim; the tag is what records that it is the odd family out.
3569            length_scale: length_scale.standardized_value(),
3570            double_penalty: spec.double_penalty,
3571            learn_length_scale: false,
3572            multiscale,
3573            identifiability: MeasureJetIdentifiability::FrozenTransform {
3574                transform: constraint_transform.clone().expect("fit-time z"),
3575            },
3576            frozen_quadrature: Some(MeasureJetFrozenQuadrature {
3577                masses: masses.clone(),
3578                eps_band: eps_band.clone(),
3579                support_means: support_means.clone(),
3580                penalty_normalization_scales: penalty_normalization_scales.clone(),
3581                raw_penalty_normalization_scales: raw_penalty_normalization_scales.clone(),
3582                fused_penalty_normalization_scale: *fused_penalty_normalization_scale,
3583                sigma_coord: None,
3584            }),
3585        };
3586        (data, frozen)
3587    }
3588
3589    /// ψ-producer vs central finite differences of the NORMALIZED fit-time
3590    /// candidates under frozen geometry — per-level mode (coords α, lnτ).
3591    /// This is the end-to-end gate #901 never had: the derivative is checked
3592    /// against the exact object the optimizer consumes.
3593    #[test]
3594    pub(crate) fn psi_producer_matches_fd_per_level_mode() {
3595        let (data, frozen) = frozen_spec_fixture(0.0, true);
3596        let derivs =
3597            build_measure_jet_basis_psi_derivatives(data.view(), &frozen).expect("psi derivatives");
3598        let l_count = frozen
3599            .frozen_quadrature
3600            .as_ref()
3601            .expect("frozen quadrature")
3602            .eps_band
3603            .len();
3604        assert_eq!(
3605            derivs.penalties_first.len(),
3606            2,
3607            "per-level coords are (α, lnτ)"
3608        );
3609        assert_eq!(derivs.penalties_first[0].len(), l_count + 1);
3610        assert_eq!(derivs.penalties_cross_pairs, vec![(0, 1)]);
3611        let pen_at = |alpha: f64, tau0: f64| {
3612            let trial = MeasureJetBasisSpec {
3613                alpha,
3614                tau0,
3615                ..frozen.clone()
3616            };
3617            build_measure_jet_basis(data.view(), &trial)
3618                .expect("trial build")
3619                .active_penalties
3620                .into_iter()
3621                .map(|penalty| penalty.matrix)
3622                .collect::<Vec<_>>()
3623        };
3624        // Second-difference-optimal step (see the jets FD test): the 4-point
3625        // cross stencil shares the ~ε·scale/h² roundoff floor.
3626        let h = 1e-4;
3627        let (a0, t0) = (frozen.alpha, frozen.tau0);
3628        let ap = pen_at(a0 + h, t0);
3629        let am = pen_at(a0 - h, t0);
3630        let tp = pen_at(a0, t0 * h.exp());
3631        let tm = pen_at(a0, t0 * (-h).exp());
3632        assert_eq!(
3633            ap.len(),
3634            l_count + 1,
3635            "fixture must keep every scale active"
3636        );
3637        for level in 0..l_count {
3638            let fd_alpha = (&ap[level] - &am[level]) / (2.0 * h);
3639            let fd_tau = (&tp[level] - &tm[level]) / (2.0 * h);
3640            for (name, analytic, fd) in [
3641                ("alpha", &derivs.penalties_first[0][level], fd_alpha),
3642                ("ln_tau", &derivs.penalties_first[1][level], fd_tau),
3643            ] {
3644                let scale = fd.iter().fold(1e-30_f64, |acc, v| acc.max(v.abs()));
3645                for (x, y) in analytic.iter().zip(fd.iter()) {
3646                    assert!(
3647                        (x - y).abs() <= 5e-5 * scale,
3648                        "{name} jet of scale-candidate {level}: analytic {x:.6e} vs FD {y:.6e}"
3649                    );
3650                }
3651            }
3652        }
3653        // The function-space null candidate is independent of α and τ.
3654        for coord in 0..2 {
3655            assert!(
3656                derivs.penalties_first[coord][l_count]
3657                    .iter()
3658                    .all(|v| *v == 0.0),
3659                "null-component candidate must have zero (α, lnτ) drift"
3660            );
3661        }
3662        // Cross derivative through the provider, against a 4-point FD.
3663        let provider = derivs
3664            .penalties_cross_provider
3665            .as_ref()
3666            .expect("cross provider");
3667        let cross = provider.evaluate(0, 1).expect("cross pair (α, lnτ)");
3668        let pp = pen_at(a0 + h, t0 * h.exp());
3669        let pm = pen_at(a0 + h, t0 * (-h).exp());
3670        let mp = pen_at(a0 - h, t0 * h.exp());
3671        let mm = pen_at(a0 - h, t0 * (-h).exp());
3672        for level in 0..l_count {
3673            let fd = (&(&pp[level] - &pm[level]) - &(&mp[level] - &mm[level])) / (4.0 * h * h);
3674            let scale = fd.iter().fold(1e-30_f64, |acc, v| acc.max(v.abs()));
3675            for (x, y) in cross[level].iter().zip(fd.iter()) {
3676                assert!(
3677                    (x - y).abs() <= 5e-4 * scale,
3678                    "cross (α, lnτ) jet of scale-candidate {level}: analytic {x:.6e} vs FD {y:.6e}"
3679                );
3680            }
3681        }
3682    }
3683
3684    /// Design-moving ℓ dial (#1116): the producer's design jets and every
3685    /// normalized penalty candidate's jets must match central differences of the
3686    /// REBUILT objects under frozen geometry. Although the center-value forms
3687    /// `Q` and `H₀` are ℓ-invariant, their coefficient pullbacks `E(ℓ)ᵀQ E(ℓ)`
3688    /// and `E(ℓ)ᵀH₀E(ℓ)` are not.
3689    #[test]
3690    pub(crate) fn psi_producer_matches_fd_length_scale() {
3691        // Single-scale with opt-in ℓ learning; frozen geometry so only ℓ moves
3692        // across the FD trials.
3693        let (data, mut frozen) = frozen_spec_fixture(0.0, false);
3694        frozen.learn_length_scale = true;
3695        let derivs =
3696            build_measure_jet_basis_psi_derivatives(data.view(), &frozen).expect("psi derivatives");
3697        // ℓ is the only coordinate in single-scale + learn_length_scale.
3698        assert_eq!(
3699            derivs.design_first.len(),
3700            1,
3701            "single-scale + learn_length_scale enrolls exactly the ℓ coordinate"
3702        );
3703        assert_eq!(
3704            derivs.penalties_first[0].len(),
3705            2,
3706            "single-scale double penalty carries Primary + affine/null component"
3707        );
3708        // Rebuild design and normalized penalties at ℓ·e^{±h}; the explicit
3709        // positive length_scale is honored verbatim while the frozen transform
3710        // keeps the coefficient chart fixed.
3711        let ell0 = frozen.length_scale;
3712        let build_at = |ell: f64| {
3713            let trial = MeasureJetBasisSpec {
3714                length_scale: ell,
3715                ..frozen.clone()
3716            };
3717            build_measure_jet_basis(data.view(), &trial).expect("trial build")
3718        };
3719        let h: f64 = 1e-4;
3720        let plus = build_at(ell0 * h.exp());
3721        let minus = build_at(ell0 * (-h).exp());
3722        let at = build_at(ell0);
3723        assert_eq!(
3724            plus.active_penalties.len(),
3725            2,
3726            "fixture must keep both candidates active"
3727        );
3728        assert_eq!(
3729            minus.active_penalties.len(),
3730            2,
3731            "fixture must keep both candidates active"
3732        );
3733        assert_eq!(
3734            at.active_penalties.len(),
3735            2,
3736            "fixture must keep both candidates active"
3737        );
3738
3739        let x_plus = plus.design.to_dense();
3740        let x_minus = minus.design.to_dense();
3741        let x_0 = at.design.to_dense();
3742        let fd_first = (&x_plus - &x_minus) / (2.0 * h);
3743        let fd_second = (&x_plus - &(&x_0 * 2.0) + &x_minus) / (h * h);
3744        let scale1 = fd_first.iter().fold(1e-30_f64, |acc, v| acc.max(v.abs()));
3745        for (x, y) in derivs.design_first[0].iter().zip(fd_first.iter()) {
3746            assert!(
3747                (x - y).abs() <= 5e-5 * scale1,
3748                "∂X/∂lnℓ: analytic {x:.6e} vs FD {y:.6e}"
3749            );
3750        }
3751        let scale2 = fd_second.iter().fold(1e-30_f64, |acc, v| acc.max(v.abs()));
3752        for (x, y) in derivs.design_second_diag[0].iter().zip(fd_second.iter()) {
3753            assert!(
3754                (x - y).abs() <= 1e-3 * scale2,
3755                "∂²X/∂lnℓ²: analytic {x:.6e} vs FD {y:.6e}"
3756            );
3757        }
3758
3759        for candidate in 0..2 {
3760            let fd_penalty_first = (&plus.active_penalties[candidate].matrix
3761                - &minus.active_penalties[candidate].matrix)
3762                / (2.0 * h);
3763            let fd_penalty_second = (&plus.active_penalties[candidate].matrix
3764                - &(&at.active_penalties[candidate].matrix * 2.0)
3765                + &minus.active_penalties[candidate].matrix)
3766                / (h * h);
3767            // A central difference cannot resolve a derivative below its own
3768            // cancellation noise: differencing entries of size `E` at step `h`
3769            // leaves `~ε·E/h` in the first difference and `~ε·E/h²` in the
3770            // second, whatever the true derivative is. The null component's
3771            // shipped matrix (the rebuilt metric-consistent ridge) is EXACTLY
3772            // ℓ-invariant, so its analytic jets are exactly zero and its FD is
3773            // pure noise — measured at 3.5e-13 against a `1e-12` scale floor
3774            // that predates the exact answer. Grading that against a relative
3775            // tolerance alone asserts the ORACLE is exact, which it is not.
3776            // The factor 8 covers the handful of roundings between the two
3777            // rebuilds; it is not a fudge on the gradient, which is still
3778            // graded relatively wherever the FD resolves anything.
3779            let entry_scale = [&plus, &minus, &at]
3780                .iter()
3781                .flat_map(|built| built.active_penalties[candidate].matrix.iter())
3782                .fold(0.0_f64, |acc, value| acc.max(value.abs()));
3783            let first_floor = 8.0 * f64::EPSILON * entry_scale / h;
3784            let second_floor = 8.0 * f64::EPSILON * entry_scale / (h * h);
3785            let first_scale = fd_penalty_first
3786                .iter()
3787                .fold(1e-12_f64, |acc, value| acc.max(value.abs()));
3788            let second_scale = fd_penalty_second
3789                .iter()
3790                .fold(1e-10_f64, |acc, value| acc.max(value.abs()));
3791            for (analytic, finite_difference) in derivs.penalties_first[0][candidate]
3792                .iter()
3793                .zip(fd_penalty_first.iter())
3794            {
3795                assert!(
3796                    (analytic - finite_difference).abs() <= 1e-4 * first_scale + first_floor,
3797                    "candidate {candidate} ∂S~/∂lnℓ: analytic {analytic:.6e} vs FD \
3798                     {finite_difference:.6e} (rel budget {:.3e}, oracle floor {first_floor:.3e})",
3799                    1e-4 * first_scale
3800                );
3801            }
3802            for (analytic, finite_difference) in derivs.penalties_second_diag[0][candidate]
3803                .iter()
3804                .zip(fd_penalty_second.iter())
3805            {
3806                assert!(
3807                    (analytic - finite_difference).abs() <= 5e-3 * second_scale + second_floor,
3808                    "candidate {candidate} ∂²S~/∂lnℓ²: analytic {analytic:.6e} vs FD \
3809                     {finite_difference:.6e} (rel budget {:.3e}, oracle floor {second_floor:.3e})",
3810                    5e-3 * second_scale
3811                );
3812            }
3813        }
3814    }
3815
3816    /// Quadrature nodes must be the mass-weighted cell barycenters
3817    /// (first-moment-exact lumping), with empty cells keeping their seed
3818    /// coordinates at zero mass.
3819    #[test]
3820    pub(crate) fn quadrature_nodes_are_cell_barycenters() {
3821        // Two tight groups around (0,0) and (10,10); a third seed far away
3822        // captures nothing.
3823        let data = array![
3824            [0.0, 0.2],
3825            [0.4, -0.2],
3826            [0.2, 0.0],
3827            [9.8, 10.1],
3828            [10.2, 9.9],
3829        ];
3830        let seeds = array![[0.1, 0.1], [10.0, 10.0], [-50.0, -50.0]];
3831        let (nodes, masses) =
3832            measure_jet_quadrature_nodes(data.view(), seeds.view()).expect("quadrature nodes");
3833        assert!((masses.sum() - 1.0).abs() <= 1e-15, "masses must sum to 1");
3834        assert!((masses[0] - 0.6).abs() <= 1e-15);
3835        assert!((masses[1] - 0.4).abs() <= 1e-15);
3836        assert_eq!(masses[2], 0.0);
3837        // Cell 0 barycenter = (0.2, 0.0).
3838        assert_eq!(nodes[(0, 0)], 0.2);
3839        assert_eq!(nodes[(0, 1)], 0.0);
3840        // Cell 1 barycenter = (10.0, 10.0), which is not a sampled row.
3841        assert_eq!(nodes[(1, 0)], 10.0);
3842        assert_eq!(nodes[(1, 1)], 10.0);
3843        // Empty cell keeps its seed coordinates.
3844        assert_eq!(nodes[(2, 0)], -50.0);
3845        assert_eq!(nodes[(2, 1)], -50.0);
3846    }
3847
3848    /// Freeze→replay: rebuilding from the first build's frozen transform and
3849    /// frozen quadrature must reproduce design and penalty bit-for-bit (the
3850    /// predict-path contract).
3851    #[test]
3852    pub(crate) fn build_replay_roundtrip_reproduces_design_and_penalty() {
3853        // A bent filament with a side cluster; multiscale opt-in so this
3854        // exercises the per-scale (spectral) replay path (#1116).
3855        let n = 140usize;
3856        let data = Array2::<f64>::from_shape_fn((n, 2), |(i, k)| {
3857            let t = i as f64 / (n as f64 - 1.0);
3858            if k == 0 {
3859                t * 3.0
3860            } else {
3861                0.5 * (t * 3.0).cos() + if i % 9 == 0 { 0.8 } else { 0.0 }
3862            }
3863        });
3864        let spec = MeasureJetBasisSpec {
3865            center_strategy: CenterStrategy::FarthestPoint { num_centers: 70 },
3866            multiscale: true,
3867            ..MeasureJetBasisSpec::default()
3868        };
3869        let first = build_measure_jet_basis(data.view(), &spec).expect("first build");
3870        let BasisMetadata::MeasureJet {
3871            centers,
3872            length_scale,
3873            eps_band,
3874            order_s,
3875            alpha,
3876            tau0,
3877            masses,
3878            support_means,
3879            penalty_normalization_scales,
3880            raw_penalty_normalization_scales,
3881            fused_penalty_normalization_scale,
3882            constraint_transform,
3883            ..
3884        } = &first.metadata
3885        else {
3886            panic!("measure-jet build must return MeasureJet metadata");
3887        };
3888        let replay_spec = MeasureJetBasisSpec {
3889            center_strategy: CenterStrategy::UserProvided(centers.clone()),
3890            order_s: *order_s,
3891            alpha: *alpha,
3892            tau0: *tau0,
3893            num_scales: eps_band.len(),
3894            // MeasureJet freezes its range STANDARDIZED and replays it
3895            // verbatim; the tag is what records that it is the odd family out.
3896            length_scale: length_scale.standardized_value(),
3897            double_penalty: spec.double_penalty,
3898            learn_length_scale: spec.learn_length_scale,
3899            multiscale: spec.multiscale,
3900            identifiability: MeasureJetIdentifiability::FrozenTransform {
3901                transform: constraint_transform.clone().expect("fit-time z"),
3902            },
3903            frozen_quadrature: Some(MeasureJetFrozenQuadrature {
3904                masses: masses.clone(),
3905                eps_band: eps_band.clone(),
3906                support_means: support_means.clone(),
3907                penalty_normalization_scales: penalty_normalization_scales.clone(),
3908                raw_penalty_normalization_scales: raw_penalty_normalization_scales.clone(),
3909                fused_penalty_normalization_scale: *fused_penalty_normalization_scale,
3910                sigma_coord: None,
3911            }),
3912        };
3913        // Per-level mode: one candidate per band scale plus the function-space
3914        // null component, and the count must survive replay bit-for-bit.
3915        assert_eq!(
3916            first.active_penalties.len(),
3917            eps_band.len() + 1,
3918            "per-level mode must emit one candidate per scale + null component"
3919        );
3920        let second = build_measure_jet_basis(data.view(), &replay_spec).expect("replay build");
3921        let x1 = first.design.to_dense();
3922        let x2 = second.design.to_dense();
3923        assert_eq!(x1.shape(), x2.shape());
3924        for (a, b) in x1.iter().zip(x2.iter()) {
3925            assert!((a - b).abs() <= 1e-12, "design replay drift: {a} vs {b}");
3926        }
3927        assert_eq!(first.active_penalties.len(), second.active_penalties.len());
3928        for (p1, p2) in first
3929            .active_penalties
3930            .iter()
3931            .zip(second.active_penalties.iter())
3932        {
3933            for (a, b) in p1.matrix.iter().zip(p2.matrix.iter()) {
3934                assert!((a - b).abs() <= 1e-12, "penalty replay drift: {a} vs {b}");
3935            }
3936        }
3937    }
3938}