Skip to main content

gam_solve/
residual_cascade.rs

1//! Multiresolution residual cascade for scattered 2-3D smooths at huge n
2//! (compute-first primitive #3, #1032; siblings: the 1-D scan in
3//! [`crate::spline_scan`], the 2-D grid in
4//! [`gam_terms::grid_spline_2d`]).
5//!
6//! Model. In metric-scaled coordinates `z = diag(metric)·x` the smooth is
7//!   `f(z) = P(z)'γ + Σ_l Σ_j c_{l,j} · φ((z − ξ_{l,j})/δ_l)`,
8//! an unpenalized linear polynomial layer `P = {1, z_1, …, z_d}` at the root
9//! plus, per level `l = 0..L`, compactly supported Wendland bumps
10//! `φ(r) = (1−r)₊⁴(4r+1)` (positive definite and C² on ℝ³) of support radius
11//! `δ_l = OVERLAP·h_l` planted on the NEW centers of a nested net with
12//! covering radius `h_l = h₀·2^{−l}`. Coefficients are a-priori independent,
13//! `c_{l,j} ~ N(0, τ²·4^{−l(s−d/2)})` — the standard multilevel frame whose
14//! diagonal prior norm is equivalent to the Sobolev-`s` (semi)norm on
15//! quasi-uniform nested nets (Narcowich–Ward inverse estimates + Le Gia–
16//! Wendland multilevel stability; `d/2 < s ≤ (d+3)/2`, the native smoothness
17//! of the Wendland-(3,1) bump). The assembled claim is certified in-test
18//! against a dense kernel solve on small n (#904 style), not assumed.
19//!
20//! Nets. Each level's center set is a greedy hash-grid ε-net scanned in data
21//! order, seeded with the previous level's net: covering radius ≤ h_l over
22//! the data AND separation ≥ h_l — the same quasi-uniformity guarantees
23//! farthest-point sampling gives, at O(n) per level (each point checks the
24//! 3^d neighboring cells of one hash grid of cell size h_l). Nets are nested
25//! (`Ξ_0 ⊂ Ξ_1 ⊂ …`); a center carries a bump only at its birth level.
26//!
27//! Fit. With `W = diag(w)`, `D = diag(0 on the polynomial layer, d_l =
28//! 4^{l(s−d/2)} on level-l bumps)` and `λ = σ²/τ²`, the posterior mode solves
29//! `(X'WX + λD)c = X'Wy`. `X` is sparse — a row touches the O(1) bumps per
30//! level whose supports cover it, O(qL) nonzeros — and is held in CSR. For
31//! moderate column counts (`m ≤ DENSE_GRAM_MAX`) the normal equations are
32//! solved by dense Cholesky with the EXACT log-determinant (same route as the
33//! grid sibling); beyond that the solve is preconditioned CG with the two-level
34//! additive-Schwarz coarse-space preconditioner `P = blockdiag(A_CC,
35//! diag(A_FF))`. The multilevel Wendland frame is redundant across scales — a
36//! coarse bump and the fine bumps in its support are strongly correlated — so
37//! the data-fit Gram `X'WX` couples levels and a pure-diagonal preconditioner
38//! leaves a conditioning that GROWS with the number of data-identified levels
39//! (hence with n). The coarse space `C` (polynomial layer + the data-dominated
40//! coarsest levels, see `coarse_space_cols`) is solved EXACTLY by a small dense
41//! Cholesky and the penalty-dominated fine tail `F` — where `A_ll ≈ λ d_l I` is
42//! already uniformly conditioned — by its Jacobi diagonal. That deflation is
43//! what makes `P^{−1/2}(X'WX+λD)P^{−1/2}` uniformly conditioned, so the CG
44//! iteration count is genuinely n-independent (the in-test gate asserts an
45//! ADDITIVE bound across a 4× n jump, not a multiplicative one). Every CG solve
46//! reports its relative residual `‖b − Ac‖/‖b‖`: a computable backward-error
47//! certificate (`c` solves a system perturbed by no more than that fraction)
48//! inherited by every linear functional of the solution.
49//!
50//! REML. λ maximizes the profiled-σ² restricted criterion
51//!   `ℓ_R(λ) = −½[ log|X'WX+λD| − log|λD|₊ + (n−d−1)·log σ̂²(λ) ] + const`,
52//! `log|λD|₊ = r·logλ + Σ_j log d_j` over the `r` penalized columns and
53//! `σ̂² = (y'Wy − c'X'Wy)/(n−d−1)` — the same shape as the siblings, with the
54//! penalty-logdet constant kept so criteria are comparable across cascade
55//! depths. Eliminating the polynomial null block once gives the
56//! penalty-whitened Schur complement `B`, for which the normalized determinant
57//! is `log|G₀₀| + log|I+B/λ|`. Its spectrum is exact on the dense route and
58//! represented by one fixed-probe Lanczos quadrature on the iterative route.
59//! Thus the score, gradient, and curvature are analytic functions of log λ
60//! with the SAME spectral nodes at every trial. The dense route has a rigorous
61//! interval extension that isolates every stationary interval before
62//! safeguarded root refinement.
63//!
64//! Which designs get that proof is set by `CERTIFIED_SPECTRUM_MAX`, NOT by the
65//! dense Gram cache. The exact spectrum is a symmetric reduction of the
66//! `rank × rank` Schur complement, so its bound is that reduction's transient
67//! memory; the Gram cache's bound (`DENSE_GRAM_MAX`) is a much tighter LIFETIME
68//! budget for a per-design array. Past the cache the Schur complement is
69//! accumulated straight from the CSR rows into ONE packed upper triangle for the
70//! duration of the reduction and dropped, so certification continues well past
71//! it — which is what lets a cascade whose refinement crosses 1536 columns
72//! finish at all (#2546). What the criterion consumes is `Θ` and `Vᵀβ`, never
73//! the eigenbasis, so the triangle is reduced in place and `β` rides along
74//! through the reflectors and rotations (`gam_linalg::packed_symmetric_spectrum`,
75//! #2758); the transient is one `m²/2` array rather than the seven-plus `m²`
76//! blocks a general eigendecomposition holds, and the derived width moves with
77//! it. Past the SPECTRUM budget the route deliberately refuses automatic
78//! λ selection: a fixed-probe SLQ log-determinant has no exact-real outer
79//! enclosure, and neither does an exact factorization, which returns a number at
80//! one λ and no enclosure over a λ cell. Closing the separate β-seeded
81//! residual Krylov space can make the residual exact, but cannot enclose that
82//! determinant; a merely converged residual tail is numerical point evidence
83//! only. Fixed-λ fits remain available on the iterative route.
84//!
85//! The same elimination puts the profiled RESIDUAL in the same form, making the
86//! diagnostic criterion solve-free at every λ whenever its residual rule is
87//! admitted. With
88//! `β = D^{−1/2}(b₁ − G₁₀G₀₀^{−1}b₀)` and `S_k(λ) = β'(B+λI)^{−k}β`, the residual
89//! is `R = anchor − S₁` and its three `log λ` derivatives are built from
90//! `S₂, S₃, S₄`; `anchor = y'Wy − b₀'G₀₀^{−1}b₀` is the part no λ can move. On
91//! the dense route the eigenbasis projects β directly. Past the cap, `S_k(λ)` is
92//! `∫(θ+λ)^{−k} dμ(θ)` for the measure β induces on `spec(B)`, so ONE Lanczos run
93//! seeded with β (rather than with a Rademacher probe) returns the Jacobi matrix
94//! of its Golub–Meurant Gauss rule — the same `(node, weight)` shape the dense
95//! route stores, so both routes then evaluate one expression. That rule is
96//! admitted only when it has earned point-evaluation trust: either the Krylov
97//! space has consumed `rank(B) ≤ n − nullity` and is therefore invariant (the
98//! rule is then exact for every kernel), or the gaps over the nested
99//! `m/4, m/2, m` rules contract enough that their geometric tail estimate is at
100//! most `√ε` over the whole domain. A failed admission is an error; the
101//! ill-conditioned two-PCG-solves-per-λ fallback is not revived (#2503).
102//! Neither residual admission encloses the independent fixed-probe SLQ
103//! determinant, so neither authorizes automatic iterative-route REML (#2513).
104//!
105//! Refinement certificate. After fitting L levels, the candidate level L+1 is
106//! constructed (O(n)) and what it would buy is compared against what it would
107//! cost — both exactly, and both in the currency λ was already selected in.
108//! For the penalized objective `F(c) = ‖√W(y−Xc)‖² + λc'Dc`, appending columns
109//! `X₂` with penalty `λd_{L+1}I` decreases the minimum by `gain = g'S⁻¹g`,
110//! `g = X₂'W r̂`, `S = X₂'W(I−H)X₂ + λd_{L+1}I` the Schur complement, and
111//! multiplies the restricted likelihood's Occam factor by `exp(−occam/2)` with
112//! `occam = log det(S/(λd_{L+1}))` — the log-determinant of the SAME operator
113//! the gain is a quadratic form in. At the profiled σ̂² the restricted
114//! log-likelihood therefore moves by
115//!
116//! ```text
117//!     2·Δ = dof·log(rss_pen/(rss_pen − gain)) − occam
118//! ```
119//!
120//! so one more level is warranted exactly when `gain > rss_pen·(1 −
121//! e^{−occam/dof})`. That break-even gain is the tolerance: the objective
122//! decrease the level's own DIMENSION already pays for. Nothing in it is
123//! chosen — a fixed fraction of `rss_pen` charges nothing for the width of the
124//! set it is buying, which is why it demanded a level of 32790 candidate
125//! columns against 5997 identifiable directions (#2759).
126//!
127//! The cascade refines (adds the level, refits, re-selects λ) until the
128//! evidence stops improving, the net stops producing new centers (every point
129//! is a center), or the next level reaches a structural boundary: data
130//! identifiability, certified-spectrum memory, level count, or center count. A
131//! boundary reached while the evidence still improves is `Underresolved` with
132//! the retained checkpoint and that evidence; it is never sent downstream to a
133//! rank-flat score search and never converted into a fit.
134//!
135//! Both numbers come from ONE evaluation of the design with the complete
136//! candidate level appended, at the incumbent's λ — available past every
137//! capacity budget, because a single fixed-λ evaluation needs no certified
138//! spectrum and no identifiable rank. The matrix-free two-sided bracket on
139//! `gain` (below) is kept as the SCREEN that skips that evaluation: Hadamard on
140//! `S ⪯ diag(X₂'WX₂) + λd` bounds `occam` from above for free, so a gain
141//! bracket whose LOWER end already clears the break-even gain of that bound
142//! proves the level warranted without building anything.
143//!
144//! A capacity boundary is a boundary on WIDTH, and the level it stops is not
145//! all-or-nothing. When the complete candidate level would carry more penalized
146//! modes than the sample identifies or the certified spectrum can enclose, the
147//! cascade adds as many of its candidates as the budget allows — the largest
148//! `|g_j|` first, the same terms the bound is a sum of — and leaves the rest for
149//! the bound to certify. Refusing the whole level because the PROPOSAL was wide,
150//! while the gain it carried was concentrated far inside the budget, is #2700.
151//! The tolerance is still compared against the bound over the COMPLETE candidate
152//! set, and a truncated level's leftovers are re-assessed at their own radius
153//! before any fit is minted, so a partial level buys width, never a weaker
154//! certificate.
155//!
156//! Posterior. Coefficient covariance is `σ²(X'WX+λD)^{−1}`; pointwise
157//! prediction variance routes the basis row through one (certified) solve.
158//! Exact posterior samples come from perturb-and-solve: `c_s = A^{−1}(X'Wy +
159//! σ(X'W^{1/2}z₁ + √λ D^{1/2}z₂))` with iid standard-normal `z₁, z₂` has
160//! mean `ĉ` and covariance exactly `σ²A^{−1}` (deterministically seeded; one
161//! certified solve per sample).
162//!
163//! Payoff. Build O(n·(L + 3^d)), fit O(nnz · iters) per λ trial with
164//! n-independent iters — O(n log n) end to end, against the dense n×k kernel
165//! Gram + O(k³) per trial that duchon/matern pay today. Gap behavior is
166//! mechanical: levels wider than a gap keep support across it (polynomial +
167//! coarse bumps bridge), finer levels have no data and revert to their prior
168//! variance, so the posterior mean bridges instead of sagging while the
169//! variance grows into the gap.
170
171use std::collections::HashMap;
172use std::sync::Arc;
173
174use faer::sparse::{SparseColMat, SymbolicSparseColMat};
175use gam_linalg::packed_symmetric_spectrum::{
176    packed_symmetric_spectrum_with_probe, packed_upper_len, packed_upper_row_offset,
177};
178use gam_math::score_opt::{
179    AffineRemlProfile, ScoreJet, certified_ln_positive,
180};
181use gam_linalg::sparse_exact::{
182    SparseExactFactor, factorize_sparse_spd_strict, logdet_from_factor, solve_sparse_spd,
183    sparse_spd_factor_nnz,
184};
185use gam_terms::grid_spline_2d::{chol_solve, cholesky_logdet};
186use ndarray::Array1;
187
188/// Bump support radius as a multiple of the level's covering radius:
189/// `δ_l = OVERLAP·h_l`. Separation ≥ h_l caps the bumps covering a point at
190/// a packing constant per level (O(q) row nonzeros per level).
191const OVERLAP: f64 = 2.0;
192/// Root covering radius as a fraction of the largest scaled axis range.
193const H0_FRACTION: f64 = 0.5;
194/// Levels in the initial cascade before refinement certificates run.
195const INITIAL_LEVELS: usize = 3;
196/// Hard cap on cascade depth (h shrinks 2^16-fold below the root).
197const MAX_LEVELS: usize = 16;
198/// Hard cap on total centers across all levels.
199const MAX_CENTERS: usize = 200_000;
200
201/// Column count up to which the normal equations go through dense Cholesky
202/// (exact logdet, no iteration); above it, PCG + SLQ. 1536² doubles ≈ 18 MB.
203///
204/// This sizes a PERSISTENT per-design cache: `Core::dense_gram` is held for the
205/// whole life of the design and reused by every solve and every λ. It does NOT
206/// bound the certified REML proof — see [`CERTIFIED_SPECTRUM_MAX`], which is a
207/// transient budget and reaches further.
208const DENSE_GRAM_MAX: usize = 1536;
209
210/// Column count up to which the CERTIFIED automatic-REML proof is available.
211///
212/// The proof is not a log-determinant, and that is the whole reason it needs its
213/// own bound. It is the λ-INDEPENDENT Schur spectrum built by
214/// [`Core::dense_cascade_spectrum`]: with `B = D^{−1/2}(G₁₁ − G₁₀G₀₀^{−1}G₀₁)
215/// D^{−1/2} = VΘV'`, every determinant mode and every residual moment is an
216/// analytic kernel of `θ_i + λ`, which is what lets
217/// [`AffineRemlProfile::enclose`] produce genuine INTERVAL extensions of the
218/// score value and its first two derivatives — the objects the KKT root and the
219/// global candidate ordering are certified in.
220///
221/// An exact factorization of `X'WX + λD` does not substitute for that, however
222/// exact it is. A factorization — dense or sparse-direct — is a POINTWISE
223/// object: it returns a number at one λ and supports no enclosure over a λ cell,
224/// so it can certify neither a score sign on an interval nor a stationary point.
225/// (The former endpoint-jet/global-Lipschitz enclosure that tried to bridge that
226/// gap did not collapse in saturated tails and was removed; see
227/// [`CascadeRemlProfile::affine_view`].) The requirement is therefore ALL
228/// eigenvalues of the `rank × rank` whitened Schur complement, together with the
229/// whitened response's coordinates in its eigenbasis — and NOT the eigenbasis
230/// itself, which is where this budget's history went wrong (#2758).
231///
232/// So the bound is that decomposition's LIVE MEMORY, and the width is DERIVED
233/// from it: `sqrt(CERTIFIED_SPECTRUM_BYTES / bytes-per-m²)`, over the
234/// [`CERTIFIED_SPECTRUM_BYTES_PER_COLUMN_SQUARED`] the route was measured to
235/// hold, all freed as soon as the modes are extracted. Being a transient rather
236/// than a lifetime cache is why this budget reaches so much further in columns
237/// than [`DENSE_GRAM_MAX`]: the Schur complement is assembled from the CSR
238/// design for the duration of the decomposition and dropped, instead of being
239/// kept for the fit.
240///
241/// Time is not the binding resource here and is not what the number is derived
242/// from: the decomposition is `O(rank³)` and is paid ONCE per cascade depth
243/// (`fit_reml` builds the profile once and the certified search then evaluates
244/// mode sums, `O(modes)` per trial, with no linear algebra at all).
245///
246/// That claim is now stated with a measurement behind it, because a wider cap
247/// admits wider designs and "not binding" should not be taken on faith. On four
248/// cores a `rank = 6795` profile builds in **46.2 s** end to end — assembly,
249/// reduction and sweep — which is `4·rank³/3 = 4.2e11` flops at 9.1 GFLOP/s
250/// through the packed symmetric matrix-vector product and rank-2 update, both
251/// Rayon-parallel over rows. `rank = 1922` takes 1.16 s. Extrapolating the cubic
252/// to the cap gives ~2.7 minutes at `rank = 10362`, so the widest admissible
253/// design costs single-digit minutes of one-off certification, not hours.
254const CERTIFIED_SPECTRUM_MAX: usize =
255    (CERTIFIED_SPECTRUM_BYTES / CERTIFIED_SPECTRUM_BYTES_PER_COLUMN_SQUARED).isqrt();
256
257/// Live memory the certified spectral proof may hold at its peak. The largest
258/// transient this crate asks of a workstation; [`CERTIFIED_SPECTRUM_MAX`] is
259/// derived from it rather than chosen, so moving the budget moves the width and
260/// the two cannot drift apart.
261const CERTIFIED_SPECTRUM_BYTES: usize = 512 * 1024 * 1024;
262
263/// Bytes the certified route holds at its peak, per `m²` of design width.
264///
265/// This used to be a count of whole `m × m` `f64` blocks, and it was **8** —
266/// measured at 6.41-6.84 and rounded up, because the inventory the file could
267/// see (the upper Gram, the Schur complement, the eigenvector matrix) was not
268/// what ran: `eigh` is `faer`'s self-adjoint EVD and its tridiagonalization
269/// allocates workspace this crate never named. Eight blocks is `64` bytes per
270/// `m²` and a `1/√8` factor on the admissible width — 2896 columns, against a
271/// 6000-row fixture that identifies 5997 penalized directions (#2758).
272///
273/// Every one of those blocks was carrying something the criterion does not
274/// consume. It consumes `Θ` and `Vᵀβ`; the eigenbasis is read at exactly one
275/// site, to form that projection. So the route now holds:
276///
277/// ```text
278///   packed upper Schur triangle   rank(rank+1)/2 · 8 B   ->  4 B per m²
279///   cross block G01               q · rank · 8 B, q ≤ 4  ->  O(m)
280///   tridiagonal + working vectors O(rank)                ->  O(m)
281/// ```
282///
283/// and nothing else: the `m × m` Gram is not assembled at all (the two blocks
284/// the Schur complement needs are accumulated straight from the CSR rows), the
285/// triangle is reduced IN PLACE, and `Vᵀβ` rides along one vector at a time
286/// through [`gam_linalg::packed_symmetric_spectrum`]. The declared number is
287/// therefore an INVENTORY again — one packed `f64` triangle, `8/2 = 4` bytes
288/// per `m²` — with the next integer of headroom for the allocator's own
289/// rounding.
290///
291/// `zz_measure_certified_spectrum_peak_memory_2546` re-measures it and fails if
292/// it is ever exceeded, because a figure below the realized one would let
293/// [`CERTIFIED_SPECTRUM_MAX`] admit a width that overruns
294/// [`CERTIFIED_SPECTRUM_BYTES`].
295const CERTIFIED_SPECTRUM_BYTES_PER_COLUMN_SQUARED: usize = 5;
296
297/// Memory budget for the exact sparse-direct factor of `A = X'WX + λD`, stated
298/// as nonzeros of `L`.
299///
300/// Past [`DENSE_GRAM_MAX`] the design is still SPARSE — a row touches the `O(1)`
301/// bumps per level whose supports cover it, `O(qL)` nonzeros — so `A` is sparse
302/// and has an exact sparse Cholesky. Nothing about a fixed-λ log-determinant
303/// requires iteration or a stochastic estimate; what it requires is that the
304/// FILL-IN fit in memory, and `nnz(A)` does not predict `nnz(L)`. So the
305/// realized fill of the AMD ordering is measured by a symbolic pass before any
306/// numeric work is committed, and compared against this budget.
307///
308/// The number is that budget divided by the factor's own per-entry cost: the
309/// simplicial factor stores one `f64` value and one `usize` row index per
310/// nonzero, 16 bytes, and 256 MiB of factor is the largest this route will pay,
311/// so `256·2^20 / 16 = 16·2^20` nonzeros. Beyond it no exact factorization is
312/// available at all and the log-determinant falls back to the stochastic
313/// estimate — reported as [`LogdetMethod::Slq`], and never underwriting a proof,
314/// since [`ResidualCascadeDesign::fit_reml`] refuses far below this width.
315const SPARSE_FACTOR_MAX_NNZ: usize = 16 * 1024 * 1024;
316
317/// PCG convergence: relative residual ‖b − Ac‖/‖b‖ (the backward-error
318/// certificate) demanded of every solve, and the iteration cap past which
319/// the solve is an error rather than a silent approximation. The certification
320/// suite gates the iterative route at 1e-9; asking for more burns matvecs
321/// without strengthening any downstream certificate.
322const CG_RTOL: f64 = 1e-9;
323const CG_MAX_ITERS: usize = 4000;
324
325/// Coarse-space additive-Schwarz preconditioner controls (issue #1032: the
326/// "BPX/level-diagonal preconditioned CG, n-independent iters" spec).
327///
328/// The multilevel Wendland frame is redundant across scales — a coarse bump and
329/// the fine bumps inside its support are strongly correlated — so the data-fit
330/// Gram `X'WX` couples levels and a pure-diagonal (Jacobi) preconditioner leaves
331/// a conditioning that grows with the number of *data-identified* levels, hence
332/// with `n` (more rows ⇒ finer levels carry data ⇒ another collinear coarse
333/// scale the diagonal can't decouple). The cure is the textbook two-level
334/// additive Schwarz coarse space: solve the coarse block — the polynomial layer
335/// plus every level the penalty has NOT yet made diagonally dominant — EXACTLY,
336/// and precondition the remaining penalty-dominated fine levels (where
337/// `A_ll ≈ λ d_l I` is already uniformly conditioned) by their Jacobi diagonal.
338///
339/// A level is "data-dominated" while `λ d_l < COARSE_DOMINANCE · median diag
340/// (X'WX) over the level`. Because columns are laid out poly, level-0, level-1,
341/// … and `d_l` increases while the per-level data weight decreases, the
342/// data-dominated levels are exactly the coarsest prefix `[0, ncoarse)`, so the
343/// coarse space is a contiguous column prefix and the cut is a single scan. The
344/// crossover level grows only as `½ log₄(n/λ)` — `ncoarse = O(√(n/λ))` columns —
345/// so the exact coarse factorization stays small against the sparse matvecs at
346/// every n the primitive serves. [`COARSE_SPACE_MAX`] caps it as a safety valve
347/// (past the cap the finer data-dominated levels fall back to Jacobi and the
348/// iteration count rises, but the CG residual certificate still guarantees the
349/// solve); [`MIN_COARSE_LEVELS`] always deflates the two coarsest scales, which
350/// are near-collinear with the polynomial layer at every λ.
351const COARSE_DOMINANCE: f64 = 4.0;
352/// Safety ceiling on the exact-coarse column count. It must NOT bind at the n
353/// the primitive serves: the n-independent iteration count rests on the coarse
354/// block containing the WHOLE data-dominated prefix (`O(√(n/λ))` columns), so a
355/// cap that truncates that prefix is exactly what makes the iteration count
356/// climb with n (a finer data-dominated level demoted to Jacobi cannot be
357/// decoupled from the coarse scales it is collinear with). At the n-scales the
358/// iterative route engages (tens of thousands of rows → a ≈1.4k-column
359/// prefix) this is non-binding headroom; it only triggers in the genuinely
360/// degenerate case the quasi-uniformity guard is meant to catch first. The
361/// realized coarse factorization runs at the actual prefix length, not the cap,
362/// so the ceiling costs nothing until it fires.
363const COARSE_SPACE_MAX: usize = 4096;
364const MIN_COARSE_LEVELS: usize = 2;
365
366/// Quasi-uniformity guard (issue #1032, caveat 2). The BPX n-independent CG
367/// iteration bound rests on the nested ε-nets being quasi-uniform *in the
368/// metric-scaled coordinates `z = diag(metric)·x` the bumps live in*. The
369/// greedy net guarantees covering ≤ h and separation ≥ h in `z` by
370/// construction, so the only way the BPX norm-equivalence constant blows up is
371/// when the metric is so anisotropic that the metric-scaled point cloud is
372/// effectively degenerate along a direction — the data collapses onto a lower
373/// dimension in `z`, the root covering radius `h₀ = ½·max_a range_a` swamps the
374/// collapsed axis, the level-`l` bumps overlap pathologically, and the
375/// preconditioner constant (hence the iteration count) grows without an
376/// n-independent bound. The realized symptom is `solve_iters` climbing toward
377/// [`CG_MAX_ITERS`]; this guard detects the *cause* up front from the
378/// metric-scaled per-axis spread so the selected route can refuse BEFORE
379/// paying an unbounded iterative solve, rather than discovering
380/// the blow-up only after `CG_MAX_ITERS` work.
381///
382/// Condition measure: the ratio of the largest to smallest metric-scaled
383/// per-axis standard deviation (a scale-free aspect ratio of the scaled
384/// cloud). Past this threshold the net is no longer quasi-uniform in every
385/// direction and the BPX bound is not trustworthy. Derived, not a knob: a
386/// `10³` aspect ratio means the collapsed axis carries <0.1% of the dominant
387/// axis's variation, at which point its bumps span the whole cloud and the
388/// multilevel hierarchy degenerates to a single ill-conditioned level.
389const QUASI_UNIFORMITY_MAX_ASPECT: f64 = 1.0e3;
390
391/// SLQ controls: fixed Rademacher probes (shared across λ trials) and the
392/// Lanczos depth per probe (full reorthogonalization; early exit on
393/// breakdown).
394const SLQ_PROBES: usize = 24;
395const SLQ_LANCZOS_STEPS: usize = 48;
396/// Live bytes the profiled-residual quadrature's full-reorthogonalization basis
397/// may occupy past the dense cap (see [`Core::residual_quadrature_budget`]). The
398/// basis is `steps x rank` doubles, and it is the only quantity in that run that
399/// grows with both; the matvecs and the tridiagonal eigensolve are negligible
400/// beside it.
401///
402/// The number is chosen so the run can REACH its Krylov ceiling, because that is
403/// where the rule becomes exact and stopping short of it buys nothing at all: a
404/// budget of `0.9 * ceiling` pays 90% of the work and then refuses point evaluation.
405/// The binding requirement is therefore `ceiling * rank * 8` bytes, measured on the
406/// designs the #2503 integration fixtures actually build:
407///
408/// ```text
409///   n =  800, level 6   ceiling  797   rank  7387    47 MB
410///   n = 1200, level 8   ceiling 1197   rank 30879   296 MB
411///   n = 2500, level 7   ceiling 2497   rank 16565   331 MB
412///   n = 6000, level 7   ceiling 5997   rank  8432   404 MB
413/// ```
414///
415/// 512 MiB covers all of them and still bounds the run on the
416/// hundred-thousand-column designs `MAX_CENTERS` permits, where the ceiling is
417/// unreachable at any budget worth paying and the point route refuses rather
418/// than reviving the ill-conditioned solve.
419const RESIDUAL_QUADRATURE_BASIS_BYTES: usize = 512 << 20;
420
421/// Deterministic seed for the SLQ probes and posterior samples.
422const RNG_SEED: u64 = 0x1032_CA5C_ADE0_5EED;
423
424
425// ───────────────────────────── deterministic RNG ────────────────────────────
426
427/// SplitMix64: tiny, deterministic, full-period stream generator.
428struct SplitMix64(u64);
429
430impl SplitMix64 {
431    fn new(seed: u64) -> Self {
432        SplitMix64(seed)
433    }
434
435    fn next_u64(&mut self) -> u64 {
436        gam_linalg::utils::splitmix64(&mut self.0)
437    }
438
439    /// Rademacher ±1.
440    fn next_sign(&mut self) -> f64 {
441        if self.next_u64() & 1 == 0 { 1.0 } else { -1.0 }
442    }
443}
444
445// ─────────────────────────────── hash grids ─────────────────────────────────
446
447/// Integer cell of a point at a given cell width (coordinates are already
448/// metric-scaled and shifted to be ≥ 0, so indices are small and exact).
449#[inline]
450fn cell_of(z: &[f64; 3], dim: usize, width: f64) -> (i32, i32, i32) {
451    let mut c = [0_i32; 3];
452    for a in 0..dim {
453        c[a] = (z[a] / width).floor() as i32;
454    }
455    (c[0], c[1], c[2])
456}
457
458/// Hash grid over a point set: cell → indices. Lookup scans the 3^d
459/// neighborhood, which covers every point within one cell width.
460struct HashGrid {
461    width: f64,
462    dim: usize,
463    cells: HashMap<(i32, i32, i32), Vec<u32>>,
464}
465
466impl HashGrid {
467    fn new(width: f64, dim: usize) -> Self {
468        HashGrid {
469            width,
470            dim,
471            cells: HashMap::new(),
472        }
473    }
474
475    fn insert(&mut self, idx: u32, z: &[f64; 3]) {
476        let key = cell_of(z, self.dim, self.width);
477        self.cells.entry(key).or_default().push(idx);
478    }
479
480    /// Visit every stored index in the 3^d cells around `z` (deterministic
481    /// order: lexicographic cells, insertion order within a cell).
482    fn for_neighbors(&self, z: &[f64; 3], mut visit: impl FnMut(u32)) {
483        let (c0, c1, c2) = cell_of(z, self.dim, self.width);
484        let d2 = if self.dim > 2 { 1 } else { 0 };
485        let d1 = if self.dim > 1 { 1 } else { 0 };
486        for i0 in -1..=1_i32 {
487            for i1 in -d1..=d1 {
488                for i2 in -d2..=d2 {
489                    if let Some(bucket) = self.cells.get(&(c0 + i0, c1 + i1, c2 + i2)) {
490                        for &idx in bucket {
491                            visit(idx);
492                        }
493                    }
494                }
495            }
496        }
497    }
498}
499
500#[inline]
501fn dist2(a: &[f64; 3], b: &[f64; 3], dim: usize) -> f64 {
502    let mut s = 0.0;
503    for k in 0..dim {
504        let d = a[k] - b[k];
505        s += d * d;
506    }
507    s
508}
509
510/// Wendland-(3,1) bump `(1−r)₊⁴(4r+1)`: positive definite on ℝ^d, d ≤ 3,
511/// C², native space H^{(d+3)/2}.
512#[inline]
513fn wendland(r: f64) -> f64 {
514    if r >= 1.0 {
515        return 0.0;
516    }
517    let v = 1.0 - r;
518    let v2 = v * v;
519    v2 * v2 * (4.0 * r + 1.0)
520}
521
522// ───────────────────────────── design assembly ──────────────────────────────
523
524/// One resolution level: its NEW centers (scaled coordinates), covering
525/// radius, support radius, prior precision weight, and a lookup grid of cell
526/// width δ_l over those centers.
527struct Level {
528    h: f64,
529    delta: f64,
530    /// Prior precision weight `d_l = 4^{l(s−d/2)}` (prior variance τ²/d_l).
531    weight: f64,
532    centers: Vec<[f64; 3]>,
533    /// First flat column index of this level's coefficients.
534    col_offset: usize,
535    grid: HashGrid,
536}
537
538/// Immutable fitted-design core shared between the design handle and fits.
539struct Core {
540    dim: usize,
541    metric: [f64; 3],
542    /// Lower corner / range of the scaled bounding box (polynomial layer
543    /// coordinates are `2(z − lo)/range − 1` for conditioning).
544    z_lo: [f64; 3],
545    z_range: [f64; 3],
546    sobolev_s: f64,
547    levels: Vec<Level>,
548    /// Full nested net Ξ_L (scaled coords), retained so the candidate level
549    /// L+1 can extend it without re-deriving coarser levels.
550    net: Vec<[f64; 3]>,
551    /// Total columns: `dim + 1` polynomial + all level centers.
552    m: usize,
553    /// CSR design rows (column-sorted within a row).
554    row_ptr: Vec<usize>,
555    col_idx: Vec<u32>,
556    vals: Vec<f64>,
557    /// Inputs retained for matvecs, residuals, and refinement.
558    w: Vec<f64>,
559    y: Vec<f64>,
560    /// Scaled data coordinates (shifted to the box corner).
561    z: Vec<[f64; 3]>,
562    /// `X'Wy`, `y'Wy`, `diag(X'WX)`.
563    rhs: Vec<f64>,
564    ytwy: f64,
565    gram_diag: Vec<f64>,
566    /// Per-column prior precision weight (0 on the polynomial layer).
567    pen_diag: Vec<f64>,
568    /// `Σ_j log d_j` over penalized columns (the λ-free part of log|λD|₊,
569    /// kept so REML criteria compare across cascade depths).
570    pen_logdet_const: f64,
571    /// Dense upper-triangular `X'WX` when `m ≤ DENSE_GRAM_MAX` (row-major
572    /// m×m, lower mirror filled at solve time); None on the iterative route.
573    dense_gram: Option<Vec<f64>>,
574    /// Predict-only factored precision: the lower Cholesky factor `L` of
575    /// `A = X'WX + λD` at the FIT's λ, populated only on a core rebuilt from a
576    /// persisted [`ResidualCascadeState`] (where the training CSR is dropped).
577    /// When present, `solve_coeff` replays the posterior-variance solve through
578    /// this factor instead of the absent training design; `None` on a
579    /// training-built core, which solves through `dense_gram`/PCG as usual.
580    predict_chol: Option<Vec<f64>>,
581}
582
583/// Solver route a fit took for its log-determinant.
584#[derive(Clone, Copy, Debug, PartialEq, Eq)]
585pub enum LogdetMethod {
586    /// Exact dense linear algebra: either the dense Cholesky of `X'WX + λD` at
587    /// this λ, or the λ-independent Schur eigendecomposition the certified REML
588    /// profile is built from. Both are exact; which one ran depends on whether a
589    /// λ was fixed or selected.
590    DenseExact,
591    /// Exact sparse-direct Cholesky of `X'WX + λD` at this λ (AMD-ordered
592    /// simplicial LLᵀ); the log-determinant is `2·Σ log L_jj`. Available past
593    /// the dense Gram cache, where the design is still sparse.
594    SparseExact,
595    /// Diagonal control variate + stochastic Lanczos quadrature on fixed
596    /// deterministic probes. NOT exact — the only route that is not, and taken
597    /// only when the sparse factor's fill-in exceeds
598    /// `SPARSE_FACTOR_MAX_NNZ`.
599    Slq,
600}
601
602/// Computable certificates attached to a fit.
603#[derive(Clone, Copy, Debug)]
604pub struct CascadeCertificate {
605    /// Backward error of the coefficient solve: ‖b − Aĉ‖/‖b‖ (0 on the dense
606    /// route).
607    pub solve_rel_residual: f64,
608    /// CG iterations of the coefficient solve (0 on the dense route); the
609    /// n-independence gate watches this.
610    pub solve_iters: usize,
611    /// Route the log-determinant took.
612    pub logdet_method: LogdetMethod,
613}
614
615/// The exact nested-model comparison one candidate set was decided on.
616///
617/// Appending the complete candidate level `X₂` (penalty `λ·d`) to the design
618/// and re-minimizing at the SAME λ decreases the penalized objective by
619/// [`Self::gain`] and multiplies the restricted likelihood's Occam factor by
620/// `exp(−occam/2)`, with
621///
622/// ```text
623///     gain  = gᵀS⁻¹g,   g = X₂ᵀW r̂,   S = X₂ᵀW(I − H)X₂ + λd·I
624///     occam = log det(S/(λd))
625/// ```
626///
627/// the two spectral functionals of ONE operator. At the profiled σ̂² the
628/// restricted log-likelihood moves by
629///
630/// ```text
631///     2·evidence = dof·log(rss_pen/(rss_pen − gain)) − occam
632/// ```
633///
634/// which is positive exactly when `gain > tolerance`. A returned fit carries
635/// the candidate set that came CLOSEST to warranting one more level — the
636/// largest [`Self::evidence`] over the next level and, when a capacity budget
637/// forced the finest level to be partial, the candidates that level left behind
638/// at its own radius — and every one of them is at or below its own tolerance.
639///
640/// Two routes produce one: the design carrying the candidate set is built and
641/// solved, in which case every field is EXACT; or the matrix-free gain bracket
642/// and its Hadamard Occam bound already settle the comparison, in which case
643/// `gain` is a certified lower bound and `occam` a certified upper one — both
644/// read in the direction that understates [`Self::evidence`], so a positive
645/// evidence from that route is still a proof. The second route only ever
646/// settles the POSITIVE side: a fit is never minted on it.
647#[derive(Clone, Copy, Debug, PartialEq)]
648pub struct RefinementCertificate {
649    /// Penalized-objective decrease the complete candidate set buys at this
650    /// fit's λ: `rss_pen − rss_pen_refined`, differenced from a design that was
651    /// built and solved — or, on a refusal the screen settled, a certified
652    /// lower bound on it.
653    pub gain: f64,
654    /// `log det(I + X₂ᵀW(I − H)X₂/(λd)) ⩾ 0`, the candidate set's Occam factor,
655    /// or a certified upper bound on it. It is the charge for the set's
656    /// DIMENSION, weighted by how far each of its directions is identified by
657    /// the data: a candidate column with no rows in its support contributes
658    /// exactly zero to it, and to the gain.
659    pub occam: f64,
660    /// Break-even gain `rss_pen·(1 − e^{−occam/dof})` — the objective decrease
661    /// this candidate set's own Occam factor already pays for. DERIVED from the
662    /// set, never chosen: there is no tolerance constant in the cascade.
663    pub tolerance: f64,
664    /// Restricted log-likelihood change from appending the set, at this fit's
665    /// λ. Non-positive exactly when `gain ⩽ tolerance`, and non-positive on
666    /// every certificate a returned fit carries.
667    pub evidence: f64,
668}
669
670impl RefinementCertificate {
671    /// The certificate of a candidate set that does not exist: an empty net
672    /// certifies zero remaining gain against a zero charge.
673    const EXHAUSTED: Self = Self {
674        gain: 0.0,
675        occam: 0.0,
676        tolerance: 0.0,
677        evidence: 0.0,
678    };
679
680    /// Whether one more level is warranted — the marginal likelihood improves.
681    fn warrants_refinement(&self) -> bool {
682        self.evidence > 0.0
683    }
684}
685
686impl std::fmt::Display for RefinementCertificate {
687    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
688        write!(
689            f,
690            "gain {:.6e} against break-even {:.6e} (occam {:.6e}), restricted log-likelihood \
691             {:+.6e}",
692            self.gain, self.tolerance, self.occam, self.evidence
693        )
694    }
695}
696
697/// A structural limit that prevented the cascade from adding the next
698/// resolution level with certified automatic REML. These are never convergence
699/// certificates: if the requested gain tolerance has not passed, they produce
700/// [`ResidualCascadeError::Underresolved`] instead of a fit.
701#[derive(Clone, Copy, Debug, PartialEq, Eq)]
702pub enum RefinementObstruction {
703    /// The representation reached its supported maximum number of levels.
704    LevelCapacity {
705        levels: usize,
706        maximum_levels: usize,
707    },
708    /// Extending the nested net would exceed its supported center capacity.
709    CenterCapacity {
710        centers: usize,
711        maximum_centers: usize,
712    },
713    /// The complete next net would carry more penalized directions than the
714    /// training sample can identify after the polynomial null space is removed.
715    /// Crossing this boundary makes automatic REML flat by rank deficiency; it
716    /// is therefore reported at refinement, before score search.
717    IdentifiabilityCapacity {
718        candidate_columns: usize,
719        candidate_penalized_modes: usize,
720        identifiable_directions: usize,
721    },
722    /// The next net remains data-identified but its λ-independent Schur
723    /// eigenspectrum would exceed the certified automatic-REML memory budget.
724    CertifiedSpectrumCapacity {
725        candidate_columns: usize,
726        certified_spectrum_max: usize,
727    },
728}
729
730impl std::fmt::Display for RefinementObstruction {
731    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
732        match *self {
733            Self::LevelCapacity {
734                levels,
735                maximum_levels,
736            } => write!(
737                f,
738                "level capacity reached ({levels} of {maximum_levels} levels)"
739            ),
740            Self::CenterCapacity {
741                centers,
742                maximum_centers,
743            } => write!(
744                f,
745                "center capacity exceeded ({centers} centers for capacity {maximum_centers})"
746            ),
747            Self::IdentifiabilityCapacity {
748                candidate_columns,
749                candidate_penalized_modes,
750                identifiable_directions,
751            } => write!(
752                f,
753                "next-level identifiability exhausted ({candidate_penalized_modes} penalized \
754                 modes from {candidate_columns} columns against {identifiable_directions} \
755                 identifiable directions)"
756            ),
757            Self::CertifiedSpectrumCapacity {
758                candidate_columns,
759                certified_spectrum_max,
760            } => write!(
761                f,
762                "next-level certified-spectrum capacity exceeded ({candidate_columns} columns \
763                 for capacity {certified_spectrum_max})"
764            ),
765        }
766    }
767}
768
769/// Result of assessing the candidate level immediately finer than a fitted
770/// design. Empty-net exhaustion is distinct from representation capacity:
771/// only the former proves that the remaining gain is exactly zero.
772#[derive(Clone, Copy, Debug, PartialEq)]
773pub enum NextLevelAssessment {
774    /// The nested net produced no new centers, so the next-level gain is zero.
775    EmptyNet,
776    /// The complete candidate level was assessed and has this gain bound.
777    GainBound(f64),
778    /// A structural limit was reached. `gain_bound` is the computed bound when
779    /// the complete candidate could be assessed (identifiability, spectrum, or
780    /// level capacity), and positive infinity only when center capacity
781    /// prevented a complete assessment.
782    CapacityExceeded {
783        obstruction: RefinementObstruction,
784        gain_bound: f64,
785    },
786}
787
788/// Multiresolution residual-cascade design: nested nets, sparse design,
789/// diagonal multilevel prior — everything needed to evaluate the REML
790/// criterion and solve at any λ.
791pub struct ResidualCascadeDesign {
792    core: Arc<Core>,
793}
794
795/// Fitted cascade with factored-by-solve posterior access.
796pub struct ResidualCascadeFit {
797    core: Arc<Core>,
798    /// Number of original training rows. Restored fits retain this scalar even
799    /// though their prediction-only core intentionally drops the row arrays.
800    training_sample_size: std::num::NonZeroUsize,
801    /// Dense-route prediction factor at the fit's λ. When present, pointwise
802    /// variance uses this one Cholesky factor instead of refactoring the same
803    /// precision matrix for every prediction point.
804    predict_chol: Option<Vec<f64>>,
805    /// Exact sparse-direct factor of `A = X'WX + λD` at THIS fit's λ, held when
806    /// the design is past the dense Gram cache. The posterior variance is one
807    /// solve per prediction point; replaying it through this factor is exact and
808    /// `O(nnz(L))`, where the alternative is a fresh PCG per point whose
809    /// backward error the point then inherits.
810    predict_sparse: Option<Arc<SparseExactFactor>>,
811    /// Coefficients: `dim+1` polynomial entries, then level blocks.
812    pub coeff: Vec<f64>,
813    /// Selected (or supplied) log smoothing parameter `log λ = log σ²/τ²`.
814    log_lambda: f64,
815    /// Profiled (or supplied) observation variance σ².
816    pub sigma2: f64,
817    /// Restricted log-likelihood at the fit, up to λ- and data-independent
818    /// additive constants. Exact on every route whose log-determinant is exact
819    /// — dense Cholesky, the certified Schur spectrum, or the sparse direct
820    /// factor — and SLQ-estimated only when the sparse factor's fill-in
821    /// exceeded its budget, which the fit's `logdet_method` reports.
822    pub restricted_loglik: f64,
823    /// Penalized residual quadratic `y'Wy − c'X'Wy`.
824    pub rss_pen: f64,
825    /// Solve/logdet certificates.
826    pub certificate: CascadeCertificate,
827    /// Present when the fit came from the refinement loop.
828    pub refinement: Option<RefinementCertificate>,
829}
830
831/// Opaque work checkpoint carried by an underresolved cascade result.
832///
833/// The current finite-resolution iterate is deliberately private: callers can
834/// inspect its numerical evidence, but cannot turn an uncertified iterate into
835/// a [`ResidualCascadeFit`]. The retained design and coefficients allow a
836/// future refinement backend to resume the work without minting a partial fit.
837pub struct ResidualCascadeCheckpoint {
838    iterate: ResidualCascadeFit,
839}
840
841impl ResidualCascadeCheckpoint {
842    fn new(iterate: ResidualCascadeFit) -> Self {
843        Self { iterate }
844    }
845
846    /// Number of levels already fitted in this checkpoint.
847    pub fn num_levels(&self) -> usize {
848        self.iterate.num_levels()
849    }
850
851    /// Number of centers already fitted in this checkpoint.
852    pub fn num_centers(&self) -> usize {
853        self.iterate.num_centers()
854    }
855
856    /// REML-selected log smoothing parameter of the retained iterate.
857    pub fn log_lambda(&self) -> f64 {
858        self.iterate.log_lambda
859    }
860
861    /// Penalized residual used to scale the requested refinement tolerance.
862    pub fn rss_pen(&self) -> f64 {
863        self.iterate.rss_pen
864    }
865
866    /// Linear-solve evidence attached to the retained iterate.
867    pub fn certificate(&self) -> CascadeCertificate {
868        self.iterate.certificate
869    }
870}
871
872impl std::fmt::Debug for ResidualCascadeCheckpoint {
873    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
874        f.debug_struct("ResidualCascadeCheckpoint")
875            .field("num_levels", &self.num_levels())
876            .field("num_centers", &self.num_centers())
877            .field("log_lambda", &self.log_lambda())
878            .field("rss_pen", &self.rss_pen())
879            .field("certificate", &self.certificate())
880            .finish_non_exhaustive()
881    }
882}
883
884/// Typed failure of the magic-default cascade fit.
885#[derive(Debug)]
886pub enum ResidualCascadeError {
887    /// Invalid input or a numerical failure in design construction/optimization.
888    Computation(String),
889    /// Automatic smoothing-parameter selection needs mathematical outer
890    /// enclosures of the score value and derivatives over λ CELLS, which only
891    /// the λ-independent Schur spectrum provides. Past
892    /// `CERTIFIED_SPECTRUM_MAX` the dense eigendecomposition that spectrum is
893    /// made of exceeds its memory budget, and what remains is a fixed-probe
894    /// stochastic quadrature — a pointwise estimate, not an enclosure. Exact or
895    /// numerically converged residual evidence cannot repair that gap, and
896    /// neither can an exact factorization at a point.
897    RemlScoreProofUnavailable {
898        columns: usize,
899        certified_spectrum_max: usize,
900    },
901    /// Stationary structure could not be isolated even though the score was
902    /// certified flat at its representable value resolution.
903    RemlOptimumResolutionFlat {
904        lo: f64,
905        hi: f64,
906        max_score_gap: f64,
907        score_resolution: f64,
908    },
909    /// The certified 1-D score search could not decompose the λ domain within
910    /// the subdivision budget derived from that domain and the requested
911    /// resolution, so no λ was selected.
912    ///
913    /// Carries the identifiability of the design because that is the cause
914    /// whenever `rank > identifiable`: past the data's own rank the profiled
915    /// residual is an interpolation and the score is flat by rank deficiency
916    /// over whole stretches of λ, where there is no stationary point to isolate
917    /// and no derivative sign to exclude a cell by — so the search subdivides
918    /// every cell it reaches and its cost is exponential in the domain's
919    /// subdivision depth (#2546). `rank <= identifiable` means the budget was
920    /// hit for some other flat-criterion reason and the numbers say so.
921    RemlScoreSearchUndecomposable {
922        columns: usize,
923        rank: usize,
924        identifiable: usize,
925        subdivisions: usize,
926        budget: usize,
927        log_lambda_lo: f64,
928        log_lambda_hi: f64,
929    },
930    /// Rounded candidate ordering is wider than its certified comparison
931    /// resolution, so no unique representative may be fitted.
932    RemlValueOrderingUnresolved {
933        maximum_excess: f64,
934        comparison_resolution: f64,
935    },
936    /// A structural capacity was reached while one more level was still
937    /// warranted by the marginal likelihood. The checkpoint preserves all
938    /// completed work while remaining unusable as a public fit.
939    Underresolved {
940        checkpoint: ResidualCascadeCheckpoint,
941        /// The comparison that says the level is still warranted, computed on a
942        /// design that was built and solved. `None` when no EXACT comparison
943        /// against the candidate set exists — a structural cap stopped the set
944        /// from being formed, or the design carrying it is past the sparse
945        /// factor's fill budget and its log-determinant is a stochastic point
946        /// estimate. Either way the refusal rests on the cap alone, and an
947        /// absent comparison can never certify the discretization spent.
948        evidence: Option<RefinementCertificate>,
949        obstruction: RefinementObstruction,
950    },
951}
952
953impl From<String> for ResidualCascadeError {
954    fn from(reason: String) -> Self {
955        Self::Computation(reason)
956    }
957}
958
959impl std::fmt::Display for ResidualCascadeError {
960    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
961        match self {
962            Self::Computation(reason) => f.write_str(reason),
963            Self::RemlScoreProofUnavailable {
964                columns,
965                certified_spectrum_max,
966            } => write!(
967                f,
968                "residual cascade: automatic REML proof unavailable because the certified \
969                 Schur eigendecomposition does not fit its memory budget ({columns} columns \
970                 exceed {certified_spectrum_max}); without the lambda-independent spectrum the \
971                 score has only a fixed-probe stochastic quadrature, which is a pointwise \
972                 estimate and not an interval enclosure, so score signs, KKT roots, and global \
973                 candidate ordering are uncertified even when the separate residual quadrature \
974                 converges; use an explicitly fixed log lambda"
975            ),
976            Self::RemlOptimumResolutionFlat {
977                lo,
978                hi,
979                max_score_gap,
980                score_resolution,
981            } => write!(
982                f,
983                "residual cascade: REML optimum is value-resolved but not stationary on \
984                 [{lo}, {hi}] (maximum score gap {max_score_gap}, score resolution \
985                 {score_resolution})"
986            ),
987            Self::RemlScoreSearchUndecomposable {
988                columns,
989                rank,
990                identifiable,
991                subdivisions,
992                budget,
993                log_lambda_lo,
994                log_lambda_hi,
995            } => {
996                write!(
997                    f,
998                    "residual cascade: the certified REML score search spent {subdivisions} cell \
999                     subdivisions on log lambda in [{log_lambda_lo}, {log_lambda_hi}] without \
1000                     decomposing it, exceeding the budget {budget} derived from that domain and \
1001                     the requested resolution"
1002                )?;
1003                if rank > identifiable {
1004                    write!(
1005                        f,
1006                        "; the design is rank deficient against its data — {rank} penalized Schur \
1007                         modes ({columns} columns) against {identifiable} identifiable directions \
1008                         — so the profiled residual interpolates and the score is flat by rank \
1009                         deficiency, with no stationary point to isolate; refine less, or fix log \
1010                         lambda explicitly"
1011                    )
1012                } else {
1013                    write!(
1014                        f,
1015                        "; the design is identified ({rank} penalized Schur modes from {columns} \
1016                         columns against {identifiable} identifiable directions), so the flat \
1017                         criterion has some other cause and the budget is reporting it rather \
1018                         than diagnosing it"
1019                    )
1020                }
1021            }
1022            Self::RemlValueOrderingUnresolved {
1023                maximum_excess,
1024                comparison_resolution,
1025            } => write!(
1026                f,
1027                "residual cascade: selected REML representative can trail another exact \
1028                 candidate by {maximum_excess}, beyond comparison resolution \
1029                 {comparison_resolution}"
1030            ),
1031            Self::Underresolved {
1032                checkpoint,
1033                evidence,
1034                obstruction,
1035            } => match evidence {
1036                Some(evidence) => write!(
1037                    f,
1038                    "residual cascade underresolved after {} levels: one more level still earns \
1039                     marginal likelihood — {evidence} — so the cascade's own evidence, not a \
1040                     tolerance constant, is what this capacity refuses; {obstruction}",
1041                    checkpoint.num_levels(),
1042                ),
1043                None => write!(
1044                    f,
1045                    "residual cascade underresolved after {} levels: no exact comparison against \
1046                     the candidate set exists — it was never formed, or the design carrying it is \
1047                     past the exact log-determinant's budget — so nothing can certify the \
1048                     discretization spent; {obstruction}",
1049                    checkpoint.num_levels(),
1050                ),
1051            },
1052        }
1053    }
1054}
1055
1056impl std::error::Error for ResidualCascadeError {}
1057
1058/// One resolution level's geometry in a persisted snapshot: the data needed to
1059/// rebuild a `Level` (its lookup grid, bumps, and column block) without the
1060/// training rows. Centers are flattened `dim`-major (`dim` floats per center).
1061#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
1062pub struct LevelState {
1063    pub h: f64,
1064    pub delta: f64,
1065    pub weight: f64,
1066    pub col_offset: u64,
1067    /// `dim·n_centers` scaled-coordinate floats, center-major.
1068    pub centers: Vec<f64>,
1069}
1070
1071/// Serializable snapshot of a [`ResidualCascadeFit`] (#1032 persistence
1072/// prerequisite). Holds everything `predict` and sample-size-based reporting
1073/// need and no training row values:
1074/// - MEAN: the nested geometry (`dim`/`metric`/box/`sobolev_s` + per-level
1075///   centers/δ/weights/col-offsets) and the root polynomial layer are all that
1076///   `basis_row_scaled`·`coeff` reads;
1077/// - VARIANCE: the factored precision `predict_chol` — the lower Cholesky factor
1078///   `L` of `A = X'WX + λD` at the fit's λ — which the posterior-variance solve
1079///   `x'A⁻¹x` replays against (the training design that originally assembled `A`
1080///   is dropped).
1081///
1082/// `from_state` rebuilds a predict-capable fit whose `Core` carries empty
1083/// training CSR and `predict_chol = Some(L)`; `solve_coeff` then routes the
1084/// variance solve through `L`. The reconstructed fit cannot be re-fit or
1085/// resampled (it has no rows), only predicted from — exactly the persistence
1086/// contract.
1087#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
1088pub struct ResidualCascadeState {
1089    /// Original training row count. Required on the wire.
1090    pub training_sample_size: std::num::NonZeroU64,
1091    pub dim: u64,
1092    /// Per-axis metric scaling (length 3; trailing entries are 1 for `dim < 3`).
1093    pub metric: [f64; 3],
1094    pub z_lo: [f64; 3],
1095    pub z_range: [f64; 3],
1096    pub sobolev_s: f64,
1097    pub levels: Vec<LevelState>,
1098    /// Total column count `dim + 1 + Σ centers`.
1099    pub m: u64,
1100    /// `Σ_j log d_j` over penalized columns (kept so restored REML scalars stay
1101    /// comparable across cascade depths).
1102    pub pen_logdet_const: f64,
1103    /// Posterior-mode coefficients (length `m`).
1104    pub coeff: Vec<f64>,
1105    pub log_lambda: f64,
1106    pub sigma2: f64,
1107    pub restricted_loglik: f64,
1108    pub rss_pen: f64,
1109    /// Lower Cholesky factor `L` of `A = X'WX + λD` at the fit's λ, `m × m`
1110    /// row-major — the factored precision the variance solve replays through.
1111    pub predict_chol: Vec<f64>,
1112}
1113
1114/// Forward substitution `L y = b` (lower factor, row-major) into `out`.
1115fn forward_sub_into(l: &[f64], p: usize, b: &[f64], out: &mut [f64]) {
1116    for i in 0..p {
1117        let mut s = b[i];
1118        for t in 0..i {
1119            s -= l[i * p + t] * out[t];
1120        }
1121        out[i] = s / l[i * p + i];
1122    }
1123}
1124
1125/// Back substitution `Lᵀ z = y` (lower factor, row-major) into `out`.
1126fn back_sub_into(l: &[f64], p: usize, y: &[f64], out: &mut [f64]) {
1127    for i in (0..p).rev() {
1128        let mut s = y[i];
1129        for t in i + 1..p {
1130            s -= l[t * p + i] * out[t];
1131        }
1132        out[i] = s / l[i * p + i];
1133    }
1134}
1135
1136/// Coarse-space additive-Schwarz preconditioner for the iterative route
1137/// (issue #1032). `A = X'WX + λD` is preconditioned by the symmetric positive
1138/// definite block-diagonal `P = blockdiag(A_CC, diag(A_FF))`, where the coarse
1139/// index set `C = [0, ncoarse)` is the polynomial layer plus the data-dominated
1140/// (coarsest) levels and `F` the penalty-dominated fine tail — see the
1141/// [`COARSE_DOMINANCE`]/[`COARSE_SPACE_MAX`] docs for why this delivers
1142/// n-independent CG iteration counts where the pure-Jacobi diagonal does not.
1143///
1144/// `solve` applies `P⁻¹` (exact coarse Cholesky solve ⊕ fine Jacobi). For the
1145/// SLQ log-determinant the symmetric factor `R = blockdiag(L_CC, diag√A_FF)`
1146/// with `P = R Rᵀ` is exposed through `apply_r_inv`/`apply_r_inv_t`, and
1147/// `log|P| = log|A_CC| + Σ_F log A_jj`.
1148struct Preconditioner {
1149    /// First fine column; coarse block is the principal `[0, ncoarse)` submatrix.
1150    ncoarse: usize,
1151    /// Lower Cholesky factor of the coarse block `A_CC` (`ncoarse × ncoarse`).
1152    coarse_chol: Vec<f64>,
1153    /// `log|A_CC|` (exact).
1154    coarse_logdet: f64,
1155    /// `1/A_jj` on the fine columns `[ncoarse, m)`.
1156    inv_fine: Vec<f64>,
1157    /// `1/√A_jj` on the fine columns (the `R⁻¹`/`R⁻ᵀ` fine scaling).
1158    inv_sqrt_fine: Vec<f64>,
1159    /// `Σ_F log A_jj` (the fine part of `log|P|`).
1160    fine_logdet: f64,
1161}
1162
1163impl Preconditioner {
1164    /// `out = P⁻¹ r`: exact coarse solve on `[0, ncoarse)`, Jacobi on the tail.
1165    fn solve(&self, r: &[f64], out: &mut [f64]) {
1166        let nc = self.ncoarse;
1167        let zc = chol_solve(&self.coarse_chol, nc, &r[..nc]);
1168        out[..nc].copy_from_slice(&zc);
1169        for (k, o) in out[nc..].iter_mut().enumerate() {
1170            *o = r[nc + k] * self.inv_fine[k];
1171        }
1172    }
1173
1174    /// `out = R⁻ᵀ v` (coarse: `L_CCᵀ` back-solve; fine: `/√A_jj`).
1175    fn apply_r_inv_t(&self, v: &[f64], out: &mut [f64]) {
1176        let nc = self.ncoarse;
1177        back_sub_into(&self.coarse_chol, nc, &v[..nc], &mut out[..nc]);
1178        for (k, o) in out[nc..].iter_mut().enumerate() {
1179            *o = v[nc + k] * self.inv_sqrt_fine[k];
1180        }
1181    }
1182
1183    /// `out = R⁻¹ v` (coarse: `L_CC` forward-solve; fine: `/√A_jj`).
1184    fn apply_r_inv(&self, v: &[f64], out: &mut [f64]) {
1185        let nc = self.ncoarse;
1186        forward_sub_into(&self.coarse_chol, nc, &v[..nc], &mut out[..nc]);
1187        for (k, o) in out[nc..].iter_mut().enumerate() {
1188            *o = v[nc + k] * self.inv_sqrt_fine[k];
1189        }
1190    }
1191
1192    /// `log|P| = log|A_CC| + Σ_F log A_jj`.
1193    fn logdet(&self) -> f64 {
1194        self.coarse_logdet + self.fine_logdet
1195    }
1196}
1197
1198/// One positive-semidefinite eigenmode of the penalty-whitened Schur
1199/// complement. `weight == 1` on the dense exact route; on the large route it
1200/// is the fixed-probe Lanczos quadrature weight. The weights sum to the
1201/// penalized rank, so constants have the same null-recovery limit on both
1202/// routes.
1203#[derive(Clone, Copy)]
1204struct CascadeSpectralMode {
1205    eigenvalue: f64,
1206    weight: f64,
1207}
1208
1209/// One Lanczos run's Jacobi matrix on the penalty-whitened Schur complement
1210/// `B`, plus everything a Golub–Meurant quadrature rule needs from it.
1211///
1212/// See [`Core::schur_lanczos`]. `alpha` and `beta` are `T_m`'s diagonal and
1213/// off-diagonal, with `beta.len() == alpha.len() - 1` whenever `alpha` is
1214/// non-empty, which is the shape [`symmetric_tridiagonal_eigen`] reads.
1215struct SchurLanczos {
1216    /// `alpha_1..alpha_m` — the diagonal of `T_m`.
1217    alpha: Vec<f64>,
1218    /// `beta_1..beta_{m-1}` — the off-diagonal INSIDE `T_m`.
1219    beta: Vec<f64>,
1220    /// `||r_m||`, the `(m+1, m)` entry of the untruncated Jacobi matrix. It is
1221    /// the residual of the Krylov approximation, so `tail == 0` means
1222    /// `K_m(B, start)` is `B`-invariant and the Gauss rule is EXACT.
1223    tail: f64,
1224    /// `max_i |alpha_i|` — a Rayleigh-quotient lower bound on `||B||` over the
1225    /// Krylov space, and the scale the invariance certificate is stated in.
1226    spectral_scale: f64,
1227    /// `||start||^2`, the total mass of the quadrature measure: `Sum_j w_j`.
1228    start_norm_sq: f64,
1229    /// `tail` sits at the run's own roundoff floor, or the run consumed the full
1230    /// penalized rank. Either way `K_m(B, start)` is (numerically) invariant and
1231    /// the Gauss rule reproduces `start' g(B) start` for EVERY analytic `g` and
1232    /// every `lambda` — not approximately, but to the arithmetic's own floor.
1233    invariant: bool,
1234}
1235
1236/// Lambda-independent spectral representation of the profiled REML score.
1237///
1238/// Partition the normal matrix into the polynomial null space `0` and the
1239/// penalized cascade columns `1`. Eliminating the null block gives
1240///
1241/// `|G + lambda D| / |lambda D|_+ = |G00| |I + B/lambda|`,
1242///
1243/// with `B = D^(-1/2) (G11 - G10 G00^(-1) G01) D^(-1/2)`. Consequently every
1244/// determinant mode is an analytic logistic function of `log(lambda)`. The
1245/// representation is built once, rather than re-running a basin-selecting
1246/// lattice of lambda-dependent factorizations.
1247///
1248/// The same elimination puts the PROFILED RESIDUAL in the same form wherever
1249/// the eigenbasis survives the construction — see [`CascadeResidualForm`].
1250struct CascadeRemlProfile<'a> {
1251    core: &'a Core,
1252    null_logdet: f64,
1253    modes: Vec<CascadeSpectralMode>,
1254    residual: CascadeResidualForm,
1255}
1256
1257/// The lambda-independent spectral form of the PROFILED RESIDUAL
1258/// `R(lambda) = y'Wy - b'A(lambda)^(-1) b`.
1259///
1260/// The same null-space elimination and penalty whitening that turns the
1261/// determinant into a mode sum does the same thing to the residual. In the
1262/// Schur eigenbasis `B = V Theta V'`, `A(lambda)` acts as `Theta + lambda I`,
1263/// so with
1264///
1265/// `p = V' D^(-1/2) (b1 - G10 G00^(-1) b0)`   and
1266/// `S_k(lambda) = sum_i p_i^2 / (theta_i + lambda)^k`,
1267///
1268/// `R = anchor_energy - S1`, and the three quadratic forms the score jet needs
1269/// are the next three moments of that same sum:
1270///
1271/// `c'Dc = S2`, `(Dc)'A^(-1)(Dc) = S3`, `u'Du = S4` for `u = A^(-1) D c`.
1272///
1273/// `anchor_energy = y'Wy - b0' G00^(-1) b0` is the part of the residual no
1274/// lambda can move.
1275struct CascadeResidualSpectrum {
1276    /// `theta_i`, the Schur eigenvalue of mode `i` — the SAME numbers the
1277    /// determinant modes carry.
1278    eigenvalue: Vec<f64>,
1279    /// Every mode's penalty scale, which is exactly `1` because the Schur
1280    /// complement was whitened by `D^(-1/2)` before it was decomposed. It is
1281    /// materialized because [`AffineRemlProfile`] takes the pencil
1282    /// `h_i = g_i + lambda s_i` as two parallel slices.
1283    penalty: Vec<f64>,
1284    /// `p_i^2`, the squared projection of the null-eliminated, penalty-whitened
1285    /// right-hand side onto mode `i`.
1286    projected_square: Vec<f64>,
1287    /// `y'Wy - b0' G00^(-1) b0`, as the single-response slice
1288    /// [`AffineRemlProfile`] expects.
1289    anchor_energy: [f64; 1],
1290}
1291
1292impl CascadeResidualSpectrum {
1293    /// `(R, S2, S3, S4)` at `lambda`. Every `theta_i` is nonnegative by
1294    /// construction and `lambda` is strictly positive, so every denominator is
1295    /// strictly positive; the caller still rejects a nonpositive `R`, which is a
1296    /// statement about the DATA rather than about this arithmetic.
1297    fn moments(&self, lambda: f64) -> (f64, f64, f64, f64) {
1298        let [s1, s2, s3, s4] = self.moment_sums(lambda);
1299        (self.anchor_energy[0] - s1, s2, s3, s4)
1300    }
1301
1302    /// `[S_1, S_2, S_3, S_4]` at `lambda`, before the anchor subtraction that
1303    /// turns `S_1` into the profiled residual.
1304    ///
1305    /// Exposed separately because `S_1` must NOT be recovered from `R` by
1306    /// undoing that subtraction. At the top of the search domain
1307    /// `lambda ~ theta_max/sqrt(eps)`, so `S_1 ~ ||beta||^2 sqrt(eps)/theta_max`
1308    /// and `anchor - S_1` loses about eight digits of `S_1`; recovering it would
1309    /// leave a `sqrt(eps)` relative error, which is exactly the resolution the
1310    /// nested-rule certificate is charged at. Every `S_k` is a sum of strictly
1311    /// positive terms, so read directly it carries no cancellation at all.
1312    fn moment_sums(&self, lambda: f64) -> [f64; 4] {
1313        let mut sums = [0.0_f64; 4];
1314        for (&theta, &projected_square) in self.eigenvalue.iter().zip(&self.projected_square) {
1315            let h = theta + lambda;
1316            let mut term = projected_square;
1317            for sum in &mut sums {
1318                term /= h;
1319                *sum += term;
1320            }
1321        }
1322        sums
1323    }
1324}
1325
1326/// Numerical evidence for the profiled-residual point quadrature past the dense
1327/// cap.
1328///
1329/// This is deliberately not a REML proof object. Krylov invariance makes the
1330/// residual rule exact, but a geometric tail estimate is only point-evaluation
1331/// evidence. Neither case encloses the independent stochastic determinant, so
1332/// [`ResidualCascadeDesign::fit_reml`] refuses the entire iterative route before
1333/// this evidence can participate in fit certification.
1334#[derive(Clone, Copy, Debug)]
1335struct ResidualQuadratureEvidence {
1336    /// Nodes in the rule this evidence is about.
1337    steps: usize,
1338    /// Nodes in the nested coarser rule it was charged against; `0` when the
1339    /// Krylov space closed and no comparison was needed.
1340    coarse_steps: usize,
1341    /// Penalized Schur rank. The reachable Krylov dimension may be smaller,
1342    /// because `rank(B) <= n - nullity`.
1343    rank: usize,
1344    /// Step budget the growth loop was allowed, before that ceiling.
1345    budget: usize,
1346    /// `||r_m|| / max_i |alpha_i|`: the Krylov residual against the operator
1347    /// scale. At roundoff, `K_m(B, beta)` is invariant and the rule is exact.
1348    relative_tail: f64,
1349    /// Geometric estimate of the rule's remaining relative error over
1350    /// `S_1..S_4` and the whole lambda domain, from three nested rules.
1351    tail_estimate: f64,
1352    /// The resolution `tail_estimate` had to reach.
1353    target: f64,
1354    /// Whether the Krylov space closed, making the residual rule exact.
1355    invariant: bool,
1356    /// Whether the rule may be used for the diagnostic point criterion. This is
1357    /// never sufficient to authorize automatic REML.
1358    accepted_for_point_evaluation: bool,
1359    /// `|sum_j w_j / ||beta||^2 - 1|` — the free mass self-check of a Gauss rule.
1360    mass_defect: f64,
1361    /// Fraction of `||beta||^2` that landed on roundoff-level nodes and was
1362    /// dropped as null-space mass.
1363    dropped_mass_fraction: f64,
1364}
1365
1366/// Extrapolated estimate of the profiled-residual quadrature's REMAINING relative
1367/// error, from three NESTED Gauss rules for the same measure, over `S_1..S_4` and
1368/// the whole `log lambda` domain.
1369///
1370/// An estimate, stated as one: it is a rate fitted to two observed gaps and
1371/// summed, not an inequality. It can authorize only the diagnostic point
1372/// criterion represented by [`ResidualQuadratureEvidence`], never automatic
1373/// REML: the independent stochastic determinant still lacks exact-real
1374/// value/derivative enclosures. The exact-dense comparison in
1375/// `the_quadrature_tail_estimate_bounds_the_error_against_the_exact_spectrum_2503`
1376/// charges the estimate against its intended numerical use at every production
1377/// budget.
1378///
1379/// WHY NOT TWO RULES. `(theta + lambda)^-k` has positive even derivatives, so the
1380/// standard Gauss error representation is one-signed and every rule
1381/// UNDER-estimates its integral: `G_j <= S_k` for every `j`. Hence
1382/// `G_m - G_{m/2} <= S_k - G_{m/2}` — the gap between two nested rules is an exact
1383/// LOWER bound on the coarser one's error. That direction is rigorous and it is
1384/// also not what a certificate needs: a small gap says the two errors are nearly
1385/// EQUAL, which is equally consistent with both being small and with both being
1386/// stuck. A two-rule agreement test is blind to stagnation by construction.
1387///
1388/// WHAT THREE RULES BUY. All the gaps are of one sign, so the remaining error is
1389/// the TAIL of the gap series, and three rules are enough to fit the decay that
1390/// tail obeys. The decay is not assumed: the Gauss-rule error for a resolvent
1391/// falls like `((sqrt(kappa) - 1)/(sqrt(kappa) + 1))^{2m}` — geometric in the node
1392/// count — so with `err(j) = C x^{4j/m}` sampled at `j = m/4, m/2, m` and
1393/// `x = r^{m/4}`,
1394///
1395/// ```text
1396/// g1 = G_{m/2} - G_{m/4} = C(x - x^2)      g2 = G_m - G_{m/2} = C(x^2 - x^4)
1397/// rho = |g2|/|g1| = x(1 + x)               tail = C x^4 = |g2| * x^2/(1 - x^2)
1398/// ```
1399///
1400/// so `x = (sqrt(1 + 4 rho) - 1)/2` recovers the rate from the two observed gaps
1401/// and the tail follows in closed form. The direction of the residual
1402/// approximation is the safe one: the effective rate IMPROVES with `m` once the
1403/// extreme Ritz values converge, so a rate fitted on `[m/4, m/2]` over-states the
1404/// tail beyond `m`.
1405///
1406/// `x >= 1` — no contraction — has no tail to extrapolate, and there the last
1407/// OBSERVED movement is reported instead. That decides correctly at both ends
1408/// without a special case: a rule that is genuinely stuck is still moving by more
1409/// than the target and is refused, while a rule that has already converged, whose
1410/// two gaps sit near the arithmetic floor and whose RATIO is therefore pure noise,
1411/// is not refused for the noise in a ratio. This is the stagnation case a two-rule
1412/// agreement test cannot see at all.
1413///
1414/// A gap already at the arithmetic's own floor (`eps * m * |G_m|`, the rounding of
1415/// the sum being compared) is converged, not stagnant, and contributes nothing.
1416///
1417/// WHICH QUANTITIES. The four moments, not the profiled residual `R = anchor -
1418/// S_1`, and `S_1` is read directly rather than recovered from `R` — see
1419/// [`CascadeResidualSpectrum::moment_sums`]. An error of `sqrt(eps)` relative on
1420/// `S_1` is an error of at most `sqrt(eps) * anchor` absolute on `R`, which is the
1421/// statement worth making about a difference that can cancel to nine digits.
1422///
1423/// GRID. Each `S_k` is a positive mixture of `(theta + lambda)^-k`, so
1424/// `|d log S_k / d log lambda| = k * (weighted mean of lambda/(theta+lambda)) <= k
1425/// <= 4`: every moment moves by at most a factor `e^4` per e-fold of `lambda`.
1426/// Four samples per e-fold resolve that, and the endpoints are always sampled.
1427fn residual_quadrature_tail_estimate(
1428    fine: &CascadeResidualSpectrum,
1429    mid: &CascadeResidualSpectrum,
1430    coarse: &CascadeResidualSpectrum,
1431    steps: usize,
1432    (lo, hi): (f64, f64),
1433) -> Result<f64, String> {
1434    if !(lo.is_finite() && hi.is_finite() && lo < hi) {
1435        return Err(format!(
1436            "residual cascade: invalid quadrature certification domain [{lo}, {hi}]"
1437        ));
1438    }
1439    let cells = (4.0 * (hi - lo)).ceil().max(8.0);
1440    if !cells.is_finite() {
1441        return Err(format!(
1442            "residual cascade: unbounded quadrature certification domain [{lo}, {hi}]"
1443        ));
1444    }
1445    let cells = cells as usize;
1446    let rounding = f64::EPSILON * steps.max(1) as f64;
1447    let mut worst = 0.0_f64;
1448    let mut sampled = 0usize;
1449    for step in 0..=cells {
1450        let log_lambda = lo + (hi - lo) * step as f64 / cells as f64;
1451        let lambda = log_lambda.exp();
1452        if !(lambda.is_finite() && lambda > 0.0) {
1453            continue;
1454        }
1455        sampled += 1;
1456        let fine_moments = fine.moment_sums(lambda);
1457        let mid_moments = mid.moment_sums(lambda);
1458        let coarse_moments = coarse.moment_sums(lambda);
1459        for index in 0..4 {
1460            let (value, previous, earlier) =
1461                (fine_moments[index], mid_moments[index], coarse_moments[index]);
1462            if !(value.is_finite() && previous.is_finite() && earlier.is_finite()) {
1463                return Err(format!(
1464                    "residual cascade: non-finite nested-rule moment S{} at log lambda \
1465                     {log_lambda} ({value}, {previous}, {earlier})",
1466                    index + 1
1467                ));
1468            }
1469            let magnitude = value.abs();
1470            if !(magnitude > 0.0) {
1471                // All three rules report an identically zero moment: nothing is
1472                // moving and there is nothing to extrapolate.
1473                if previous != 0.0 || earlier != 0.0 {
1474                    return Ok(f64::INFINITY);
1475                }
1476                continue;
1477            }
1478            let recent = (value - previous).abs();
1479            if recent <= rounding * magnitude {
1480                // The rule stopped moving at the arithmetic's own floor.
1481                continue;
1482            }
1483            let older = (previous - earlier).abs();
1484            let ratio = recent / older;
1485            // `x` solves `x^2 + x = ratio`: the per-quarter-budget decay rate the
1486            // two observed gaps imply.
1487            let rate = 0.5 * ((1.0 + 4.0 * ratio).sqrt() - 1.0);
1488            worst = worst.max(if rate < 1.0 {
1489                let square = rate * rate;
1490                recent * square / ((1.0 - square) * magnitude)
1491            } else {
1492                // NOT CONTRACTING: the geometric model does not apply and there
1493                // is nothing to extrapolate, so what is reported is the last
1494                // OBSERVED movement and no more. That is the honest quantity, and
1495                // it decides correctly at both ends without a special case. A rule
1496                // that is genuinely stuck is still moving by more than the target,
1497                // so this refuses it; a rule that has converged and whose two gaps
1498                // are both near the arithmetic floor — where their RATIO is pure
1499                // noise and routinely exceeds one — is not refused for the noise
1500                // in a ratio when its movement is already inside the target.
1501                recent / magnitude
1502            });
1503        }
1504    }
1505    if sampled == 0 {
1506        // No representable `lambda` on the declared domain, so the rules were
1507        // never compared. Agreement on zero samples is not agreement.
1508        return Err(format!(
1509            "residual cascade: the quadrature certification domain [{lo}, {hi}] contains no \
1510             representable lambda, so no nested-rule comparison was made"
1511        ));
1512    }
1513    Ok(worst)
1514}
1515
1516/// One Golub-Meurant Gauss rule read off a Lanczos run's Jacobi matrix, with the
1517/// two self-checks its construction hands over for free.
1518struct ResidualGaussRule {
1519    spectrum: CascadeResidualSpectrum,
1520    /// `|sum_j w_j / ||beta||^2 - 1|`.
1521    mass_defect: f64,
1522    /// Fraction of `||beta||^2` dropped by the two node floors.
1523    dropped_mass_fraction: f64,
1524}
1525
1526/// Where the profiled residual and its three log-lambda derivatives come from.
1527///
1528/// Both forms describe the SAME function of lambda; they differ only in what
1529/// the design's Schur decomposition left behind. Under the dense sizing cap the
1530/// determinant spectrum comes from a full eigendecomposition, so the eigen-BASIS
1531/// exists and the residual is a closed-form sum over exactly the modes the
1532/// determinant already uses — no linear solve at any lambda, and the whole score
1533/// is O(rank) per trial after the one decomposition.
1534///
1535/// Past the cap the determinant is a fixed-probe Hutchinson quadrature whose
1536/// nodes carry no basis to project the right-hand side onto — but the RESIDUAL
1537/// does not need one. It is a single quadratic form of a single known vector, so
1538/// one Lanczos run seeded with that vector gives the Golub–Meurant Gauss rule
1539/// for `S_k(lambda) = beta'(B + lambda I)^-k beta`, in the same node/weight
1540/// shape the dense route stores. Krylov invariance is exact residual evidence;
1541/// a contracting nested-rule tail can also support the explicitly diagnostic point
1542/// criterion, but is never promoted to an exact-real fit certificate. If neither
1543/// condition holds, construction refuses instead of retrying every point with
1544/// the ill-conditioned solve that caused #2503.
1545enum CascadeResidualForm {
1546    /// Exact eigenbasis projection under the dense cap. Interval-extendable via
1547    /// [`CascadeRemlProfile::affine_view`], because the determinant modes on
1548    /// this route are the SAME unit-weight modes.
1549    Spectral(CascadeResidualSpectrum),
1550    /// The Golub–Meurant point quadrature past the dense cap, admitted only for
1551    /// diagnostic score evaluation after its own numerical evidence passes.
1552    ///
1553    /// Never affine-viewable: this route's DETERMINANT modes are Hutchinson Ritz
1554    /// nodes with fractional weights, unrelated to the residual run's nodes.
1555    Quadrature(CascadeResidualSpectrum),
1556}
1557
1558impl CascadeResidualForm {
1559    /// The lambda-independent spectral form, when this route has one.
1560    fn spectrum(&self) -> &CascadeResidualSpectrum {
1561        match self {
1562            Self::Spectral(spectrum) | Self::Quadrature(spectrum) => spectrum,
1563        }
1564    }
1565}
1566
1567struct CascadeScoreEvaluation {
1568    jet: ScoreJet,
1569    /// `log|G + lambda D| - rank(D) log(lambda) - log|D|_+`.
1570    normalized_logdet: f64,
1571}
1572
1573/// The determinant half of the score at one `log lambda`.
1574struct DeterminantParts {
1575    /// `log|G + lambda D| - rank(D) log(lambda) - log|D|_+`.
1576    normalized_logdet: f64,
1577    /// `d/d log lambda`: `-sum_i w_i t_i` with `t_i = theta_i/(theta_i+lambda)`.
1578    /// Nonpositive, and INCREASING in `log lambda` because every `t_i` falls.
1579    first: f64,
1580    /// `d^2/d log lambda^2`: `sum_i w_i t_i (1-t_i)`. Nonnegative.
1581    second: f64,
1582}
1583
1584/// Machine-resolved bounded domain containing every determinant transition
1585/// `lambda ~ theta`. Outside it, every positive mode is within `sqrt(epsilon)` of
1586/// its analytic small- or large-lambda limit. The bounds scale with the actual
1587/// design spectrum rather than a fixed log-lambda window.
1588///
1589/// A free function rather than a profile method because the profiled RESIDUAL's
1590/// point-evaluation evidence has to be charged over exactly this interval, and
1591/// the residual is built while the profile is being assembled — the interval is
1592/// a function of the determinant modes alone, so it is available at that point.
1593fn certified_log_lambda_domain_from_modes(
1594    modes: &[CascadeSpectralMode],
1595) -> Result<(f64, f64), String> {
1596    let mut smallest = f64::INFINITY;
1597    let mut largest = 0.0_f64;
1598    for mode in modes {
1599        if mode.weight > 0.0 && mode.eigenvalue > 0.0 {
1600            smallest = smallest.min(mode.eigenvalue);
1601            largest = largest.max(mode.eigenvalue);
1602        }
1603    }
1604    if !(smallest.is_finite() && smallest > 0.0 && largest.is_finite() && largest > 0.0) {
1605        return Err(
1606            "residual cascade: the data identify no positive penalized Schur mode; log lambda is not estimable"
1607                .into(),
1608        );
1609    }
1610    let log_relative_resolution = certified_ln_positive(f64::EPSILON.sqrt()).ok_or_else(|| {
1611        "residual cascade: could not enclose the spectral-domain resolution".to_string()
1612    })?;
1613    let log_smallest = certified_ln_positive(smallest).ok_or_else(|| {
1614        "residual cascade: could not enclose the smallest spectral transition".to_string()
1615    })?;
1616    let log_largest = certified_ln_positive(largest).ok_or_else(|| {
1617        "residual cascade: could not enclose the largest spectral transition".to_string()
1618    })?;
1619    let minimum_log = certified_ln_positive(f64::MIN_POSITIVE).ok_or_else(|| {
1620        "residual cascade: could not enclose the minimum-normal logarithm".to_string()
1621    })?;
1622    let maximum_log = certified_ln_positive(f64::MAX).ok_or_else(|| {
1623        "residual cascade: could not enclose the maximum-finite logarithm".to_string()
1624    })?;
1625    let lo = log_smallest
1626        .add(log_relative_resolution)
1627        .lo
1628        .max(minimum_log.lo);
1629    let hi = log_largest
1630        .sub(log_relative_resolution)
1631        .hi
1632        .min(maximum_log.lo);
1633    if !(lo.is_finite() && hi.is_finite() && lo < hi) {
1634        return Err(format!(
1635            "residual cascade: invalid spectrum-derived log-lambda domain [{lo}, {hi}]"
1636        ));
1637    }
1638    Ok((lo, hi))
1639}
1640
1641impl CascadeRemlProfile<'_> {
1642    fn log_lambda_domain(&self) -> Result<(f64, f64), String> {
1643        certified_log_lambda_domain_from_modes(&self.modes)
1644    }
1645
1646    /// This profile as the affine spectral REML score it is, when the residual
1647    /// is spectral.
1648    ///
1649    /// With `h_i(lambda) = theta_i + lambda` the cascade's dense-route score is
1650    /// term for term an [`AffineRemlProfile`]: `sum log h_i - rank log lambda`
1651    /// is the normalized log-determinant, `R = anchor - sum p_i^2/h_i` is the
1652    /// profiled residual, and there is one response. The point of saying so is
1653    /// the ENCLOSURE. `AffineRemlProfile::enclose` evaluates the mode kernels on
1654    /// an interval lambda, so it is a genuine interval extension whose width
1655    /// collapses with the cell. The former endpoint-jet/global-Lipschitz
1656    /// enclosure did not collapse fast enough in saturated tails and has been
1657    /// removed.
1658    ///
1659    /// [`CascadeResidualForm::Quadrature`] is deliberately excluded even though
1660    /// it carries the same spectral shape. `AffineRemlProfile` computes the
1661    /// determinant from the modes it is handed — `sum_i log h_i - rank log
1662    /// lambda` — and past the dense cap the determinant is a Hutchinson
1663    /// quadrature over 24 independent probes with fractional weights, which is
1664    /// neither the residual run's node set nor unit-weight. Handing it the
1665    /// residual nodes would silently substitute one determinant for another.
1666    fn affine_view(&self) -> Result<Option<AffineRemlProfile<'_>>, String> {
1667        let CascadeResidualForm::Spectral(spectrum) = &self.residual else {
1668            return Ok(None);
1669        };
1670        let core = self.core;
1671        AffineRemlProfile::new(
1672            &spectrum.eigenvalue,
1673            &spectrum.penalty,
1674            &spectrum.projected_square,
1675            &spectrum.anchor_energy,
1676            (core.y.len() - core.nullity()) as f64,
1677            // Every whitened mode carries penalty scale 1, and the certified-null
1678            // modes were already dropped when the spectrum was built (see
1679            // `dense_cascade_spectrum`), so the penalized determinant rank is the
1680            // number of POSITIVE Schur modes — which is what makes this
1681            // enclosure's width track the score's own and not the arithmetic's
1682            // failure to cancel `Z·log λ` against `rank·log λ`.
1683            spectrum.penalty.len(),
1684            self.null_logdet,
1685        )
1686        .map(Some)
1687        .map_err(|error| format!("residual cascade: affine spectral profile rejected: {error}"))
1688    }
1689
1690    /// The normalized log-determinant and its first two `log lambda`
1691    /// derivatives.
1692    ///
1693    /// `O(modes)` and free of linear algebra on every route.
1694    fn determinant_parts(&self, log_lambda: f64, lambda: f64) -> DeterminantParts {
1695        let mut parts = DeterminantParts {
1696            normalized_logdet: self.null_logdet,
1697            first: 0.0,
1698            second: 0.0,
1699        };
1700        for mode in &self.modes {
1701            let theta = mode.eigenvalue;
1702            let weight = mode.weight;
1703            if theta == 0.0 || weight == 0.0 {
1704                continue;
1705            }
1706            // Stable forms for log(1 + theta/lambda) and
1707            // t=theta/(lambda+theta), including widely separated scales.
1708            let log_theta = theta.ln();
1709            parts.normalized_logdet += weight
1710                * if log_theta > log_lambda {
1711                    (log_theta - log_lambda) + (log_lambda - log_theta).exp().ln_1p()
1712                } else {
1713                    (log_theta - log_lambda).exp().ln_1p()
1714                };
1715            let t = if theta > lambda {
1716                1.0 / (1.0 + lambda / theta)
1717            } else {
1718                theta / (lambda + theta)
1719            };
1720            parts.first -= weight * t;
1721            parts.second += weight * t * (1.0 - t);
1722        }
1723        parts
1724    }
1725
1726    fn evaluate(&self, log_lambda: f64) -> Result<CascadeScoreEvaluation, String> {
1727        let lambda = gam_problem::checked_exp_log_strength(log_lambda)
1728            .map_err(|error| format!("residual cascade: {error}"))?;
1729
1730        let core = self.core;
1731        // R = y'Wy - b'A^-1b. With A' = lambda D,
1732        // R' = lambda c'Dc and
1733        // R'' = lambda c'Dc - 2 lambda^2 (Dc)'A^-1(Dc).
1734        // Both admitted forms are lambda-independent spectral sums. The
1735        // iterative constructor requires either an invariant Krylov space or a
1736        // sufficiently small nested-rule tail estimate instead of reviving the
1737        // ill-conditioned per-lambda solved fallback from #2503.
1738        let (rss, penalty_energy, inverse_penalty_energy, _third_energy) =
1739            self.residual.spectrum().moments(lambda);
1740        if !(rss.is_finite() && rss > 0.0) {
1741            return Err(format!(
1742                "residual cascade: degenerate penalized residual {rss}"
1743            ));
1744        }
1745        let rss_d1 = lambda * penalty_energy;
1746        let lambda2 = lambda * lambda;
1747        let rss_d2 = rss_d1 - 2.0 * lambda2 * inverse_penalty_energy;
1748        let DeterminantParts {
1749            normalized_logdet,
1750            first: determinant_d1,
1751            second: determinant_d2,
1752        } = self.determinant_parts(log_lambda, lambda);
1753
1754        let dof = (core.y.len() - core.nullity()) as f64;
1755        let rss_log_d1 = rss_d1 / rss;
1756        let rss_log_d2 = rss_d2 / rss - rss_log_d1 * rss_log_d1;
1757        let jet = ScoreJet {
1758            value: -0.5 * (normalized_logdet + dof * (rss / dof).ln()),
1759            derivative: -0.5 * (determinant_d1 + dof * rss_log_d1),
1760            curvature: -0.5 * (determinant_d2 + dof * rss_log_d2),
1761            // The cascade criterion API does not consume a third derivative;
1762            // certified dense-route search evaluates the affine interval
1763            // extension directly, so no endpoint third derivative is needed.
1764            third: 0.0,
1765        };
1766        if !(jet.value.is_finite() && jet.derivative.is_finite() && jet.curvature.is_finite()) {
1767            return Err(format!(
1768                "residual cascade: non-finite REML jet at log lambda {log_lambda}: value {}, derivative {}, curvature {}",
1769                jet.value, jet.derivative, jet.curvature
1770            ));
1771        }
1772        Ok(CascadeScoreEvaluation {
1773            jet,
1774            normalized_logdet,
1775        })
1776    }
1777
1778}
1779
1780/// Read `(row, col)` of a symmetric `m × m` Gram held as its row-major UPPER
1781/// triangle — the encoding of both `Core::dense_gram` and
1782/// [`Core::assemble_upper_gram`].
1783#[inline]
1784fn upper_gram_entry(gram: &[f64], m: usize, row: usize, col: usize) -> f64 {
1785    let (i, j) = if row <= col { (row, col) } else { (col, row) };
1786    gram[i * m + j]
1787}
1788
1789impl Core {
1790    #[inline]
1791    fn dense_gram_entry(&self, row: usize, col: usize) -> Option<f64> {
1792        let gram = self.dense_gram.as_ref()?;
1793        Some(upper_gram_entry(gram, self.m, row, col))
1794    }
1795
1796    /// The two Gram blocks the certified Schur complement is made of, and the
1797    /// ONLY two it needs: the penalized block `G₁₁` as a row-major PACKED upper
1798    /// triangle (`rank(rank+1)/2` entries) and the cross block `G₁₀ᵀ = G₀₁` as a
1799    /// dense `q × rank` array, `q = nullity() ≤ 4`.
1800    ///
1801    /// The `m × m` upper `X'WX` this used to assemble in full is never formed.
1802    /// That matters for one reason and it is not tidiness: the width at which a
1803    /// design can be certified is DERIVED from the route's live memory
1804    /// ([`CERTIFIED_SPECTRUM_MAX`]), so an `m²` transient that the mathematics
1805    /// does not consume is a `1/√2` factor on every design this crate can prove
1806    /// a smoothing parameter for. The null block `G₀₀` is not returned either —
1807    /// [`Self::null_gram_factor`] already builds and factors it directly.
1808    ///
1809    /// The cache is read when the design carries one, so a narrow design's Schur
1810    /// entries stay the exact `f64` they were; past the cache the same entries
1811    /// are accumulated from the CSR rows in one `O(nnz·q)` pass, in the same
1812    /// row-major order the cache was built in.
1813    fn assemble_schur_gram_blocks(&self) -> (Vec<f64>, Vec<f64>) {
1814        let m = self.m;
1815        let q = self.nullity();
1816        let rank = m - q;
1817        let mut penalized = vec![0.0_f64; packed_upper_len(rank)];
1818        let mut cross = vec![0.0_f64; q * rank];
1819        if let Some(gram) = &self.dense_gram {
1820            for k in 0..q {
1821                for j in 0..rank {
1822                    cross[k * rank + j] = upper_gram_entry(gram, m, k, q + j);
1823                }
1824            }
1825            for i in 0..rank {
1826                let base = packed_upper_row_offset(rank, i);
1827                for j in i..rank {
1828                    penalized[base + (j - i)] = upper_gram_entry(gram, m, q + i, q + j);
1829                }
1830            }
1831            return (penalized, cross);
1832        }
1833        for row in 0..self.w.len() {
1834            let lo = self.row_ptr[row];
1835            let hi = self.row_ptr[row + 1];
1836            for ea in lo..hi {
1837                let ca = self.col_idx[ea] as usize;
1838                let weighted = self.w[row] * self.vals[ea];
1839                // Columns are sorted within a row, so `eb >= ea` is exactly the
1840                // upper triangle and the two blocks are told apart by `ca`.
1841                if ca < q {
1842                    for eb in ea..hi {
1843                        let cb = self.col_idx[eb] as usize;
1844                        if cb >= q {
1845                            cross[ca * rank + (cb - q)] += weighted * self.vals[eb];
1846                        }
1847                    }
1848                } else {
1849                    let i = ca - q;
1850                    let base = packed_upper_row_offset(rank, i);
1851                    for eb in ea..hi {
1852                        let j = self.col_idx[eb] as usize - q;
1853                        penalized[base + (j - i)] += weighted * self.vals[eb];
1854                    }
1855                }
1856            }
1857        }
1858        (penalized, cross)
1859    }
1860
1861    /// Whether the certified spectral proof can be built on this core.
1862    ///
1863    /// `false` is the one honest refusal left — the design is wider than
1864    /// [`CERTIFIED_SPECTRUM_MAX`], so the Schur complement the proof is made of
1865    /// does not fit its memory budget, or the core was rebuilt from a persisted
1866    /// state and has no design left to assemble one from. Crossing
1867    /// [`DENSE_GRAM_MAX`] alone does not forfeit the proof.
1868    fn certified_spectrum_available(&self) -> bool {
1869        self.m <= CERTIFIED_SPECTRUM_MAX && !self.w.is_empty()
1870    }
1871
1872    /// Factor the unpenalized polynomial Gram block. It is tiny (`dim+1 <= 4`)
1873    /// on every route and is the exact anchor for the Schur complement.
1874    fn null_gram_factor(&self) -> Result<(Vec<f64>, f64), String> {
1875        let q = self.nullity();
1876        let mut gram = vec![0.0; q * q];
1877        if self.dense_gram.is_some() {
1878            for i in 0..q {
1879                for j in i..q {
1880                    let value = self.dense_gram_entry(i, j).expect("dense Gram exists");
1881                    gram[i * q + j] = value;
1882                    gram[j * q + i] = value;
1883                }
1884            }
1885        } else {
1886            for row in 0..self.w.len() {
1887                let lo = self.row_ptr[row];
1888                let hi = self.row_ptr[row + 1];
1889                for ea in lo..hi {
1890                    let ca = self.col_idx[ea] as usize;
1891                    if ca >= q {
1892                        break;
1893                    }
1894                    let weighted = self.w[row] * self.vals[ea];
1895                    for eb in ea..hi {
1896                        let cb = self.col_idx[eb] as usize;
1897                        if cb >= q {
1898                            break;
1899                        }
1900                        gram[ca * q + cb] += weighted * self.vals[eb];
1901                    }
1902                }
1903            }
1904            for i in 0..q {
1905                for j in i + 1..q {
1906                    gram[j * q + i] = gram[i * q + j];
1907                }
1908            }
1909        }
1910        let logdet = cholesky_logdet(&mut gram, q).map_err(|error| {
1911            format!("residual cascade: polynomial null-space factorization failed: {error}")
1912        })?;
1913        Ok((gram, logdet))
1914    }
1915
1916    /// Apply the penalty-whitened Schur complement `B` without materializing
1917    /// the data Gram. Scratch buffers are supplied by the Lanczos caller so
1918    /// each iteration remains allocation-free apart from the tiny null solve.
1919    fn schur_whitened_matvec(
1920        &self,
1921        null_chol: &[f64],
1922        input: &[f64],
1923        output: &mut [f64],
1924        full: &mut [f64],
1925        gram_full: &mut [f64],
1926        projected_null: &mut [f64],
1927    ) {
1928        let q = self.nullity();
1929        full.fill(0.0);
1930        for (i, &value) in input.iter().enumerate() {
1931            full[q + i] = value / self.pen_diag[q + i].sqrt();
1932        }
1933        self.matvec(0.0, full, gram_full);
1934        let null_coeff = chol_solve(null_chol, q, &gram_full[..q]);
1935        full.fill(0.0);
1936        full[..q].copy_from_slice(&null_coeff);
1937        self.matvec(0.0, full, projected_null);
1938        for i in 0..output.len() {
1939            output[i] = (gram_full[q + i] - projected_null[q + i]) / self.pen_diag[q + i].sqrt();
1940        }
1941    }
1942
1943    /// One full-reorthogonalization Lanczos run on the penalty-whitened Schur
1944    /// complement `B`, from a caller-supplied start vector.
1945    ///
1946    /// The determinant sweep (a Rademacher probe per run) and the profiled
1947    /// residual (one run seeded with the whitened right-hand side) are the same
1948    /// Krylov process on the same operator, and they read the same Jacobi matrix
1949    /// afterwards. Sharing ONE implementation is not tidiness: an accuracy gate
1950    /// on the residual quadrature that measured a copy of this recurrence would
1951    /// certify a routine that does not ship.
1952    ///
1953    /// The returned `T_m` is the Jacobi matrix of the Gauss quadrature rule for
1954    /// the measure `mu` that `start` induces on the spectrum of `B`, so
1955    /// `start' g(B) start ≈ ||start||^2 · e_1' g(T_m) e_1` for every analytic
1956    /// `g` — the Golub–Meurant rule. Two things decide whether that `≈` is an
1957    /// `=`, and both ride on the returned [`SchurLanczos`]: `tail`, the `(m+1, m)`
1958    /// entry the truncation dropped, and `dimension_ceiling`, the caller's bound
1959    /// on how many dimensions `K(B, start)` can have at all — reaching it leaves
1960    /// the space nothing to grow into, whatever roundoff has left in `tail`.
1961    fn schur_lanczos(
1962        &self,
1963        null_chol: &[f64],
1964        start: &[f64],
1965        max_steps: usize,
1966        dimension_ceiling: usize,
1967    ) -> Result<SchurLanczos, String> {
1968        let nullity = self.nullity();
1969        let rank = self.m - nullity;
1970        if start.len() != rank {
1971            return Err(format!(
1972                "residual cascade: Lanczos start vector carries {} entries against penalized \
1973                 Schur rank {rank}",
1974                start.len()
1975            ));
1976        }
1977        // `Sum_j w_j = ||start||^2` exactly, which is the free mass self-check
1978        // every caller applies to the weights it derives from this run.
1979        let start_norm_sq = start.iter().map(|value| value * value).sum::<f64>();
1980        let start_norm = start_norm_sq.sqrt();
1981        if !(start_norm.is_finite() && start_norm > 0.0) {
1982            return Err(format!(
1983                "residual cascade: Lanczos start vector has non-positive norm {start_norm}"
1984            ));
1985        }
1986        let steps = max_steps.min(rank);
1987        let mut full = vec![0.0; self.m];
1988        let mut gram_full = vec![0.0; self.m];
1989        let mut projected_null = vec![0.0; self.m];
1990        let mut matvec = vec![0.0; rank];
1991        // The reorthogonalization basis, `steps x rank` row-major in ONE allocation
1992        // rather than a `Vec` of `Vec`s. This is the run's whole memory footprint
1993        // (see `RESIDUAL_QUADRATURE_BASIS_BYTES`) and, at these sizes, its whole
1994        // cost: the loop below streams it end to end at every step, so it is
1995        // bandwidth-bound and the per-vector indirection was pure overhead.
1996        // Reserving it up front also removes the reallocation-and-copy that a
1997        // growing `Vec` of a few hundred megabytes otherwise pays repeatedly. The
1998        // arithmetic and its ORDER are unchanged, so this is bit-identical.
1999        let mut basis: Vec<f64> = Vec::with_capacity(steps.saturating_mul(rank));
2000        let mut q: Vec<f64> = start.iter().map(|&value| value / start_norm).collect();
2001        let mut q_previous: Option<Vec<f64>> = None;
2002        let mut alpha: Vec<f64> = Vec::with_capacity(steps);
2003        let mut beta: Vec<f64> = Vec::with_capacity(steps.saturating_sub(1));
2004        // `max_i |alpha_i|` is a lower bound on `||B||` restricted to the Krylov
2005        // space (every `alpha_i` is a Rayleigh quotient) and it only rises, which
2006        // is exactly what the break floor below needs. Stating that floor against
2007        // the CURRENT `alpha_i` instead — as this recurrence originally did — makes
2008        // it COLLAPSE in the regime it exists to detect: once the Krylov space has
2009        // consumed the operator's range, the remaining iterates are roundoff, the
2010        // Rayleigh quotients go to zero with them, and the floor chases the
2011        // residual down instead of catching it. Measured on the #2503 `n = 2500`
2012        // fixture, where `rank = 16565` but `rank(B) <= n - nullity = 2497`, so
2013        // 85% of the space is null: the run ground to 2023 steps without ever
2014        // reporting invariance and produced a Ritz value at `-7.9e-11`.
2015        let mut spectral_scale = 0.0_f64;
2016        let mut tail = 0.0_f64;
2017        // A Krylov space that has consumed its whole DIMENSION is invariant: there
2018        // is nothing left for it to grow into. `dimension_ceiling` is the caller's
2019        // bound on that dimension — `rank` is the trivial one, but for a start
2020        // vector inside `range(B)` the binding bound is `rank(B) <= n - nullity`
2021        // (`B = Z'WZ` with `W^(1/2) Z = (I - P) W^(1/2) X_1` and `P` of rank
2022        // `nullity`), which on a bounding-box-filled cascade is an order of
2023        // magnitude below `rank`. Reaching it makes the Gauss rule exact whatever
2024        // the accumulated roundoff has left in `tail`.
2025        let mut invariant = steps >= dimension_ceiling;
2026        for step in 0..steps {
2027            self.schur_whitened_matvec(
2028                null_chol,
2029                &q,
2030                &mut matvec,
2031                &mut full,
2032                &mut gram_full,
2033                &mut projected_null,
2034            );
2035            let diagonal = matvec
2036                .iter()
2037                .zip(q.iter())
2038                .map(|(&a, &b)| a * b)
2039                .sum::<f64>();
2040            alpha.push(diagonal);
2041            spectral_scale = spectral_scale.max(diagonal.abs());
2042            let mut residual = matvec.clone();
2043            for i in 0..rank {
2044                residual[i] -= diagonal * q[i];
2045            }
2046            if let Some(previous) = &q_previous {
2047                let previous_beta = beta.last().copied().unwrap_or(0.0);
2048                for i in 0..rank {
2049                    residual[i] -= previous_beta * previous[i];
2050                }
2051            }
2052            basis.extend_from_slice(&q);
2053            for direction in basis.chunks_exact(rank) {
2054                let projection = residual
2055                    .iter()
2056                    .zip(direction.iter())
2057                    .map(|(&a, &b)| a * b)
2058                    .sum::<f64>();
2059                for (value, &component) in residual.iter_mut().zip(direction) {
2060                    *value -= projection * component;
2061                }
2062            }
2063            let norm = residual
2064                .iter()
2065                .map(|value| value * value)
2066                .sum::<f64>()
2067                .sqrt();
2068            if !norm.is_finite() {
2069                return Err(
2070                    "residual cascade: Schur-spectrum Lanczos produced a non-finite norm".into(),
2071                );
2072            }
2073            tail = norm;
2074            let rounding_floor =
2075                f64::EPSILON * alpha.len() as f64 * spectral_scale.max(f64::MIN_POSITIVE);
2076            if norm <= rounding_floor {
2077                invariant = true;
2078                break;
2079            }
2080            if step + 1 == steps {
2081                break;
2082            }
2083            beta.push(norm);
2084            q_previous = Some(std::mem::replace(&mut q, residual));
2085            for value in &mut q {
2086                *value /= norm;
2087            }
2088        }
2089        Ok(SchurLanczos {
2090            alpha,
2091            beta,
2092            tail,
2093            spectral_scale,
2094            start_norm_sq,
2095            invariant,
2096        })
2097    }
2098
2099    /// Exact Schur spectrum, together with the response's coordinates in the
2100    /// eigenbasis it is computed from — and WITHOUT ever forming that basis.
2101    ///
2102    /// # What the certified profile actually consumes
2103    ///
2104    /// Two objects, and they are all this returns: every eigenvalue `θ_i` of
2105    /// `B = D^{−1/2}(G₁₁ − G₁₀G₀₀^{−1}G₀₁)D^{−1/2} = VΘVᵀ`, which is what makes
2106    /// each determinant mode an analytic kernel of `θ_i + λ`, and the single
2107    /// projected vector `Vᵀβ` for the whitened response `β`, whose squares are
2108    /// the residual moments' weights. The eigenVECTORS are read at exactly one
2109    /// place — that projection — and nowhere else.
2110    ///
2111    /// A general eigendecomposition cannot hand over `Vᵀβ` without building the
2112    /// whole `rank × rank` `V`, plus its tridiagonalization workspace. That is
2113    /// not a tidiness question here: [`CERTIFIED_SPECTRUM_MAX`] is DERIVED from
2114    /// this route's live memory, so every `m²` block it holds is a `1/√blocks`
2115    /// factor on the widest design this crate can select a smoothing parameter
2116    /// for at all — and a 6000-row cascade that identifies 5997 penalized
2117    /// directions was refused at 2893 for exactly that reason (#2758).
2118    ///
2119    /// So the decomposition is taken through
2120    /// [`gam_linalg::packed_symmetric_spectrum`], which reduces the PACKED
2121    /// triangle in place and carries `β` alongside: `V = QW` for the Householder
2122    /// `Q` and the QL `W`, so `Vᵀβ = Wᵀ(Qᵀβ)` is accumulated one vector at a
2123    /// time and neither factor is materialized. The mathematics is unchanged —
2124    /// all eigenvalues, the exact projection — and the residency is one packed
2125    /// triangle rather than the seven-plus `m²` blocks `eigh` was measured to
2126    /// hold.
2127    fn dense_cascade_spectrum(
2128        &self,
2129        null_chol: &[f64],
2130    ) -> Result<(Vec<CascadeSpectralMode>, CascadeResidualSpectrum), String> {
2131        let q = self.nullity();
2132        let rank = self.m - q;
2133        // The whitened right-hand side is built BEFORE the Schur triangle so
2134        // the peak holds one `O(rank²)` object, not two: `whitened_residual_rhs`
2135        // goes through `matvec` and allocates only `O(m)`.
2136        let (whitened, anchor_energy) = self.whitened_residual_rhs(null_chol);
2137        let (mut schur, cross_block) = self.assemble_schur_gram_blocks();
2138        // `G₀₀^{−1}G₀₁` once for every column, `q ≤ 4` rows: the null
2139        // elimination, held as `q × rank` rather than re-solved per entry.
2140        let mut eliminated = vec![0.0_f64; q * rank];
2141        let mut column = vec![0.0_f64; q];
2142        for j in 0..rank {
2143            for (k, value) in column.iter_mut().enumerate() {
2144                *value = cross_block[k * rank + j];
2145            }
2146            let solved = chol_solve(null_chol, q, &column);
2147            for (k, &coefficient) in solved.iter().enumerate() {
2148                eliminated[k * rank + j] = coefficient;
2149            }
2150        }
2151        // `B = D^{−1/2}(G₁₁ − G₀₁ᵀG₀₀^{−1}G₀₁)D^{−1/2}`, in place on the packed
2152        // triangle the Gram block was accumulated into.
2153        for i in 0..rank {
2154            let base = packed_upper_row_offset(rank, i);
2155            let scale_i = self.pen_diag[q + i];
2156            for j in i..rank {
2157                let mut value = schur[base + (j - i)];
2158                for k in 0..q {
2159                    value -= cross_block[k * rank + i] * eliminated[k * rank + j];
2160                }
2161                schur[base + (j - i)] = value / (scale_i * self.pen_diag[q + j]).sqrt();
2162            }
2163        }
2164        drop(cross_block);
2165        drop(eliminated);
2166        // `projected` enters as `β` and leaves as `Vᵀβ`, in the same ascending
2167        // order as `eigenvalues`.
2168        let mut projected = whitened;
2169        let eigenvalues = packed_symmetric_spectrum_with_probe(rank, &mut schur, &mut projected)
2170            .map_err(|error| {
2171            format!("residual cascade: Schur-complement eigendecomposition failed: {error}")
2172        })?;
2173        drop(schur);
2174        let scale = eigenvalues
2175            .iter()
2176            .copied()
2177            .map(f64::abs)
2178            .fold(0.0, f64::max);
2179        let roundoff = f64::EPSILON * rank.max(1) as f64 * scale.max(f64::MIN_POSITIVE);
2180        // A mode inside the decomposition's OWN roundoff floor is a null
2181        // direction of the whitened design, not a small positive one. The floor
2182        // is the same quantity the semidefiniteness check below is stated in;
2183        // reading it in one direction only ("this is not really negative") and
2184        // not the other ("so it is not really positive either") is what lets a
2185        // noise-level eigenvalue set the small-lambda end of the search domain
2186        // and divide into the residual there.
2187        let certified = |eigenvalue: f64| {
2188            if eigenvalue > roundoff {
2189                eigenvalue
2190            } else {
2191                0.0
2192            }
2193        };
2194        let modes = eigenvalues
2195            .iter()
2196            .copied()
2197            .enumerate()
2198            .map(|(index, eigenvalue)| {
2199                if !eigenvalue.is_finite() || eigenvalue < -roundoff {
2200                    Err(format!(
2201                        "residual cascade: penalty-whitened Schur mode {index} is not positive semidefinite ({eigenvalue})"
2202                    ))
2203                } else {
2204                    Ok(CascadeSpectralMode {
2205                        eigenvalue: certified(eigenvalue),
2206                        weight: 1.0,
2207                    })
2208                }
2209            })
2210            .collect::<Result<Vec<_>, String>>()?;
2211
2212        // A null mode carries NO response energy, exactly. The Schur complement
2213        // and the whitened right-hand side are built from the same design `Z`:
2214        // `B = Z'WZ` and `beta = Z'Wy`, so `Bv = 0` gives `Zv = 0` and hence
2215        // `v'beta = (Zv)'Wy = 0`. What the arithmetic returns for such a mode is
2216        // roundoff — and the residual sum divides it by `theta + lambda`, which
2217        // at the bottom of the search domain is SMALLER than that roundoff. On a
2218        // 558-column cascade the three null modes carried `p^2 ~ 3e-16` against
2219        // `lambda ~ 4e-19` and drove the profiled residual to -764 where the
2220        // mathematics bounds it below by the unpenalized residual sum of
2221        // squares. Restoring the exact identity is not a tolerance.
2222        let mut projected_square = vec![0.0_f64; rank];
2223        for (j, square) in projected_square.iter_mut().enumerate() {
2224            if certified(eigenvalues[j]) == 0.0 {
2225                continue;
2226            }
2227            *square = projected[j] * projected[j];
2228        }
2229        if !(anchor_energy.is_finite() && projected_square.iter().all(|v| v.is_finite())) {
2230            return Err(format!(
2231                "residual cascade: non-finite spectral residual representation (anchor {anchor_energy})"
2232            ));
2233        }
2234        // Certified-NULL modes are dropped, not carried as zeros, and the
2235        // penalized determinant rank drops with them.
2236        //
2237        // Exact, not an approximation: with `Z` null modes,
2238        // `Σ_i log(θ_i+λ) − rank·log λ = Σ_{θ>0} log(θ+λ) + Z·log λ − rank·log λ
2239        //  = Σ_{θ>0} log(θ+λ) − (rank−Z)·log λ`,
2240        // and a null mode's response energy is exactly zero (`Bv = 0` gives
2241        // `Zv = 0`, so `v'β = (Zv)'Wy = 0`), so the residual sum is unchanged
2242        // too. `determinant_parts` already skips `θ == 0` on the SCALAR path for
2243        // the same reason.
2244        //
2245        // Carrying them costs nothing on the scalar path and real width on the
2246        // INTERVAL path, which is why this is not cosmetic.
2247        // `AffineRemlProfile::enclose` evaluates the same expression in interval
2248        // arithmetic, where `Z·[log λ] − rank·[log λ]` does NOT cancel: it returns
2249        // a width proportional to `(Z + rank)·width(log λ)` where the real
2250        // function's width is proportional to the number of POSITIVE modes. On a
2251        // rank-deficient wide cascade — `m` columns against `n` rows with `m ≫ n`,
2252        // which is what box-filling nets produce on a small sample — `Z` is almost
2253        // all of `rank`, so every score enclosure is inflated by that ratio.
2254        //
2255        // What this does NOT do, on its own, is make such a design certifiable:
2256        // a 36-row / 1725-column design with all 1692 nulls dropped and only 33
2257        // modes left was still refused, so the inflation was not that design's
2258        // blocker. The blocker turned out to be a different and larger
2259        // overestimation one level down — `AffineRemlProfile::enclose` was a
2260        // natural interval extension whose VALUE range was first order in the
2261        // cell width with constant `rank` (`33.0·w`, measured over six decades)
2262        // against an exact `|f'|` of `1.15e-5`, so no cell could be retired as
2263        // resolution-flat and the search died on its subdivision budget. With
2264        // that enclosure centred, the same design certifies in about a second
2265        // (`auto_reml_certifies_a_design_the_data_cannot_identify`). Dropping the
2266        // nulls is kept because it is exact and strictly tightens every
2267        // enclosure, which is its own reason.
2268        let mut kept_eigenvalue = Vec::with_capacity(rank);
2269        let mut kept_projected_square = Vec::with_capacity(rank);
2270        for (index, &eigenvalue) in eigenvalues.iter().enumerate() {
2271            if certified(eigenvalue) > 0.0 {
2272                kept_eigenvalue.push(certified(eigenvalue));
2273                kept_projected_square.push(projected_square[index]);
2274            }
2275        }
2276        let kept = kept_eigenvalue.len();
2277        Ok((
2278            modes,
2279            CascadeResidualSpectrum {
2280                eigenvalue: kept_eigenvalue,
2281                penalty: vec![1.0; kept],
2282                projected_square: kept_projected_square,
2283                anchor_energy: [anchor_energy],
2284            },
2285        ))
2286    }
2287
2288    /// Fixed-probe Lanczos quadrature of the lambda-independent Schur
2289    /// spectrum. Unlike the previous lambda-dependent SLQ call, its nodes and
2290    /// weights define one smooth analytic score across the entire search
2291    /// domain, so differentiating the scalar kernels is exact for the score
2292    /// being optimized.
2293    fn iterative_cascade_spectrum(
2294        &self,
2295        null_chol: &[f64],
2296    ) -> Result<Vec<CascadeSpectralMode>, String> {
2297        let q0 = self.nullity();
2298        let rank = self.m - q0;
2299        let steps = SLQ_LANCZOS_STEPS.min(rank);
2300        let mut modes = Vec::with_capacity(SLQ_PROBES * steps);
2301
2302        for probe in 0..SLQ_PROBES {
2303            let mut rng =
2304                SplitMix64::new(RNG_SEED ^ (probe as u64).wrapping_mul(0xD134_2543_DE82_EF95));
2305            // Unit-entry Rademacher probe. `schur_lanczos` normalizes it, and
2306            // `||probe||^2 = rank` exactly (a sum of `rank` ones), so the
2307            // Hutchinson scaling below reads that norm rather than restating it.
2308            let probe_vector = (0..rank).map(|_| rng.next_sign()).collect::<Vec<_>>();
2309            let SchurLanczos {
2310                alpha,
2311                beta,
2312                start_norm_sq,
2313                ..
2314            } = self.schur_lanczos(null_chol, &probe_vector, steps, rank)?;
2315            let (eigenvalues, first_components) = symmetric_tridiagonal_eigen(&alpha, &beta)?;
2316            let scale = eigenvalues
2317                .iter()
2318                .copied()
2319                .map(f64::abs)
2320                .fold(0.0, f64::max);
2321            let roundoff = f64::EPSILON * alpha.len().max(1) as f64 * scale.max(f64::MIN_POSITIVE);
2322            for (index, (&eigenvalue, &first)) in
2323                eigenvalues.iter().zip(first_components.iter()).enumerate()
2324            {
2325                if !eigenvalue.is_finite() || eigenvalue < -roundoff {
2326                    return Err(format!(
2327                        "residual cascade: Schur-spectrum Ritz value {index} is not positive semidefinite ({eigenvalue})"
2328                    ));
2329                }
2330                let weight = start_norm_sq * first * first / SLQ_PROBES as f64;
2331                if !(weight.is_finite() && weight >= 0.0) {
2332                    return Err(format!(
2333                        "residual cascade: invalid Schur-spectrum quadrature weight {weight}"
2334                    ));
2335                }
2336                modes.push(CascadeSpectralMode {
2337                    // Same reading of the same floor as the dense route: a Ritz
2338                    // value inside the quadrature's own roundoff is a null
2339                    // direction, not a small positive mode. Admitting it as
2340                    // positive lets it set the small-lambda end of
2341                    // `log_lambda_domain`, which is how the search comes to
2342                    // demand a solve of `X'WX + λD` at a λ that leaves the
2343                    // matrix numerically singular.
2344                    eigenvalue: if eigenvalue > roundoff {
2345                        eigenvalue
2346                    } else {
2347                        0.0
2348                    },
2349                    weight,
2350                });
2351            }
2352        }
2353        Ok(modes)
2354    }
2355
2356    /// `beta = D^(-1/2)(b1 - G10 G00^(-1) b0)` and
2357    /// `anchor_energy = y'Wy - b0' G00^(-1) b0`: the null-eliminated,
2358    /// penalty-whitened right-hand side and the part of the profiled residual no
2359    /// lambda can move.
2360    ///
2361    /// Identical in exact arithmetic to what [`Self::dense_cascade_spectrum`]
2362    /// builds inline, but routed through [`Self::matvec`] instead of
2363    /// `dense_gram_entry`, so it is available past the dense cap. `matvec(0, v)`
2364    /// applies the FULL `X'WX`, and only its `1`-block rows are read — that is
2365    /// `G10 (G00^(-1) b0)`, the cross term, with no dense Gram formed.
2366    fn whitened_residual_rhs(&self, null_chol: &[f64]) -> (Vec<f64>, f64) {
2367        let q = self.nullity();
2368        let rank = self.m - q;
2369        let null_coeff = chol_solve(null_chol, q, &self.rhs[..q]);
2370        let mut full = vec![0.0; self.m];
2371        full[..q].copy_from_slice(&null_coeff);
2372        let mut cross = vec![0.0; self.m];
2373        self.matvec(0.0, &full, &mut cross);
2374        let beta = (0..rank)
2375            .map(|i| (self.rhs[q + i] - cross[q + i]) / self.pen_diag[q + i].sqrt())
2376            .collect::<Vec<_>>();
2377        let anchor_energy = self.ytwy
2378            - self.rhs[..q]
2379                .iter()
2380                .zip(null_coeff.iter())
2381                .map(|(&b, &c)| b * c)
2382                .sum::<f64>();
2383        (beta, anchor_energy)
2384    }
2385
2386    /// One Golub-Meurant Gauss rule from the leading `steps x steps` block of a
2387    /// beta-seeded Lanczos run's Jacobi matrix.
2388    ///
2389    /// `T_m`'s leading `j x j` block IS `T_j`, the Jacobi matrix the same run
2390    /// would have produced had it stopped at step `j` — the Lanczos recurrence is
2391    /// forward. So every nested rule of a single run is available for the cost of
2392    /// one tridiagonal eigendecomposition, which is what makes the convergence
2393    /// comparison in [`Self::iterative_residual_spectrum`] free of a second run.
2394    ///
2395    /// Nodes are the Ritz values `theta_j`, weights are `||beta||^2 tau_j^2` with
2396    /// `tau_j` the first component of Ritz vector `j` — exactly the
2397    /// `(eigenvalue, projected_square)` pair [`CascadeResidualSpectrum`] stores,
2398    /// so the iterative route populates the same struct and inherits
2399    /// [`CascadeResidualSpectrum::moments`] unchanged.
2400    ///
2401    /// TWO NODE FLOORS, both derived rather than tuned.
2402    ///
2403    /// The first is the dense route's, read the same way: a node inside the
2404    /// decomposition's own roundoff (`eps*m*theta_max`) is a NULL direction of the
2405    /// whitened design, and a null direction carries no response energy exactly
2406    /// (`Bv = 0` gives `Zv = 0` and hence `v'beta = 0`).
2407    ///
2408    /// The second is on the WEIGHT, and it exists because the first is not
2409    /// sufficient for a RITZ value. Measured (#2503, `side=14 levels=4`, rank 203,
2410    /// 96 steps): one node at `theta = 2.65e-11` — `1.8e-12` of `theta_max`, but
2411    /// 80x ABOVE the eigenvalue floor, so that floor passes it — carrying weight
2412    /// `8.9e-27 ||beta||^2`. At the bottom of the search domain
2413    /// (`lambda ~ 2.9e-11`) that single node contributes `w/(theta+lambda)^4` and
2414    /// `S_4` comes out `3.6e7` RELATIVE off while `S_2` is still right to `6e-9`.
2415    /// The catastrophe #2503 attributed to quadrature truncation is one node whose
2416    /// weight is pure roundoff and whose position happens to sit under `lambda`.
2417    ///
2418    /// The floor is the SQUARE of the roundoff in the quantity being squared. A
2419    /// Ritz vector's first component comes out of a QL sweep that accumulates
2420    /// `O(m)` plane rotations, so a component that should be zero comes out at
2421    /// `~eps*m`; its square, times the mass, is `(eps*m)^2 ||beta||^2`. Measured
2422    /// against that prediction across three fixtures and eleven step budgets:
2423    /// every spurious weight landed in `[1e-258, 4e-27] ||beta||^2` — i.e. at or
2424    /// below `(eps*m)^2` — while the smallest GENUINE mass the rules carried was
2425    /// `4.1e-14 ||beta||^2`, thirteen orders above the floor. The earlier
2426    /// `eps*m*||beta||^2` floor did over-drop that genuine mass, and the symptom
2427    /// was diagnostic: it GREW with `m`, so a longer run dropped more, which no
2428    /// convergent process can be right about.
2429    fn residual_gauss_rule(
2430        &self,
2431        run: &SchurLanczos,
2432        steps: usize,
2433        anchor_energy: f64,
2434        measure_mass: f64,
2435    ) -> Result<ResidualGaussRule, String> {
2436        let steps = steps.min(run.alpha.len());
2437        let (ritz, first_components) = symmetric_tridiagonal_eigen(
2438            &run.alpha[..steps],
2439            &run.beta[..steps.saturating_sub(1)],
2440        )?;
2441        let scale = ritz.iter().copied().map(f64::abs).fold(0.0, f64::max);
2442        let count = steps.max(1) as f64;
2443        let eigenvalue_floor = f64::EPSILON * count * scale.max(f64::MIN_POSITIVE);
2444        let component_roundoff = f64::EPSILON * count;
2445        let mass_floor = component_roundoff * component_roundoff * measure_mass;
2446
2447        let mut eigenvalue = Vec::with_capacity(ritz.len());
2448        let mut projected_square = Vec::with_capacity(ritz.len());
2449        let mut total_mass = 0.0_f64;
2450        let mut dropped_mass = 0.0_f64;
2451        // A Ritz value is not an eigenvalue: it comes out of a Lanczos recurrence
2452        // and a QL sweep whose backward error grows with the step count, so the
2453        // threshold at which negativity stops being arithmetic and starts being a
2454        // statement about `B` is NOT the eigenvalue floor. Below `sqrt(eps)*theta_max`
2455        // — the resolution this module works at throughout, the same one
2456        // `certified_log_lambda_domain_from_modes` pads with — a negative Ritz value is
2457        // indistinguishable from the zero it is approximating, and is clamped to
2458        // the null direction it represents. Above it, the penalty-whitened Schur
2459        // complement is genuinely indefinite, which is a defect and not roundoff.
2460        let indefinite = f64::EPSILON.sqrt() * scale.max(f64::MIN_POSITIVE);
2461        for (index, (&theta, &first)) in ritz.iter().zip(first_components.iter()).enumerate() {
2462            if !theta.is_finite() || theta < -indefinite {
2463                return Err(format!(
2464                    "residual cascade: profiled-residual Ritz value {index} of {steps} is not \
2465                     positive semidefinite ({theta}); the whitened Schur complement is \
2466                     indefinite beyond the run's own resolution {indefinite}"
2467                ));
2468            }
2469            let weight = run.start_norm_sq * first * first;
2470            if !(weight.is_finite() && weight >= 0.0) {
2471                return Err(format!(
2472                    "residual cascade: invalid profiled-residual quadrature weight {weight}"
2473                ));
2474            }
2475            total_mass += weight;
2476            if theta <= eigenvalue_floor || weight <= mass_floor {
2477                dropped_mass += weight;
2478                eigenvalue.push(0.0);
2479                projected_square.push(0.0);
2480            } else {
2481                eigenvalue.push(theta);
2482                projected_square.push(weight);
2483            }
2484        }
2485        // The weights of ANY Gauss rule sum to the measure's total mass, so
2486        // `sum_j w_j = ||beta||^2` is a free self-check on the whole Jacobi
2487        // pipeline — the recurrence, the reorthogonalization and the tridiagonal
2488        // eigensolver at once. It is charged against the accumulated rounding of
2489        // the sum it checks, not against a tuned slack.
2490        let mass_defect = ((total_mass - measure_mass) / measure_mass).abs();
2491        let mass_tolerance = 8.0 * f64::EPSILON * count;
2492        if !(mass_defect <= mass_tolerance) {
2493            return Err(format!(
2494                "residual cascade: profiled-residual quadrature weights sum to {total_mass} \
2495                 against the measure mass {measure_mass} (relative defect {mass_defect} over \
2496                 tolerance {mass_tolerance}); the Gauss rule for a measure of mass m has weights \
2497                 summing to m, so the Jacobi matrix or its eigendecomposition is wrong"
2498            ));
2499        }
2500        let modes = eigenvalue.len();
2501        Ok(ResidualGaussRule {
2502            spectrum: CascadeResidualSpectrum {
2503                eigenvalue,
2504                penalty: vec![1.0; modes],
2505                projected_square,
2506                anchor_energy: [anchor_energy],
2507            },
2508            mass_defect,
2509            dropped_mass_fraction: dropped_mass / measure_mass,
2510        })
2511    }
2512
2513    /// The profiled residual's spectral form past the dense cap, by Golub-Meurant
2514    /// quadrature of the SAME Schur operator the determinant sweep runs on — or
2515    /// `None` when no rule earned the right to be used.
2516    ///
2517    /// `S_k(lambda) = beta'(B + lambda I)^-k beta` is `integral (theta +
2518    /// lambda)^-k dmu(theta)` for the measure `mu` that `beta` induces on
2519    /// `spec(B)`. One Lanczos run seeded with `beta` (rather than with a
2520    /// Rademacher probe) gives the Jacobi matrix of the `m`-node Gauss rule for
2521    /// that measure; see [`Self::residual_gauss_rule`] for the rule itself and its
2522    /// two node floors. Where a rule is admitted, the whole score is solve-free at
2523    /// every `lambda`, exactly as on the dense route after #2455 — and the
2524    /// domain-endpoint `lambda` that no PCG can solve (#2503) is never solved at.
2525    ///
2526    /// WHAT ADMITS A RULE, and why it is not the step count.
2527    ///
2528    /// A Gauss rule accurate in VALUE need not be accurate in its
2529    /// lambda-DERIVATIVES: the nodes are placed to integrate one kernel, and
2530    /// `(theta + lambda)^-k` grows more peaked at the bottom of the spectrum with
2531    /// every power of `k`. `S_2, S_3, S_4` ARE the score's first three
2532    /// `log lambda` derivatives, so a rule may not be admitted on `R` alone. Two
2533    /// point-evaluation admissions are available and both are properties of the
2534    /// run, not of a calibrated budget:
2535    ///
2536    /// 1. THE KRYLOV SPACE CLOSED (`SchurLanczos::invariant`). Then
2537    ///    `(B + lambda I)^-k beta` lies inside `K_m` for every `k` and every
2538    ///    `lambda`, and the rule reproduces the spectral sum outright.
2539    /// 2. THE NESTED LADDER HAS CONTRACTED. Every Gauss rule for a completely
2540    ///    monotone kernel UNDER-estimates its integral (the `(2m)`-th derivative
2541    ///    of `(theta+lambda)^-k` is positive, so the standard error
2542    ///    representation is one-signed), so the rules at `m/4`, `m/2`, `m` — free,
2543    ///    since `T_m`'s leading block IS `T_j` — rise toward `S_k` with all their
2544    ///    gaps of one sign, and the remaining error is the tail of those gaps.
2545    ///    [`residual_quadrature_tail_estimate`] extrapolates that tail
2546    ///    geometrically and REFUSES when the last two gaps do not contract, which
2547    ///    is the stagnation case a bare two-rule agreement test cannot see. The
2548    ///    estimate must fall below the diagnostic point resolution —
2549    ///    `sqrt(eps)`, the same constant `certified_log_lambda_domain_from_modes` uses for
2550    ///    endpoint padding. Measured against the exact dense eigenbasis at every
2551    ///    budget on this ladder, over three designs: where the estimate passed,
2552    ///    the rule was within `1e-12`.
2553    ///
2554    /// The budget GROWS geometrically until one of those fires, so the accepted
2555    /// rule is the smallest that passes rather than the largest affordable. That
2556    /// matters in both directions: past roughly 60% of the penalized rank the run
2557    /// starts producing near-null ghost nodes (measured: rank 473, from 256 steps
2558    /// on), so the cheapest passing rule is also the cleanest one.
2559    ///
2560    /// Returns `None` when neither admission holds. The caller turns that into a
2561    /// refusal; it never revives the per-`lambda` solve fallback.
2562    fn iterative_residual_spectrum(
2563        &self,
2564        null_chol: &[f64],
2565        domain: (f64, f64),
2566    ) -> Result<(Option<CascadeResidualSpectrum>, ResidualQuadratureEvidence), String> {
2567        let rank = self.m - self.nullity();
2568        let ceiling = self.residual_krylov_ceiling();
2569        let budget = self.residual_quadrature_budget();
2570        let target = f64::EPSILON.sqrt();
2571        let (beta, anchor_energy) = self.whitened_residual_rhs(null_chol);
2572        if !(anchor_energy.is_finite() && beta.iter().all(|value| value.is_finite())) {
2573            return Err(format!(
2574                "residual cascade: non-finite whitened residual right-hand side (anchor \
2575                 {anchor_energy})"
2576            ));
2577        }
2578        let measure_mass = beta.iter().map(|value| value * value).sum::<f64>();
2579        if !(measure_mass > 0.0) {
2580            // No response energy outside the polynomial null space: the profiled
2581            // residual is the anchor at every lambda. A zero measure is exactly
2582            // integrated by the empty rule, so this evidence is exact.
2583            return Ok((
2584                Some(CascadeResidualSpectrum {
2585                    eigenvalue: Vec::new(),
2586                    penalty: Vec::new(),
2587                    projected_square: Vec::new(),
2588                    anchor_energy: [anchor_energy],
2589                }),
2590                ResidualQuadratureEvidence {
2591                    steps: 0,
2592                    coarse_steps: 0,
2593                    rank,
2594                    budget,
2595                    relative_tail: 0.0,
2596                    tail_estimate: 0.0,
2597                    target,
2598                    invariant: true,
2599                    accepted_for_point_evaluation: true,
2600                    mass_defect: 0.0,
2601                    dropped_mass_fraction: 0.0,
2602                },
2603            ));
2604        }
2605        let mut steps = SLQ_LANCZOS_STEPS.min(budget);
2606        loop {
2607            let run = self.schur_lanczos(null_chol, &beta, steps, ceiling)?;
2608            let taken = run.alpha.len();
2609            let fine = self.residual_gauss_rule(&run, taken, anchor_energy, measure_mass)?;
2610            let coarse_steps = taken / 4;
2611            let tail_estimate = if run.invariant {
2612                // A closed Krylov space makes the rule exact for every kernel;
2613                // there is nothing left for a nested comparison to add.
2614                0.0
2615            } else if coarse_steps == 0 {
2616                // Fewer than four nodes leaves no nested ladder to extrapolate
2617                // along, and "no evidence" is not "converged".
2618                f64::INFINITY
2619            } else {
2620                let mid =
2621                    self.residual_gauss_rule(&run, taken / 2, anchor_energy, measure_mass)?;
2622                let coarse =
2623                    self.residual_gauss_rule(&run, coarse_steps, anchor_energy, measure_mass)?;
2624                residual_quadrature_tail_estimate(
2625                    &fine.spectrum,
2626                    &mid.spectrum,
2627                    &coarse.spectrum,
2628                    taken,
2629                    domain,
2630                )?
2631            };
2632            let evidence = ResidualQuadratureEvidence {
2633                steps: taken,
2634                coarse_steps: if run.invariant { 0 } else { coarse_steps },
2635                rank,
2636                budget,
2637                relative_tail: run.tail / run.spectral_scale.max(f64::MIN_POSITIVE),
2638                tail_estimate,
2639                target,
2640                invariant: run.invariant,
2641                accepted_for_point_evaluation: run.invariant || tail_estimate <= target,
2642                mass_defect: fine.mass_defect,
2643                dropped_mass_fraction: fine.dropped_mass_fraction,
2644            };
2645            if evidence.accepted_for_point_evaluation {
2646                return Ok((Some(fine.spectrum), evidence));
2647            }
2648            if taken >= budget {
2649                return Ok((None, evidence));
2650            }
2651            steps = steps.saturating_mul(2).min(budget);
2652        }
2653    }
2654
2655    /// Lanczos steps the profiled-residual quadrature may grow to past the dense
2656    /// cap.
2657    ///
2658    /// Not an accuracy dial — a rule is admitted by its own convergence, not by
2659    /// reaching a step count — so this bounds how large a Krylov space we are
2660    /// willing to REORTHOGONALIZE before declining the point evaluation. The binding
2661    /// resource is the basis itself (`steps x rank` doubles held live), so the
2662    /// bound is stated as that memory and the number in the code is the one being
2663    /// reasoned about.
2664    ///
2665    /// Two structural ceilings apply on top. `rank` is the obvious one. The other
2666    /// is `n - nullity`: `B = Z'WZ` for an `n x rank` whitened design, so
2667    /// `rank(B) <= n - nullity`, and in exact arithmetic `K_m(B, beta)` cannot
2668    /// grow past that dimension. Steps beyond it are spent entirely inside the
2669    /// numerical null space, which is where the ghost nodes the weight floor has
2670    /// to clean up come from.
2671    fn residual_quadrature_budget(&self) -> usize {
2672        let rank = self.m - self.nullity();
2673        let by_memory =
2674            RESIDUAL_QUADRATURE_BASIS_BYTES / (size_of::<f64>() * rank.max(1)).max(1);
2675        by_memory
2676            .max(SLQ_LANCZOS_STEPS)
2677            .min(self.residual_krylov_ceiling())
2678    }
2679
2680    /// Largest dimension `K_m(B, beta)` can reach, so reaching it proves the space
2681    /// is `B`-invariant and the Gauss rule EXACT.
2682    ///
2683    /// `B = D^(-1/2)(G11 - G10 G00^(-1) G01) D^(-1/2) = Z'WZ` with
2684    /// `W^(1/2) Z = (I - P) W^(1/2) X_1` and `P` the `W`-projector onto the
2685    /// polynomial block, so `rank(B) <= n - nullity`; and `beta = Z'Wy` lies in
2686    /// `range(B)`, so the Krylov space cannot leave it. On a cascade whose net
2687    /// fills the bounding BOX rather than the data cloud this is the binding bound
2688    /// by an order of magnitude — measured on the `n = 800` 2-D fixture at
2689    /// refinement level 6: `rank = 7387` against `n - nullity = 797`, because 89%
2690    /// of the columns are void-filling centres the data cannot pin. Reading the
2691    /// ceiling as `rank` there made the invariance test unreachable and sent the
2692    /// route back to the solve at full budget.
2693    fn residual_krylov_ceiling(&self) -> usize {
2694        let rank = self.m - self.nullity();
2695        rank.min(self.y.len().saturating_sub(self.nullity())).max(1)
2696    }
2697
2698    fn reml_profile(&self) -> Result<CascadeRemlProfile<'_>, String> {
2699        let (null_chol, null_logdet) = self.null_gram_factor()?;
2700        // The exact spectral form is taken whenever the dense eigendecomposition
2701        // fits its memory budget — which is wider than the Gram cache, so a
2702        // design past `DENSE_GRAM_MAX` still gets the certifiable profile and
2703        // pays for the Gram only for the duration of the decomposition.
2704        let (modes, residual) = if self.certified_spectrum_available() {
2705            let (modes, spectrum) = self.dense_cascade_spectrum(&null_chol)?;
2706            (modes, CascadeResidualForm::Spectral(spectrum))
2707        } else {
2708            let modes = self.iterative_cascade_spectrum(&null_chol)?;
2709            // The residual's numerical evidence is charged over exactly the
2710            // interval the diagnostic point criterion may visit, so the
2711            // determinant modes — which define that interval — are built first.
2712            let domain = certified_log_lambda_domain_from_modes(&modes)?;
2713            let (spectrum, evidence) =
2714                self.iterative_residual_spectrum(&null_chol, domain)?;
2715            let spectrum = spectrum.ok_or_else(|| {
2716                format!(
2717                    "residual cascade: profiled-residual quadrature did not resolve inside its \
2718                     resource-derived budget (steps {}, coarse steps {}, rank {}, budget {}, \
2719                     invariant {}, tail estimate {:.3e} against target {:.3e}, relative tail {:.3e}, \
2720                     mass defect {:.3e}, dropped mass fraction {:.3e}); refusing the \
2721                     ill-conditioned per-lambda solve fallback",
2722                    evidence.steps,
2723                    evidence.coarse_steps,
2724                    evidence.rank,
2725                    evidence.budget,
2726                    evidence.invariant,
2727                    evidence.tail_estimate,
2728                    evidence.target,
2729                    evidence.relative_tail,
2730                    evidence.mass_defect,
2731                    evidence.dropped_mass_fraction,
2732                )
2733            })?;
2734            (modes, CascadeResidualForm::Quadrature(spectrum))
2735        };
2736        Ok(CascadeRemlProfile {
2737            core: self,
2738            null_logdet,
2739            modes,
2740            residual,
2741        })
2742    }
2743
2744    /// Scale a raw point into shifted metric coordinates.
2745    fn scale_point(&self, x: &[f64]) -> [f64; 3] {
2746        let mut z = [0.0_f64; 3];
2747        for a in 0..self.dim {
2748            z[a] = self.metric[a] * x[a] - self.z_lo[a];
2749        }
2750        z
2751    }
2752
2753    /// Sparse basis row at a scaled point: polynomial layer then every bump
2754    /// whose support covers it, as (column, value) pairs sorted by column.
2755    fn basis_row_scaled(&self, z: &[f64; 3]) -> Vec<(usize, f64)> {
2756        let mut row = Vec::with_capacity(self.dim + 1 + self.levels.len() * 8);
2757        row.push((0, 1.0));
2758        for a in 0..self.dim {
2759            row.push((a + 1, 2.0 * z[a] / self.z_range[a] - 1.0));
2760        }
2761        for level in &self.levels {
2762            let start = row.len();
2763            level.grid.for_neighbors(z, |j| {
2764                let c = &level.centers[j as usize];
2765                let r = dist2(z, c, self.dim).sqrt() / level.delta;
2766                let v = wendland(r);
2767                if v > 0.0 {
2768                    row.push((level.col_offset + j as usize, v));
2769                }
2770            });
2771            row[start..].sort_unstable_by_key(|&(col, _)| col);
2772        }
2773        row
2774    }
2775
2776    /// `out = (X'WX + λD)·v` through the CSR rows: O(nnz).
2777    fn matvec(&self, lambda: f64, v: &[f64], out: &mut [f64]) {
2778        for (o, (&d, &x)) in out.iter_mut().zip(self.pen_diag.iter().zip(v.iter())) {
2779            *o = lambda * d * x;
2780        }
2781        for i in 0..self.w.len() {
2782            let lo = self.row_ptr[i];
2783            let hi = self.row_ptr[i + 1];
2784            let mut t = 0.0;
2785            for e in lo..hi {
2786                t += self.vals[e] * v[self.col_idx[e] as usize];
2787            }
2788            t *= self.w[i];
2789            for e in lo..hi {
2790                out[self.col_idx[e] as usize] += self.vals[e] * t;
2791            }
2792        }
2793    }
2794
2795    /// Jacobi / level-diagonal preconditioner: `diag(X'WX) + λ·diag(λD)`.
2796    /// Levels share a constant prior weight, so this IS the level-block
2797    /// (BPX-flavored) diagonal in the multilevel frame.
2798    /// Coarse column count of the additive-Schwarz coarse space at `λ`: the
2799    /// polynomial layer plus the longest prefix of data-dominated levels
2800    /// (`λ d_l < COARSE_DOMINANCE · median diag(X'WX) over the level`), with the
2801    /// two coarsest levels always deflated and the total capped at
2802    /// [`COARSE_SPACE_MAX`]. Because `d_l` rises while the per-level data weight
2803    /// falls, the data-dominated set is a contiguous prefix, so one scan from the
2804    /// coarsest level finds the cut. (See [`COARSE_DOMINANCE`].)
2805    fn coarse_space_cols(&self, lambda: f64) -> usize {
2806        let mut ncoarse = self.nullity();
2807        let mut buf: Vec<f64> = Vec::new();
2808        for (li, level) in self.levels.iter().enumerate() {
2809            let a = level.col_offset;
2810            let b = a + level.centers.len();
2811            if b <= a {
2812                continue;
2813            }
2814            if b > COARSE_SPACE_MAX {
2815                break;
2816            }
2817            let dominated = if li < MIN_COARSE_LEVELS {
2818                true
2819            } else {
2820                buf.clear();
2821                buf.extend_from_slice(&self.gram_diag[a..b]);
2822                buf.sort_unstable_by(|x, y| x.total_cmp(y));
2823                let gram_median = buf[buf.len() / 2];
2824                lambda * level.weight < COARSE_DOMINANCE * gram_median
2825            };
2826            if dominated {
2827                ncoarse = b;
2828            } else {
2829                break;
2830            }
2831        }
2832        // Keep at least one fine column so the split is well-defined; if every
2833        // level is coarse the iterative route is degenerate anyway and the dense
2834        // route would have been taken, but guard regardless.
2835        let ncoarse = ncoarse.min(self.m);
2836        // Debug-only coarse-space layout trace (#1032). Gated on the log level so
2837        // the per-call string build stays out of this preconditioner hot path,
2838        // and routed through `log` (an `eprintln!` here trips the src banned-macro
2839        // gate and broke the build).
2840        if log::log_enabled!(log::Level::Debug) {
2841            let mut s = String::new();
2842            for (li, level) in self.levels.iter().enumerate() {
2843                let a = level.col_offset;
2844                let b = a + level.centers.len();
2845                let mut buf: Vec<f64> = self.gram_diag[a..b].to_vec();
2846                buf.sort_unstable_by(|x, y| x.total_cmp(y));
2847                let med = if buf.is_empty() {
2848                    0.0
2849                } else {
2850                    buf[buf.len() / 2]
2851                };
2852                let coarse = b <= ncoarse;
2853                s.push_str(&format!(
2854                    " L{li}[{}c off{a} w={:.2e} λw={:.2e} med={:.2e} {}]",
2855                    level.centers.len(),
2856                    level.weight,
2857                    lambda * level.weight,
2858                    med,
2859                    if coarse { "C" } else { "F" }
2860                ));
2861            }
2862            log::debug!(
2863                "[1032-COARSE] λ={lambda:.3e} m={} ncoarse={ncoarse} cap={COARSE_SPACE_MAX}{s}",
2864                self.m
2865            );
2866        }
2867        ncoarse
2868    }
2869
2870    /// Build the coarse-space additive-Schwarz preconditioner at `λ`: assemble
2871    /// and factor the coarse block `A_CC` from the CSR (coarse columns are the
2872    /// prefix `[0, ncoarse)`, and each CSR row is column-sorted, so a row's
2873    /// coarse entries are its leading run), then the Jacobi diagonal on the fine
2874    /// tail. `O(n · q_C²) + O(ncoarse³)` — paid once per `λ`, not per CG step.
2875    fn build_preconditioner(&self, lambda: f64) -> Result<Preconditioner, String> {
2876        let m = self.m;
2877        let nc = self.coarse_space_cols(lambda);
2878        let mut acc = vec![0.0_f64; nc * nc];
2879        for i in 0..self.w.len() {
2880            let lo = self.row_ptr[i];
2881            let hi = self.row_ptr[i + 1];
2882            // Leading run of coarse columns (CSR rows are column-sorted).
2883            let mut end = lo;
2884            while end < hi && (self.col_idx[end] as usize) < nc {
2885                end += 1;
2886            }
2887            for ea in lo..end {
2888                let ca = self.col_idx[ea] as usize;
2889                let va = self.w[i] * self.vals[ea];
2890                for eb in ea..end {
2891                    let cb = self.col_idx[eb] as usize;
2892                    acc[ca * nc + cb] += va * self.vals[eb];
2893                }
2894            }
2895        }
2896        for i in 0..nc {
2897            for j in i + 1..nc {
2898                acc[j * nc + i] = acc[i * nc + j];
2899            }
2900        }
2901        for i in 0..nc {
2902            acc[i * nc + i] += lambda * self.pen_diag[i];
2903        }
2904        let coarse_logdet = cholesky_logdet(&mut acc, nc)?;
2905        let mut inv_fine = Vec::with_capacity(m - nc);
2906        let mut inv_sqrt_fine = Vec::with_capacity(m - nc);
2907        let mut fine_logdet = 0.0;
2908        for j in nc..m {
2909            let p = self.gram_diag[j] + lambda * self.pen_diag[j];
2910            // A positive preconditioner diagonal is the whole requirement; no
2911            // floor stands between "positive" and "usable".
2912            if !(p.is_finite() && p > 0.0) {
2913                return Err(format!(
2914                    "residual cascade: non-positive preconditioner diagonal {p} at column {j}"
2915                ));
2916            }
2917            inv_fine.push(1.0 / p);
2918            inv_sqrt_fine.push(1.0 / p.sqrt());
2919            fine_logdet += p.ln();
2920        }
2921        Ok(Preconditioner {
2922            ncoarse: nc,
2923            coarse_chol: acc,
2924            coarse_logdet,
2925            inv_fine,
2926            inv_sqrt_fine,
2927            fine_logdet,
2928        })
2929    }
2930
2931    /// Preconditioned CG on `(X'WX + λD)c = b` to relative residual CG_RTOL.
2932    /// Returns the solution with its backward-error certificate.
2933    fn pcg(
2934        &self,
2935        lambda: f64,
2936        b: &[f64],
2937        warm: Option<&[f64]>,
2938    ) -> Result<(Vec<f64>, f64, usize), String> {
2939        let prec = self.build_preconditioner(lambda)?;
2940        self.pcg_with(lambda, &prec, b, warm)
2941    }
2942
2943    /// [`Core::pcg`] against a preconditioner the caller already built at this
2944    /// λ. The preconditioner depends on λ and nothing else, so a caller with
2945    /// several right-hand sides at one λ builds it once.
2946    fn pcg_with(
2947        &self,
2948        lambda: f64,
2949        prec: &Preconditioner,
2950        b: &[f64],
2951        warm: Option<&[f64]>,
2952    ) -> Result<(Vec<f64>, f64, usize), String> {
2953        let m = self.m;
2954        let b_norm = b.iter().map(|v| v * v).sum::<f64>().sqrt();
2955        if b_norm == 0.0 {
2956            return Ok((vec![0.0; m], 0.0, 0));
2957        }
2958        let mut zv = vec![0.0; m];
2959        let mut x = match warm {
2960            Some(x0) => {
2961                if x0.len() != m {
2962                    return Err(format!(
2963                        "residual cascade: warm-start length {} != system size {m}",
2964                        x0.len()
2965                    ));
2966                }
2967                x0.to_vec()
2968            }
2969            None => {
2970                prec.solve(b, &mut zv);
2971                zv.clone()
2972            }
2973        };
2974        let mut r = vec![0.0; m];
2975        self.matvec(lambda, &x, &mut r);
2976        for (ri, &bi) in r.iter_mut().zip(b.iter()) {
2977            *ri = bi - *ri;
2978        }
2979        prec.solve(&r, &mut zv);
2980        let mut p_dir = zv.clone();
2981        let mut rz: f64 = r.iter().zip(zv.iter()).map(|(&a, &c)| a * c).sum();
2982        let mut ap = vec![0.0; m];
2983        let max_iters = CG_MAX_ITERS;
2984        for iter in 0..max_iters {
2985            let r_norm = r.iter().map(|v| v * v).sum::<f64>().sqrt();
2986            if r_norm <= CG_RTOL * b_norm {
2987                return Ok((x, r_norm / b_norm, iter));
2988            }
2989            self.matvec(lambda, &p_dir, &mut ap);
2990            let pap: f64 = p_dir.iter().zip(ap.iter()).map(|(&a, &c)| a * c).sum();
2991            if !(pap.is_finite() && pap > 0.0) {
2992                return Err(format!(
2993                    "residual cascade: CG curvature breakdown (p'Ap = {pap}) at iteration {iter}"
2994                ));
2995            }
2996            let alpha = rz / pap;
2997            for j in 0..m {
2998                x[j] += alpha * p_dir[j];
2999                r[j] -= alpha * ap[j];
3000            }
3001            prec.solve(&r, &mut zv);
3002            let rz_new: f64 = r.iter().zip(zv.iter()).map(|(&a, &c)| a * c).sum();
3003            let beta = rz_new / rz;
3004            rz = rz_new;
3005            for j in 0..m {
3006                p_dir[j] = zv[j] + beta * p_dir[j];
3007            }
3008        }
3009        Err(format!(
3010            "residual cascade: CG failed to reach relative residual {CG_RTOL} within \
3011             {CG_MAX_ITERS} iterations (the coarse-space additive-Schwarz preconditioner should \
3012             make this n-independent; this indicates a degenerate design)"
3013        ))
3014    }
3015
3016    /// Expand the cached dense upper Gram + λD into a full symmetric matrix.
3017    fn dense_system(&self, lambda: f64) -> Option<Vec<f64>> {
3018        let gram = self.dense_gram.as_ref()?;
3019        let m = self.m;
3020        let mut a = vec![0.0; m * m];
3021        for i in 0..m {
3022            for j in i..m {
3023                let mut v = gram[i * m + j];
3024                if i == j {
3025                    v += lambda * self.pen_diag[i];
3026                }
3027                a[i * m + j] = v;
3028                a[j * m + i] = v;
3029            }
3030        }
3031        Some(a)
3032    }
3033
3034    /// `A = X'WX + λD` as canonical symmetric-UPPER CSC, assembled from the CSR
3035    /// design.
3036    ///
3037    /// A sparse accumulator, not a triplet list. `X` is transposed once
3038    /// (`nnz(X)` entries), then column `j` of `A` is gathered by walking the rows
3039    /// that touch column `j` and accumulating each such row's entries with
3040    /// column index `≤ j` into a dense scratch of length `m`. Peak extra memory
3041    /// is `O(m + nnz(X) + nnz(A))`. A triplet list would instead materialize
3042    /// `Σ_i k_i(k_i+1)/2` entries — the full upper outer product of every row,
3043    /// duplicates included — which scales with the ROW count rather than with
3044    /// the design and is an order of magnitude past `nnz(A)` on a wide cascade.
3045    ///
3046    /// CSR rows are built polynomial-block-first and then level by level with
3047    /// ascending `col_offset`, so a row's column indices ascend; that is what
3048    /// lets the inner scan stop at the first column past `j`.
3049    fn sparse_upper_system(&self, lambda: f64) -> Result<SparseColMat<usize, f64>, String> {
3050        let m = self.m;
3051        let n = self.w.len();
3052        let mut x_col_ptr = vec![0_usize; m + 1];
3053        for &c in &self.col_idx {
3054            x_col_ptr[c as usize + 1] += 1;
3055        }
3056        for j in 0..m {
3057            x_col_ptr[j + 1] += x_col_ptr[j];
3058        }
3059        let nnz_x = x_col_ptr[m];
3060        let mut x_rows = vec![0_u32; nnz_x];
3061        let mut x_vals = vec![0.0_f64; nnz_x];
3062        {
3063            let mut cursor = x_col_ptr.clone();
3064            for i in 0..n {
3065                for e in self.row_ptr[i]..self.row_ptr[i + 1] {
3066                    let c = self.col_idx[e] as usize;
3067                    let slot = cursor[c];
3068                    x_rows[slot] = i as u32;
3069                    x_vals[slot] = self.vals[e];
3070                    cursor[c] = slot + 1;
3071                }
3072            }
3073        }
3074        let mut acc = vec![0.0_f64; m];
3075        let mut marked = vec![false; m];
3076        let mut touched: Vec<usize> = Vec::new();
3077        let mut col_ptr: Vec<usize> = Vec::with_capacity(m + 1);
3078        col_ptr.push(0);
3079        let mut row_idx: Vec<usize> = Vec::new();
3080        let mut values: Vec<f64> = Vec::new();
3081        for j in 0..m {
3082            touched.clear();
3083            for e in x_col_ptr[j]..x_col_ptr[j + 1] {
3084                let row = x_rows[e] as usize;
3085                let weighted = self.w[row] * x_vals[e];
3086                for f in self.row_ptr[row]..self.row_ptr[row + 1] {
3087                    let c = self.col_idx[f] as usize;
3088                    if c > j {
3089                        break;
3090                    }
3091                    if !marked[c] {
3092                        marked[c] = true;
3093                        touched.push(c);
3094                    }
3095                    acc[c] += weighted * self.vals[f];
3096                }
3097            }
3098            // The prior precision is diagonal, so it only ever lands on (j, j).
3099            // The diagonal is stored unconditionally: a column the data never
3100            // touches still carries `λ d_j`, and an all-zero column would make
3101            // the symbolic factorization see a structurally singular matrix.
3102            if !marked[j] {
3103                marked[j] = true;
3104                touched.push(j);
3105            }
3106            acc[j] += lambda * self.pen_diag[j];
3107            touched.sort_unstable();
3108            for &c in &touched {
3109                let value = acc[c];
3110                acc[c] = 0.0;
3111                marked[c] = false;
3112                if !value.is_finite() {
3113                    return Err(format!(
3114                        "residual cascade: non-finite sparse normal-equation entry ({c}, {j}) = {value}"
3115                    ));
3116                }
3117                if value != 0.0 || c == j {
3118                    row_idx.push(c);
3119                    values.push(value);
3120                }
3121            }
3122            col_ptr.push(row_idx.len());
3123        }
3124        let symbolic = SymbolicSparseColMat::<usize>::new_checked(m, m, col_ptr, None, row_idx);
3125        Ok(SparseColMat::<usize, f64>::new(symbolic, values))
3126    }
3127
3128    /// Exact sparse-direct factor of `A = X'WX + λD` at this λ, or `None` when
3129    /// the AMD ordering's MEASURED fill-in exceeds [`SPARSE_FACTOR_MAX_NNZ`].
3130    ///
3131    /// The symbolic phase is run twice — once here to price the fill before
3132    /// committing, once inside the factorization — because the decision has to
3133    /// be made from `nnz(L)` and only the symbolic phase can supply it. That is
3134    /// `O(nnz(A))` against the numeric phase's `O(Σ_j nnz(L_{:,j})²)`, so it is
3135    /// not the cost being controlled.
3136    fn sparse_exact_factor(&self, lambda: f64) -> Result<Option<SparseExactFactor>, String> {
3137        let a = self.sparse_upper_system(lambda)?;
3138        let nnz_a = a.compute_nnz();
3139        let nnz_l = sparse_spd_factor_nnz(&a).map_err(|error| {
3140            format!("residual cascade: sparse normal-equation symbolic analysis failed: {error}")
3141        })?;
3142        log::info!(
3143            "[2546-FILL] m={} nnz(A)={nnz_a} nnz(L)={nnz_l} dense_upper={} \
3144             fill_vs_A={:.2} fraction_of_dense={:.4}",
3145            self.m,
3146            self.m * (self.m + 1) / 2,
3147            nnz_l as f64 / nnz_a.max(1) as f64,
3148            2.0 * nnz_l as f64 / (self.m as f64 * (self.m as f64 + 1.0))
3149        );
3150        if nnz_l > SPARSE_FACTOR_MAX_NNZ {
3151            return Ok(None);
3152        }
3153        factorize_sparse_spd_strict(&a).map(Some).map_err(|error| {
3154            format!(
3155                "residual cascade: exact sparse factorization of X'WX + {lambda} D failed \
3156                 (m = {}, nnz(A) = {nnz_a}, nnz(L) = {nnz_l}): {error}",
3157                self.m
3158            )
3159        })
3160    }
3161
3162    /// Exact log-determinant of `X'WX + λD` by dense Cholesky. Errors when
3163    /// the design is past the dense sizing cap.
3164    fn logdet_dense(&self, lambda: f64) -> Result<f64, String> {
3165        let mut a = self.dense_system(lambda).ok_or_else(|| {
3166            format!(
3167                "residual cascade: dense logdet requested past the sizing cap \
3168                 (m = {} > {DENSE_GRAM_MAX})",
3169                self.m
3170            )
3171        })?;
3172        cholesky_logdet(&mut a, self.m)
3173    }
3174
3175    /// SLQ log-determinant: exact control variate `log|P|` (the coarse-space
3176    /// additive-Schwarz preconditioner's own log-determinant — `log|A_CC|` plus
3177    /// the fine Jacobi `Σ_F log A_jj`) plus stochastic Lanczos quadrature for
3178    /// `tr log(R⁻¹ A R⁻ᵀ)`, `P = R Rᵀ`, on fixed deterministic Rademacher probes
3179    /// shared across every λ (common random numbers ⇒ the REML criterion is a
3180    /// smooth deterministic function of λ). The same coarse deflation that makes
3181    /// the PCG iteration count n-independent makes `R⁻¹ A R⁻ᵀ` uniformly
3182    /// conditioned, so the Lanczos quadrature converges in a depth-independent
3183    /// number of steps too.
3184    fn logdet_slq(&self, lambda: f64) -> Result<f64, String> {
3185        let m = self.m;
3186        let prec = self.build_preconditioner(lambda)?;
3187        let logdet = prec.logdet();
3188        // M·v = R⁻¹ A R⁻ᵀ v (eigenvalues of P^{−1/2} A P^{−1/2}) without forming M.
3189        let mut scratch_in = vec![0.0; m];
3190        let mut scratch_out = vec![0.0; m];
3191        let mut vbuf = vec![0.0; m];
3192        let mut trace_est = 0.0;
3193        let steps = SLQ_LANCZOS_STEPS.min(m);
3194        let mut basis: Vec<Vec<f64>> = Vec::with_capacity(steps);
3195        for probe in 0..SLQ_PROBES {
3196            let mut rng =
3197                SplitMix64::new(RNG_SEED ^ (probe as u64).wrapping_mul(0xD134_2543_DE82_EF95));
3198            let mut q = vec![0.0; m];
3199            for qj in q.iter_mut() {
3200                *qj = rng.next_sign();
3201            }
3202            let z_norm2 = m as f64;
3203            let inv_norm = 1.0 / (m as f64).sqrt();
3204            for qj in q.iter_mut() {
3205                *qj *= inv_norm;
3206            }
3207            // Lanczos with full reorthogonalization.
3208            basis.clear();
3209            let mut alpha = Vec::with_capacity(steps);
3210            let mut beta: Vec<f64> = Vec::with_capacity(steps);
3211            let mut q_prev: Option<Vec<f64>> = None;
3212            for _step in 0..steps {
3213                // v = R⁻¹ A R⁻ᵀ q.
3214                prec.apply_r_inv_t(&q, &mut scratch_in);
3215                self.matvec(lambda, &scratch_in, &mut scratch_out);
3216                prec.apply_r_inv(&scratch_out, &mut vbuf);
3217                let mut v: Vec<f64> = vbuf.clone();
3218                let a: f64 = v.iter().zip(q.iter()).map(|(&x, &y)| x * y).sum();
3219                alpha.push(a);
3220                for j in 0..m {
3221                    v[j] -= a * q[j];
3222                }
3223                if let Some(prev) = &q_prev {
3224                    let b_prev = beta.last().copied().unwrap_or(0.0);
3225                    for j in 0..m {
3226                        v[j] -= b_prev * prev[j];
3227                    }
3228                }
3229                // Full reorthogonalization against the stored basis.
3230                basis.push(q.clone());
3231                for qb in &basis {
3232                    let proj: f64 = v.iter().zip(qb.iter()).map(|(&x, &y)| x * y).sum();
3233                    for j in 0..m {
3234                        v[j] -= proj * qb[j];
3235                    }
3236                }
3237                let b: f64 = v.iter().map(|x| x * x).sum::<f64>().sqrt();
3238                if !(b.is_finite()) {
3239                    return Err("residual cascade: Lanczos breakdown (non-finite norm)".into());
3240                }
3241                if b < 1e-13 {
3242                    break;
3243                }
3244                beta.push(b);
3245                q_prev = Some(std::mem::replace(&mut q, v));
3246                for qj in q.iter_mut() {
3247                    *qj /= b;
3248                }
3249            }
3250            beta.truncate(alpha.len().saturating_sub(1));
3251            let (theta, tau) = symmetric_tridiagonal_eigen(&alpha, &beta)?;
3252            let mut quad = 0.0;
3253            for (&t, &w0) in theta.iter().zip(tau.iter()) {
3254                // A Ritz value of an SPD operator is positive; a non-positive one
3255                // says the system is not PD, and that is the whole test.
3256                if !(t.is_finite() && t > 0.0) {
3257                    return Err(format!(
3258                        "residual cascade: non-positive Ritz value {t} in SLQ (system not PD)"
3259                    ));
3260                }
3261                quad += w0 * w0 * t.ln();
3262            }
3263            trace_est += z_norm2 * quad;
3264        }
3265        Ok(logdet + trace_est / SLQ_PROBES as f64)
3266    }
3267
3268    /// Log-determinant through the most exact route available, with the route
3269    /// it took.
3270    ///
3271    /// `sparse` is the factor the caller has already built at this λ (see
3272    /// [`Self::sparse_exact_factor`]); one factorization serves both this
3273    /// determinant and every subsequent prediction solve, so it is threaded in
3274    /// rather than rebuilt here.
3275    fn logdet_with(
3276        &self,
3277        lambda: f64,
3278        sparse: Option<&SparseExactFactor>,
3279    ) -> Result<(f64, LogdetMethod), String> {
3280        if self.dense_gram.is_some() {
3281            return Ok((self.logdet_dense(lambda)?, LogdetMethod::DenseExact));
3282        }
3283        if let Some(factor) = sparse {
3284            // `2·Σ log L_jj` from an exact factorization of the very matrix
3285            // whose determinant is asked for. Not iterative, not stochastic.
3286            let logdet = logdet_from_factor(factor).map_err(|error| {
3287                format!("residual cascade: sparse log-determinant unavailable: {error}")
3288            })?;
3289            return Ok((logdet, LogdetMethod::SparseExact));
3290        }
3291        // The one route left that is not exact, and the only place a stochastic
3292        // determinant survives: the AMD ordering's fill-in on this design does
3293        // not fit `SPARSE_FACTOR_MAX_NNZ`, so no exact factorization exists to
3294        // read a diagonal off. It is REPORTED as `Slq` on the fit's certificate
3295        // and it underwrites nothing — `fit_reml` refuses at a far smaller
3296        // width, so no score sign, KKT root, or candidate ordering can ever
3297        // rest on this value.
3298        Ok((self.logdet_slq(lambda)?, LogdetMethod::Slq))
3299    }
3300
3301    /// Coefficient solve at λ: dense Cholesky when cached, else certified PCG.
3302    fn solve_coeff(
3303        &self,
3304        lambda: f64,
3305        b: &[f64],
3306        warm: Option<&[f64]>,
3307    ) -> Result<(Vec<f64>, f64, usize), String> {
3308        // A core rebuilt from a persisted state carries no training design, only
3309        // the factored precision `L` of `A = X'WX + λD` at the fit's λ. Replay
3310        // the solve through it (exact — predict always solves at that same λ).
3311        if let Some(l) = &self.predict_chol {
3312            return Ok((chol_solve(l, self.m, b), 0.0, 0));
3313        }
3314        if let Some(mut a) = self.dense_system(lambda) {
3315            cholesky_logdet(&mut a, self.m)?;
3316            return Ok((chol_solve(&a, self.m, b), 0.0, 0));
3317        }
3318        self.pcg(lambda, b, warm)
3319    }
3320
3321    /// Assemble the lower Cholesky factor `L` of `A = X'WX + λD` as a dense
3322    /// `m × m` row-major matrix — the factored precision a persisted predict
3323    /// replays its posterior-variance solve through. Uses the cached dense Gram
3324    /// when present; otherwise scatters the CSR row outer products into the
3325    /// upper triangle (one O(nnz·q) pass), the same assembly `build` uses under
3326    /// the sizing cap, just without the cap. Factoring is O(m³) — paid once at
3327    /// snapshot time, not per predict.
3328    fn assemble_predict_factor(&self, lambda: f64) -> Result<Vec<f64>, String> {
3329        let m = self.m;
3330        let mut a = vec![0.0_f64; m * m];
3331        if let Some(gram) = &self.dense_gram {
3332            for i in 0..m {
3333                for j in i..m {
3334                    let v = gram[i * m + j];
3335                    a[i * m + j] = v;
3336                    a[j * m + i] = v;
3337                }
3338            }
3339        } else {
3340            for i in 0..self.w.len() {
3341                let lo = self.row_ptr[i];
3342                let hi = self.row_ptr[i + 1];
3343                for ea in lo..hi {
3344                    let ca = self.col_idx[ea] as usize;
3345                    let va = self.w[i] * self.vals[ea];
3346                    for eb in ea..hi {
3347                        let cb = self.col_idx[eb] as usize;
3348                        a[ca * m + cb] += va * self.vals[eb];
3349                    }
3350                }
3351            }
3352            // Mirror the upper triangle into the lower.
3353            for i in 0..m {
3354                for j in i + 1..m {
3355                    a[j * m + i] = a[i * m + j];
3356                }
3357            }
3358        }
3359        for (i, d) in self.pen_diag.iter().enumerate() {
3360            a[i * m + i] += lambda * d;
3361        }
3362        cholesky_logdet(&mut a, m)?;
3363        Ok(a)
3364    }
3365
3366    /// Penalized residual quadratic at a solution: `y'Wy − c'X'Wy`.
3367    fn rss_pen(&self, coeff: &[f64]) -> f64 {
3368        let mut quad = 0.0;
3369        for (c, r) in coeff.iter().zip(self.rhs.iter()) {
3370            quad += c * r;
3371        }
3372        self.ytwy - quad
3373    }
3374
3375    /// Number of unpenalized (polynomial) columns.
3376    fn nullity(&self) -> usize {
3377        self.dim + 1
3378    }
3379
3380    /// Working residual `r_i = y_i − (Xc)_i`.
3381    fn residuals(&self, coeff: &[f64]) -> Vec<f64> {
3382        let n = self.y.len();
3383        let mut r = Vec::with_capacity(n);
3384        for i in 0..n {
3385            let mut fit = 0.0;
3386            for e in self.row_ptr[i]..self.row_ptr[i + 1] {
3387                fit += self.vals[e] * coeff[self.col_idx[e] as usize];
3388            }
3389            r.push(self.y[i] - fit);
3390        }
3391        r
3392    }
3393}
3394
3395// ──────────────────── symmetric tridiagonal eigensolver ─────────────────────
3396
3397/// Eigenvalues and FIRST eigenvector components of a symmetric tridiagonal
3398/// matrix (diag `d`, off-diagonal `e`), by implicit-shift QL with the
3399/// first-row vector carried through the rotations — exactly what Lanczos
3400/// quadrature needs.
3401fn symmetric_tridiagonal_eigen(d: &[f64], e: &[f64]) -> Result<(Vec<f64>, Vec<f64>), String> {
3402    let n = d.len();
3403    if n == 0 {
3404        return Ok((Vec::new(), Vec::new()));
3405    }
3406    let mut diag = d.to_vec();
3407    let mut off = vec![0.0; n];
3408    off[..n - 1].copy_from_slice(&e[..n - 1]);
3409    let mut first = vec![0.0; n];
3410    first[0] = 1.0;
3411    for l in 0..n {
3412        let mut iter = 0;
3413        loop {
3414            // Find a negligible off-diagonal to split at.
3415            let mut msplit = n - 1;
3416            for mm in l..n - 1 {
3417                let dd = diag[mm].abs() + diag[mm + 1].abs();
3418                if off[mm].abs() <= f64::EPSILON * dd {
3419                    msplit = mm;
3420                    break;
3421                }
3422            }
3423            if msplit == l {
3424                break;
3425            }
3426            iter += 1;
3427            if iter > 60 {
3428                return Err("residual cascade: tridiagonal QL failed to converge".into());
3429            }
3430            let mut g = (diag[l + 1] - diag[l]) / (2.0 * off[l]);
3431            let mut r = g.hypot(1.0);
3432            g = diag[msplit] - diag[l] + off[l] / (g + r.copysign(g));
3433            let (mut s, mut c) = (1.0, 1.0);
3434            let mut p = 0.0;
3435            let mut broke_early = false;
3436            for i in (l..msplit).rev() {
3437                let mut f = s * off[i];
3438                let b = c * off[i];
3439                r = f.hypot(g);
3440                off[i + 1] = r;
3441                if r == 0.0 {
3442                    diag[i + 1] -= p;
3443                    off[msplit] = 0.0;
3444                    broke_early = true;
3445                    break;
3446                }
3447                s = f / r;
3448                c = g / r;
3449                g = diag[i + 1] - p;
3450                r = (diag[i] - g) * s + 2.0 * c * b;
3451                p = s * r;
3452                diag[i + 1] = g + p;
3453                g = c * r - b;
3454                // Carry the first-row eigenvector components.
3455                f = first[i + 1];
3456                first[i + 1] = s * first[i] + c * f;
3457                first[i] = c * first[i] - s * f;
3458            }
3459            if broke_early {
3460                continue;
3461            }
3462            diag[l] -= p;
3463            off[l] = g;
3464            off[msplit] = 0.0;
3465        }
3466    }
3467    Ok((diag, first))
3468}
3469
3470// ───────────────────────────── net construction ─────────────────────────────
3471
3472/// Extend a nested net to covering radius `h` over the DOMAIN: first every data
3473/// point further than `h` from the (seeded) net becomes a new center, then every
3474/// cell of the `h`-grid over the bounding box `[0, box_hi]` whose centre is not
3475/// yet within `h` of the net is filled with a synthetic center. O((n + box
3476/// cells)·3^d). Returns the new centers.
3477///
3478/// Covering the box, not merely the data cloud, is what the multilevel Wendland
3479/// norm-equivalence (Narcowich–Ward inverse estimates + Le Gia–Wendland
3480/// multilevel stability) actually requires: the nested centres must be
3481/// quasi-uniform over the domain Ω. In data-dense regions every cell is already
3482/// covered by a data center, so the fill is a no-op there; in a data void it
3483/// plants the fine centres whose coefficients carry no data and revert to the
3484/// prior — the mechanism by which the posterior mean bridges a gap (coarse
3485/// data-pinned bumps) while the posterior variance GROWS into it (fine void
3486/// bumps the data cannot pin). The synthetic centres carry (almost) no data
3487/// rows, so their Gram diagonal is ~0 and they land in the penalty-dominated
3488/// fine block where the Jacobi preconditioner is exact — they neither perturb
3489/// the coarse factorization nor the n-independent iteration count.
3490fn extend_net(
3491    net: &mut Vec<[f64; 3]>,
3492    points: &[[f64; 3]],
3493    dim: usize,
3494    h: f64,
3495    box_hi: &[f64; 3],
3496) -> Vec<[f64; 3]> {
3497    let mut grid = HashGrid::new(h, dim);
3498    for (idx, c) in net.iter().enumerate() {
3499        grid.insert(idx as u32, c);
3500    }
3501    let h2 = h * h;
3502    let mut new_centers = Vec::new();
3503    let try_add = |net: &mut Vec<[f64; 3]>,
3504                   grid: &mut HashGrid,
3505                   new_centers: &mut Vec<[f64; 3]>,
3506                   p: &[f64; 3]| {
3507        let mut covered = false;
3508        grid.for_neighbors(p, |j| {
3509            if !covered && dist2(p, &net[j as usize], dim) <= h2 {
3510                covered = true;
3511            }
3512        });
3513        if !covered {
3514            let idx = net.len() as u32;
3515            net.push(*p);
3516            grid.insert(idx, p);
3517            new_centers.push(*p);
3518        }
3519    };
3520    for p in points {
3521        try_add(net, &mut grid, &mut new_centers, p);
3522        if net.len() > MAX_CENTERS {
3523            return new_centers;
3524        }
3525    }
3526    // Fill the bounding box so the net covers the domain, not just the data.
3527    //
3528    // The box has ~`(box_hi/h)^dim` cells, so the fill cost grows like
3529    // `(2^l)^dim` as the covering radius `h = h₀·2^{-l}` shrinks with the
3530    // level `l`. At fine levels below the data spacing that is an explosion
3531    // (every sub-data-spacing cell of the whole domain becomes a synthetic
3532    // center), which is unbounded work the caller never needs: once the net
3533    // crosses `MAX_CENTERS` the build path errors and the auto-route's typed
3534    // next-level assessment reports center-capacity underresolution. So
3535    // cap the fill IN the loop — stop planting synthetic centers the moment
3536    // the net exceeds the cap rather than materializing the entire fine-level
3537    // box first. Coarse levels (few cells, never near the cap) keep the full
3538    // quasi-uniform domain fill and the polynomial-bridge gap behavior intact.
3539    let mut cells = [1_i64; 3];
3540    for a in 0..dim {
3541        cells[a] = (box_hi[a] / h).ceil() as i64 + 1;
3542    }
3543    let mut c = [0.0_f64; 3];
3544    'fill: for i0 in 0..cells[0] {
3545        c[0] = (i0 as f64 + 0.5) * h;
3546        for i1 in 0..cells[1] {
3547            if dim > 1 {
3548                c[1] = (i1 as f64 + 0.5) * h;
3549            }
3550            for i2 in 0..cells[2] {
3551                if dim > 2 {
3552                    c[2] = (i2 as f64 + 0.5) * h;
3553                }
3554                try_add(net, &mut grid, &mut new_centers, &c);
3555                if net.len() > MAX_CENTERS {
3556                    break 'fill;
3557                }
3558            }
3559        }
3560    }
3561    new_centers
3562}
3563
3564/// Bit-exact key of a scaled center, so a planned selection can be checked
3565/// against the candidates the nested net offers without a tolerance (both sides
3566/// are copies of the same `extend_net` output, never a recomputation).
3567fn center_key(center: &[f64; 3]) -> [u64; 3] {
3568    [
3569        center[0].to_bits(),
3570        center[1].to_bits(),
3571        center[2].to_bits(),
3572    ]
3573}
3574
3575/// One level of the cascade ladder: its resolution exponent `e` (radius
3576/// `h = h₀·2⁻ᵉ`) and, when a capacity budget forced the level to take only part
3577/// of what the net offered, the exact centers it carries.
3578#[derive(Clone, Debug)]
3579struct LevelPlan {
3580    exponent: f64,
3581    /// `None` is the complete dyadic level: every center the nested net plants
3582    /// at this radius.
3583    centers: Option<Vec<[f64; 3]>>,
3584}
3585
3586impl ResidualCascadeDesign {
3587    /// Build the cascade design: validate, scale by the metric, grow `levels`
3588    /// nested nets, and assemble the sparse design plus its sufficient
3589    /// statistics in O(n·(levels + 3^d)).
3590    ///
3591    /// `xs` holds one slice per axis (2 or 3 of them), `metric` the positive
3592    /// per-axis scaling of the learned metric, `sobolev_s` the Sobolev order
3593    /// of the equivalent (semi)norm — must satisfy `d/2 < s ≤ (d+3)/2` (the
3594    /// Wendland-(3,1) native smoothness).
3595    pub fn build(
3596        xs: &[&[f64]],
3597        y: &[f64],
3598        w: &[f64],
3599        metric: &[f64],
3600        sobolev_s: f64,
3601        levels: usize,
3602    ) -> Result<Self, String> {
3603        if levels == 0 || levels > MAX_LEVELS {
3604            return Err(format!(
3605                "residual cascade: levels must be in 1..={MAX_LEVELS}, got {levels}"
3606            ));
3607        }
3608        let level_exponents: Vec<f64> = (0..levels).map(|level| level as f64).collect();
3609        Self::build_at_exponents(xs, y, w, metric, sobolev_s, &level_exponents)
3610    }
3611
3612    /// Shared constructor for the dyadic ladder. An exponent `e` means
3613    /// `h = h₀·2⁻ᵉ`; [`Self::build`] supplies the one production ladder,
3614    /// `0, 1, …, levels−1`. The causal rank-boundary regression also evaluates
3615    /// fractional exponents through this same constructor, so its sub-level
3616    /// counterfactual cannot drift from the production basis construction.
3617    fn build_at_exponents(
3618        xs: &[&[f64]],
3619        y: &[f64],
3620        w: &[f64],
3621        metric: &[f64],
3622        sobolev_s: f64,
3623        level_exponents: &[f64],
3624    ) -> Result<Self, String> {
3625        let plan: Vec<LevelPlan> = level_exponents
3626            .iter()
3627            .map(|&exponent| LevelPlan {
3628                exponent,
3629                centers: None,
3630            })
3631            .collect();
3632        Self::build_from_plan(xs, y, w, metric, sobolev_s, &plan)
3633    }
3634
3635    /// Constructor the whole crate builds through: every level names its
3636    /// resolution exponent and, when a capacity budget forced it to take only
3637    /// part of the net's candidates, the EXACT centers it carries. Holding the
3638    /// selection in the plan rather than re-deriving it is what makes a partial
3639    /// level reproducible: the design is a pure function of the plan, so a
3640    /// re-build at the same plan is the same basis.
3641    fn build_from_plan(
3642        xs: &[&[f64]],
3643        y: &[f64],
3644        w: &[f64],
3645        metric: &[f64],
3646        sobolev_s: f64,
3647        plan: &[LevelPlan],
3648    ) -> Result<Self, String> {
3649        let levels = plan.len();
3650        let level_exponents: Vec<f64> = plan.iter().map(|level| level.exponent).collect();
3651        let level_exponents = level_exponents.as_slice();
3652        if levels == 0 || levels > MAX_LEVELS {
3653            return Err(format!(
3654                "residual cascade: levels must be in 1..={MAX_LEVELS}, got {levels}"
3655            ));
3656        }
3657        if level_exponents[0] != 0.0
3658            || level_exponents
3659                .iter()
3660                .any(|exponent| !exponent.is_finite() || *exponent < 0.0)
3661            || level_exponents.windows(2).any(|pair| pair[0] >= pair[1])
3662        {
3663            return Err(format!(
3664                "residual cascade: resolution exponents must start at zero and increase \
3665                 strictly, got {level_exponents:?}"
3666            ));
3667        }
3668        let dim = xs.len();
3669        if !(dim == 2 || dim == 3) {
3670            return Err(format!(
3671                "residual cascade: built for scattered 2-3D smooths, got {dim} axes"
3672            ));
3673        }
3674        let n = y.len();
3675        if w.len() != n || xs.iter().any(|x| x.len() != n) {
3676            return Err(format!(
3677                "residual cascade: length mismatch (y={n}, w={}, axes={:?})",
3678                w.len(),
3679                xs.iter().map(|x| x.len()).collect::<Vec<_>>()
3680            ));
3681        }
3682        if n <= dim + 1 {
3683            return Err(format!(
3684                "residual cascade: needs more than {} rows for the profiled REML degrees of \
3685                 freedom, got {n}",
3686                dim + 1
3687            ));
3688        }
3689        if metric.len() != dim || metric.iter().any(|&s| !(s.is_finite() && s > 0.0)) {
3690            return Err(format!(
3691                "residual cascade: metric must be {dim} finite positive scales, got {metric:?}"
3692            ));
3693        }
3694        if !(sobolev_s > dim as f64 / 2.0 && sobolev_s <= (dim as f64 + 3.0) / 2.0) {
3695            return Err(format!(
3696                "residual cascade: sobolev_s must lie in (d/2, (d+3)/2] = ({}, {}] for the \
3697                 Wendland-(3,1) bump, got {sobolev_s}",
3698                dim as f64 / 2.0,
3699                (dim as f64 + 3.0) / 2.0
3700            ));
3701        }
3702        for i in 0..n {
3703            if !(y[i].is_finite() && w[i].is_finite() && w[i] > 0.0)
3704                || xs.iter().any(|x| !x[i].is_finite())
3705            {
3706                return Err(format!(
3707                    "residual cascade: non-finite or non-positive input at row {i}"
3708                ));
3709            }
3710        }
3711        // Scaled, corner-shifted coordinates.
3712        let mut z_lo = [f64::INFINITY; 3];
3713        let mut z_hi = [f64::NEG_INFINITY; 3];
3714        for a in 0..dim {
3715            for &v in xs[a] {
3716                let s = metric[a] * v;
3717                z_lo[a] = z_lo[a].min(s);
3718                z_hi[a] = z_hi[a].max(s);
3719            }
3720        }
3721        let mut z_range = [1.0_f64; 3];
3722        let mut max_range = 0.0_f64;
3723        for a in 0..dim {
3724            if !(z_hi[a] > z_lo[a]) {
3725                return Err(format!(
3726                    "residual cascade: degenerate axis {a} bounding box [{}, {}]",
3727                    z_lo[a], z_hi[a]
3728                ));
3729            }
3730            z_range[a] = z_hi[a] - z_lo[a];
3731            max_range = max_range.max(z_range[a]);
3732        }
3733        for a in dim..3 {
3734            z_lo[a] = 0.0;
3735        }
3736        let z: Vec<[f64; 3]> = (0..n)
3737            .map(|i| {
3738                let mut p = [0.0_f64; 3];
3739                for a in 0..dim {
3740                    p[a] = metric[a] * xs[a][i] - z_lo[a];
3741                }
3742                p
3743            })
3744            .collect();
3745        let mut metric3 = [1.0_f64; 3];
3746        metric3[..dim].copy_from_slice(metric);
3747
3748        let h0 = H0_FRACTION * max_range;
3749        let mut net: Vec<[f64; 3]> = Vec::new();
3750        let mut level_specs = Vec::with_capacity(levels);
3751        let mut col = dim + 1;
3752        let mut pen_logdet_const = 0.0;
3753        for (l, planned) in plan.iter().enumerate() {
3754            let exponent = planned.exponent;
3755            let h = h0 * 0.5_f64.powf(exponent);
3756            // The candidate set is derived the SAME way for a complete and a
3757            // partial level — one `extend_net` against the net built so far —
3758            // so a selection can only ever name centers the nested net would
3759            // have planted anyway. A partial level then plants exactly its
3760            // selection, leaving the rest of the candidates for a later level
3761            // to cover at a finer radius.
3762            let mut probe = net.clone();
3763            let candidates = extend_net(&mut probe, &z, dim, h, &z_range);
3764            let new_centers = match &planned.centers {
3765                None => {
3766                    net = probe;
3767                    candidates
3768                }
3769                Some(selection) => {
3770                    let admissible: std::collections::HashSet<[u64; 3]> =
3771                        candidates.iter().map(center_key).collect();
3772                    if selection.is_empty() {
3773                        return Err(format!(
3774                            "residual cascade: level {l} selects no centers at exponent {exponent}"
3775                        ));
3776                    }
3777                    let mut seen: std::collections::HashSet<[u64; 3]> =
3778                        std::collections::HashSet::with_capacity(selection.len());
3779                    for center in selection {
3780                        let key = center_key(center);
3781                        if !admissible.contains(&key) || !seen.insert(key) {
3782                            return Err(format!(
3783                                "residual cascade: level {l} selects {center:?}, which the nested \
3784                                 net does not offer as a distinct candidate at exponent {exponent}"
3785                            ));
3786                        }
3787                    }
3788                    net.extend_from_slice(selection);
3789                    selection.clone()
3790                }
3791            };
3792            if net.len() > MAX_CENTERS {
3793                return Err(format!(
3794                    "residual cascade: center cap {MAX_CENTERS} exceeded at level {l}"
3795                ));
3796            }
3797            let weight = level_weight(exponent, sobolev_s, dim);
3798            pen_logdet_const += new_centers.len() as f64 * weight.ln();
3799            let delta = OVERLAP * h;
3800            let mut grid = HashGrid::new(delta, dim);
3801            for (j, c) in new_centers.iter().enumerate() {
3802                grid.insert(j as u32, c);
3803            }
3804            let col_offset = col;
3805            col += new_centers.len();
3806            level_specs.push(Level {
3807                h,
3808                delta,
3809                weight,
3810                centers: new_centers,
3811                col_offset,
3812                grid,
3813            });
3814        }
3815        let m = col;
3816
3817        // CSR assembly + sufficient statistics in one pass.
3818        let mut row_ptr = Vec::with_capacity(n + 1);
3819        row_ptr.push(0_usize);
3820        let mut col_idx: Vec<u32> = Vec::new();
3821        let mut vals: Vec<f64> = Vec::new();
3822        let mut rhs = vec![0.0_f64; m];
3823        let mut gram_diag = vec![0.0_f64; m];
3824        let mut ytwy = 0.0_f64;
3825        let probe_core = CoreScaffold {
3826            dim,
3827            z_range,
3828            levels: &level_specs,
3829        };
3830        for i in 0..n {
3831            let row = probe_core.basis_row(&z[i]);
3832            for &(c, v) in &row {
3833                col_idx.push(c as u32);
3834                vals.push(v);
3835                rhs[c] += w[i] * y[i] * v;
3836                gram_diag[c] += w[i] * v * v;
3837            }
3838            ytwy += w[i] * y[i] * y[i];
3839            row_ptr.push(col_idx.len());
3840        }
3841        let mut pen_diag = vec![0.0_f64; m];
3842        for level in &level_specs {
3843            for j in 0..level.centers.len() {
3844                pen_diag[level.col_offset + j] = level.weight;
3845            }
3846        }
3847
3848        // Dense Gram cache under the sizing cap: O(n·q²) scatter of row outer
3849        // products into the upper triangle.
3850        let dense_gram = if m <= DENSE_GRAM_MAX {
3851            let mut gram = vec![0.0_f64; m * m];
3852            for i in 0..n {
3853                let lo = row_ptr[i];
3854                let hi = row_ptr[i + 1];
3855                for ea in lo..hi {
3856                    let ca = col_idx[ea] as usize;
3857                    let va = w[i] * vals[ea];
3858                    for eb in ea..hi {
3859                        gram[ca * m + col_idx[eb] as usize] += va * vals[eb];
3860                    }
3861                }
3862            }
3863            Some(gram)
3864        } else {
3865            None
3866        };
3867
3868        Ok(ResidualCascadeDesign {
3869            core: Arc::new(Core {
3870                dim,
3871                metric: metric3,
3872                z_lo,
3873                z_range,
3874                sobolev_s,
3875                levels: level_specs,
3876                net,
3877                m,
3878                row_ptr,
3879                col_idx,
3880                vals,
3881                w: w.to_vec(),
3882                y: y.to_vec(),
3883                z,
3884                rhs,
3885                ytwy,
3886                gram_diag,
3887                pen_diag,
3888                pen_logdet_const,
3889                dense_gram,
3890                predict_chol: None,
3891            }),
3892        })
3893    }
3894
3895    /// Number of resolution levels.
3896    pub fn num_levels(&self) -> usize {
3897        self.core.levels.len()
3898    }
3899
3900    /// Aspect ratio of the metric-scaled point cloud: the ratio of the largest
3901    /// to smallest per-axis standard deviation of the scaled coordinates `z`.
3902    /// This is the metric-condition measure the quasi-uniformity guard (issue
3903    /// #1032, caveat 2) keys on — see `QUASI_UNIFORMITY_MAX_ASPECT`. A value
3904    /// near 1 is an isotropic (benign) cloud; a large value means the metric
3905    /// has collapsed the data onto a lower-dimensional sheet in `z`, breaking
3906    /// the BPX n-independent iteration bound.
3907    pub fn metric_scaled_aspect_ratio(&self) -> f64 {
3908        let dim = self.core.dim;
3909        let n = self.core.z.len();
3910        if dim == 0 || n == 0 {
3911            return 1.0;
3912        }
3913        let mut mean = [0.0_f64; 3];
3914        for p in &self.core.z {
3915            for a in 0..dim {
3916                mean[a] += p[a];
3917            }
3918        }
3919        for m in mean.iter_mut().take(dim) {
3920            *m /= n as f64;
3921        }
3922        let mut var = [0.0_f64; 3];
3923        for p in &self.core.z {
3924            for a in 0..dim {
3925                let d = p[a] - mean[a];
3926                var[a] += d * d;
3927            }
3928        }
3929        let mut sd_lo = f64::INFINITY;
3930        let mut sd_hi = 0.0_f64;
3931        for v in var.iter().take(dim) {
3932            let sd = (v / n as f64).sqrt();
3933            sd_lo = sd_lo.min(sd);
3934            sd_hi = sd_hi.max(sd);
3935        }
3936        if !(sd_lo > 0.0 && sd_lo.is_finite()) {
3937            // A collapsed axis (zero scaled spread) is maximally degenerate.
3938            return f64::INFINITY;
3939        }
3940        sd_hi / sd_lo
3941    }
3942
3943    /// Quasi-uniformity certificate (issue #1032, caveat 2): `true` iff the
3944    /// metric-scaled cloud is isotropic enough that the BPX n-independent CG
3945    /// iteration bound is trustworthy. When this returns `false`, automatic
3946    /// fitting must return a typed refusal rather than pay an iterative solve
3947    /// whose iteration count is no longer n-independent. The CG residual
3948    /// certificate would still *catch* a mis-solve at `CG_MAX_ITERS`, but
3949    /// the guard prevents the silent O(n·iters) blow-up up front.
3950    pub fn quasi_uniformity_certified(&self) -> bool {
3951        self.metric_scaled_aspect_ratio() <= QUASI_UNIFORMITY_MAX_ASPECT
3952    }
3953
3954    /// Total centers across all levels.
3955    pub fn num_centers(&self) -> usize {
3956        self.core.m - self.core.nullity()
3957    }
3958
3959    /// NEW centers of one level in ORIGINAL (unscaled) coordinates.
3960    pub fn centers(&self, level: usize) -> Vec<Vec<f64>> {
3961        let lv = &self.core.levels[level];
3962        lv.centers
3963            .iter()
3964            .map(|c| {
3965                (0..self.core.dim)
3966                    .map(|a| (c[a] + self.core.z_lo[a]) / self.core.metric[a])
3967                    .collect()
3968            })
3969            .collect()
3970    }
3971
3972    /// Sparse basis row at a raw point, as (column, value) pairs sorted by
3973    /// column within each block — the exact row the fit used for training
3974    /// rows, exposed so oracles can assemble the dense system independently.
3975    pub fn basis_row(&self, x: &[f64]) -> Result<Vec<(usize, f64)>, String> {
3976        self.check_point(x)?;
3977        Ok(self.core.basis_row_scaled(&self.core.scale_point(x)))
3978    }
3979
3980    fn check_point(&self, x: &[f64]) -> Result<(), String> {
3981        if x.len() != self.core.dim || x.iter().any(|v| !v.is_finite()) {
3982            return Err(format!(
3983                "residual cascade: point must be {} finite coordinates, got {x:?}",
3984                self.core.dim
3985            ));
3986        }
3987        Ok(())
3988    }
3989
3990    /// Exact penalty quadratic `c'Dc` (unit-λ multilevel prior energy).
3991    pub fn penalty_value(&self, coeff: &[f64]) -> Result<f64, String> {
3992        if coeff.len() != self.core.m {
3993            return Err(format!(
3994                "residual cascade: coefficient length {} != {}",
3995                coeff.len(),
3996                self.core.m
3997            ));
3998        }
3999        Ok(coeff
4000            .iter()
4001            .zip(self.core.pen_diag.iter())
4002            .map(|(&c, &d)| d * c * c)
4003            .sum())
4004    }
4005
4006    /// Profiled-σ² REML criterion at `log λ` (differences across λ are
4007    /// exact-real certifiable on the dense route; one fixed numerical spectral
4008    /// quadrature is used for diagnostic evaluation past the cap).
4009    pub fn criterion(&self, log_lambda: f64) -> Result<f64, String> {
4010        Ok(self.core.reml_profile()?.evaluate(log_lambda)?.jet.value)
4011    }
4012
4013    /// Fit at a FIXED `log λ`, with σ² either supplied or profiled.
4014    pub fn fit_at(
4015        &self,
4016        log_lambda: f64,
4017        sigma2: Option<f64>,
4018    ) -> Result<ResidualCascadeFit, String> {
4019        self.fit_at_with_warm(log_lambda, sigma2, None, None)
4020    }
4021
4022    fn fit_at_with_warm(
4023        &self,
4024        log_lambda: f64,
4025        sigma2: Option<f64>,
4026        warm: Option<&[f64]>,
4027        profile_normalized_logdet: Option<f64>,
4028    ) -> Result<ResidualCascadeFit, String> {
4029        let core = &self.core;
4030        let lambda = gam_problem::checked_exp_log_strength(log_lambda)
4031            .map_err(|error| format!("residual cascade: {error}"))?;
4032        // Exact sparse-direct factor at this λ, past the dense Gram cache. One
4033        // factorization serves the log-determinant AND every prediction solve
4034        // this fit will later perform, so it is built once here. A core rebuilt
4035        // from a persisted state has no CSR design to assemble it from and
4036        // carries its own dense factor instead.
4037        let sparse_factor = if core.dense_gram.is_none() && core.predict_chol.is_none() {
4038            core.sparse_exact_factor(lambda)?.map(Arc::new)
4039        } else {
4040            None
4041        };
4042        let (coeff, rel_res, iters) = core.solve_coeff(lambda, &core.rhs, warm)?;
4043        let rss_pen = core.rss_pen(&coeff);
4044        let dof = (core.y.len() - core.nullity()) as f64;
4045        let sigma2 = match sigma2 {
4046            Some(s) => {
4047                if !(s.is_finite() && s > 0.0) {
4048                    return Err(format!("residual cascade: invalid sigma2 {s}"));
4049                }
4050                s
4051            }
4052            None => {
4053                if !(rss_pen > 0.0) {
4054                    return Err(format!(
4055                        "residual cascade: degenerate penalized residual {rss_pen}"
4056                    ));
4057                }
4058                rss_pen / dof
4059            }
4060        };
4061        let r = (core.m - core.nullity()) as f64;
4062        let (logdet, logdet_method) = match profile_normalized_logdet {
4063            // Supplied only by `fit_reml`, which refuses unless the exact
4064            // lambda-independent Schur spectrum was formed — so a normalized
4065            // logdet arriving here is exact dense linear algebra by
4066            // construction, at every width the certified route admits.
4067            Some(normalized) => (
4068                normalized + r * log_lambda + core.pen_logdet_const,
4069                LogdetMethod::DenseExact,
4070            ),
4071            None => core.logdet_with(lambda, sparse_factor.as_deref())?,
4072        };
4073        // Full restricted log-likelihood at this (λ, σ²) up to λ- and σ-free
4074        // constants; at the profiled σ̂² the quadratic collapses to `dof`.
4075        let restricted_loglik = -0.5
4076            * (logdet - r * log_lambda - core.pen_logdet_const
4077                + dof * sigma2.ln()
4078                + rss_pen / sigma2);
4079        let predict_chol = if core.dense_gram.is_some() {
4080            Some(core.assemble_predict_factor(lambda)?)
4081        } else {
4082            None
4083        };
4084        Ok(ResidualCascadeFit {
4085            core: Arc::clone(&self.core),
4086            training_sample_size: std::num::NonZeroUsize::new(core.y.len())
4087                .expect("ResidualCascadeDesign requires training rows"),
4088            predict_chol,
4089            predict_sparse: sparse_factor,
4090            coeff,
4091            log_lambda,
4092            sigma2,
4093            restricted_loglik,
4094            rss_pen,
4095            certificate: CascadeCertificate {
4096                solve_rel_residual: rel_res,
4097                solve_iters: iters,
4098                logdet_method,
4099            },
4100            refinement: None,
4101        })
4102    }
4103
4104    /// Fit with `log λ` selected by the profiled REML criterion. Every
4105    /// stationary interval in the bounded domain is isolated from analytic
4106    /// derivative enclosures, refined by safeguarded Newton/bisection, and
4107    /// compared with both exact boundary candidates.
4108    ///
4109    /// Automatic selection is limited to designs whose λ-independent Schur
4110    /// spectrum can be formed, i.e. inside `CERTIFIED_SPECTRUM_MAX` — NOT to
4111    /// designs that carry a dense Gram cache. The two used to be the same gate,
4112    /// which meant a cascade whose refinement legitimately crossed
4113    /// `DENSE_GRAM_MAX` could be fitted but never certified, and so could not
4114    /// finish at all (#2546). The Gram is a cache; the spectrum is the proof.
4115    ///
4116    /// Past the spectrum budget there is no exact-real enclosure of the score,
4117    /// even when the separate β-seeded residual Krylov space closes: a pointwise
4118    /// solve, factorization, or quadrature value cannot certify a score sign, a
4119    /// stationary point, or a global ordering. Returning a typed refusal is the
4120    /// only sound result there; [`Self::fit_at`] remains available when the user
4121    /// explicitly fixes the smoothing parameter.
4122    pub fn fit_reml(&self) -> Result<ResidualCascadeFit, ResidualCascadeError> {
4123        if self.core.m > CERTIFIED_SPECTRUM_MAX {
4124            return Err(ResidualCascadeError::RemlScoreProofUnavailable {
4125                columns: self.core.m,
4126                certified_spectrum_max: CERTIFIED_SPECTRUM_MAX,
4127            });
4128        }
4129        let profile = self.core.reml_profile()?;
4130        let (log_lambda_lo, log_lambda_hi) = profile.log_lambda_domain()?;
4131        let resolution = f64::EPSILON.sqrt();
4132        let failed = |error: &dyn std::fmt::Display| {
4133            format!("residual cascade: REML stationary isolation failed: {error}")
4134        };
4135        let affine = profile.affine_view()?.ok_or(
4136            ResidualCascadeError::RemlScoreProofUnavailable {
4137                columns: self.core.m,
4138                certified_spectrum_max: CERTIFIED_SPECTRUM_MAX,
4139            },
4140        )?;
4141        // The budget refusal is re-typed here rather than formatted into the
4142        // generic computation failure: a nameless refusal at a derived depth
4143        // costs a day of triage, and the two numbers that explain it —
4144        // `rank` against `n - nullity` — are only available at this seam
4145        // (#2546).
4146        let search = match affine.maximize_value_ordered(
4147            log_lambda_lo,
4148            log_lambda_hi,
4149            resolution,
4150        ) {
4151            Ok(search) => search,
4152            Err(gam_math::score_opt::ScoreSearchError::SubdivisionBudget {
4153                subdivisions,
4154                budget,
4155                ..
4156            }) => {
4157                let nullity = self.core.nullity();
4158                return Err(ResidualCascadeError::RemlScoreSearchUndecomposable {
4159                    columns: self.core.m,
4160                    rank: self.core.m - nullity,
4161                    identifiable: self.core.y.len().saturating_sub(nullity),
4162                    subdivisions,
4163                    budget,
4164                    log_lambda_lo,
4165                    log_lambda_hi,
4166                });
4167            }
4168            Err(error) => return Err(ResidualCascadeError::Computation(failed(&error))),
4169        };
4170        if search.value_certificate.maximum_excess
4171            > search.value_certificate.comparison_resolution
4172        {
4173            return Err(ResidualCascadeError::RemlValueOrderingUnresolved {
4174                maximum_excess: search.value_certificate.maximum_excess,
4175                comparison_resolution: search.value_certificate.comparison_resolution,
4176            });
4177        }
4178        enum KktKind {
4179            LowerBoundary,
4180            UpperBoundary,
4181            Stationary,
4182        }
4183        let (bracket, kkt_kind) = match search.location {
4184            gam_math::score_opt::ScoreOptimumLocation::LowerBoundary => (
4185                gam_math::score_opt::ClosedInterval::point(search.lower_boundary.x),
4186                KktKind::LowerBoundary,
4187            ),
4188            gam_math::score_opt::ScoreOptimumLocation::UpperBoundary => (
4189                gam_math::score_opt::ClosedInterval::point(search.upper_boundary.x),
4190                KktKind::UpperBoundary,
4191            ),
4192            gam_math::score_opt::ScoreOptimumLocation::Stationary(index) => (
4193                search
4194                    .stationary_points
4195                    .get(index)
4196                    .ok_or_else(|| {
4197                        ResidualCascadeError::Computation(
4198                            "residual cascade: optimizer returned an invalid stationary index"
4199                                .to_string(),
4200                        )
4201                    })?
4202                    .bracket,
4203                KktKind::Stationary,
4204            ),
4205            gam_math::score_opt::ScoreOptimumLocation::ResolutionFlat(index) => {
4206                let flat = search.resolution_flat_regions.get(index).ok_or_else(|| {
4207                    ResidualCascadeError::Computation(
4208                        "residual cascade: optimizer returned an invalid resolution-flat index"
4209                            .to_string(),
4210                    )
4211                })?;
4212                return Err(ResidualCascadeError::RemlOptimumResolutionFlat {
4213                    lo: flat.bracket.lo,
4214                    hi: flat.bracket.hi,
4215                    max_score_gap: flat.max_score_gap,
4216                    score_resolution: flat.score_resolution,
4217                });
4218            }
4219        };
4220        let kkt = affine
4221            .enclose(bracket.lo, bracket.hi)
4222            .map_err(|error| ResidualCascadeError::Computation(failed(&error)))?;
4223        let kkt_holds = match kkt_kind {
4224            KktKind::LowerBoundary => kkt.derivative.hi <= 0.0,
4225            KktKind::UpperBoundary => kkt.derivative.lo >= 0.0,
4226            KktKind::Stationary => {
4227                kkt.derivative.contains_zero() && kkt.curvature.hi < 0.0
4228            }
4229        };
4230        if !kkt_holds {
4231            return Err(ResidualCascadeError::Computation(format!(
4232                "residual cascade: exact-real REML KKT certificate failed on \
4233                 {bracket:?}: {kkt:?}"
4234            )));
4235        }
4236        let selected_log_lambda = search.optimum.x;
4237        let selected = profile.evaluate(selected_log_lambda)?;
4238        Ok(self.fit_at_with_warm(
4239            selected_log_lambda,
4240            None,
4241            None,
4242            Some(selected.normalized_logdet),
4243        )?)
4244    }
4245
4246    /// Assess the candidate level at `exponent` AND decide what the refinement
4247    /// may actually take from it.
4248    ///
4249    /// The assessment is always over the COMPLETE candidate set, because that is
4250    /// the quantity the refinement decision has to be taken on: a bound over a
4251    /// subset would certify nothing about the candidates left out, and the
4252    /// Occam factor it is compared against is the complete set's too. The SELECTION is a different question,
4253    /// and it is the one the capacity budgets answer — how many more penalized
4254    /// modes this design may carry before automatic REML loses the rank it
4255    /// needs (`n − nullity` identifiable directions) or the certified spectrum
4256    /// outruns its memory ([`CERTIFIED_SPECTRUM_MAX`]).
4257    ///
4258    /// Those two questions used to share one answer: a candidate level wider
4259    /// than the budget was refused whole, so a refinement whose gain is carried
4260    /// by a handful of centers was blocked by the CARDINALITY of the proposal
4261    /// rather than by anything about the gain (#2700). A capacity limit is now
4262    /// a refusal only when the budget is exhausted — when nothing at all can be
4263    /// added. Otherwise the level is taken partially, largest `|g_j|` first,
4264    /// which is the ordering that maximizes the captured share of the same
4265    /// `Σ_j g_j²` the bound is made of.
4266    fn plan_level_at_exponent(
4267        &self,
4268        fit: &ResidualCascadeFit,
4269        exponent: f64,
4270        screen: Option<EvidenceScale>,
4271    ) -> Result<NextLevelPlan, String> {
4272        let core = &self.core;
4273        if !Arc::ptr_eq(core, &fit.core) {
4274            return Err("residual cascade: fit does not belong to this design".into());
4275        }
4276        let next_l = core.levels.len();
4277        let h = core.levels[0].h * 0.5_f64.powf(exponent);
4278        // `h == h_L` is the RE-assessment of a level that capacity forced to be
4279        // partial: the candidates it had to leave behind are exactly what
4280        // `extend_net` still offers at that radius, and their gain is what
4281        // decides whether the cascade has converged there.
4282        if !(exponent.is_finite() && h > 0.0 && h <= core.levels[next_l - 1].h) {
4283            return Err(format!(
4284                "residual cascade: next resolution exponent {exponent} does not refine the \
4285                 current radius {}",
4286                core.levels[next_l - 1].h
4287            ));
4288        }
4289        // A level finer than the current one is a NEW level; re-assessing the
4290        // finest radius extends the level already there and cannot exhaust the
4291        // level count. Decided from the RADIUS alone, before any candidate set
4292        // exists, because it describes where the set would go and not what is
4293        // in it — every outcome below has to carry it, including the exhausted
4294        // ones, or a caller that materializes the candidate set from the plan
4295        // plants a second level at a radius that already has one.
4296        let extends_last = h == core.levels[next_l - 1].h;
4297        let mut net = core.net.clone();
4298        let candidates = extend_net(&mut net, &core.z, core.dim, h, &core.z_range);
4299        if candidates.is_empty() {
4300            return Ok(NextLevelPlan::exhausted(
4301                NextLevelAssessment::EmptyNet,
4302                extends_last,
4303            ));
4304        }
4305        if net.len() > MAX_CENTERS {
4306            return Ok(NextLevelPlan::exhausted(
4307                NextLevelAssessment::CapacityExceeded {
4308                    obstruction: RefinementObstruction::CenterCapacity {
4309                        centers: net.len(),
4310                        maximum_centers: MAX_CENTERS,
4311                    },
4312                    // The cap stopped candidate construction before every column
4313                    // could contribute to ‖X₂'Wr̂‖². Infinity is the honest
4314                    // conservative upper bound; a finite partial sum would not
4315                    // certify the omitted columns.
4316                    gain_bound: f64::INFINITY,
4317                },
4318                extends_last,
4319            ));
4320        }
4321        let delta = OVERLAP * h;
4322        let mut grid = HashGrid::new(delta, core.dim);
4323        for (j, c) in candidates.iter().enumerate() {
4324            grid.insert(j as u32, c);
4325        }
4326        let r = core.residuals(&fit.coeff);
4327        let mut g = vec![0.0_f64; candidates.len()];
4328        // Which candidates carry a row at all. A candidate whose bump covers no
4329        // observation is an exactly zero column: it contributes nothing to `g`,
4330        // nothing to the Schur complement, and — because its `λd` diagonal
4331        // appears identically in `log|A|` and in `log|λD|₊` — nothing to the
4332        // restricted likelihood either. Dropping it from the design the
4333        // comparison is taken on is therefore an identity, not an
4334        // approximation, and it is worth taking: at the radii this decision is
4335        // made at, most of a dyadic level's centers sit between data points.
4336        let mut supported = vec![false; candidates.len()];
4337        for (i, zi) in core.z.iter().enumerate() {
4338            let wr = core.w[i] * r[i];
4339            grid.for_neighbors(zi, |j| {
4340                let j = j as usize;
4341                let rad = dist2(zi, &candidates[j], core.dim).sqrt() / delta;
4342                let value = wendland(rad);
4343                if value != 0.0 {
4344                    supported[j] = true;
4345                }
4346                g[j] += wr * value;
4347            });
4348        }
4349        let supported: Vec<[f64; 3]> = candidates
4350            .iter()
4351            .zip(supported.iter())
4352            .filter_map(|(center, &carries)| carries.then_some(*center))
4353            .collect();
4354        let d_next = level_weight(exponent, core.sobolev_s, core.dim);
4355        let lambda = gam_problem::checked_exp_log_strength(fit.log_lambda)
4356            .map_err(|error| format!("residual cascade refinement: {error}"))?;
4357        let bracket = certified_refinement_gain(
4358            core,
4359            &CandidateLevel {
4360                centers: &candidates,
4361                grid: &grid,
4362                delta,
4363                ridge: lambda * d_next,
4364            },
4365            &g,
4366            lambda,
4367            screen,
4368        )?;
4369        let gain_bound = bracket.upper;
4370        let candidate_penalized_modes = net.len();
4371        let candidate_columns = core.nullity() + candidate_penalized_modes;
4372        let identifiable_directions = core.y.len().saturating_sub(core.nullity());
4373        if !extends_last && next_l >= MAX_LEVELS {
4374            return Ok(NextLevelPlan::exhausted_with(
4375                NextLevelAssessment::CapacityExceeded {
4376                    obstruction: RefinementObstruction::LevelCapacity {
4377                        levels: next_l,
4378                        maximum_levels: MAX_LEVELS,
4379                    },
4380                    gain_bound,
4381                },
4382                bracket,
4383                supported,
4384                extends_last,
4385            ));
4386        }
4387        // Penalized modes this design may still carry with the certified
4388        // automatic route intact. Both bounds are structural, neither is a
4389        // tuning parameter: past `identifiable_directions = n − nullity` the
4390        // REML score is flat by rank deficiency, and past
4391        // `CERTIFIED_SPECTRUM_MAX` columns the exact Schur eigendecomposition
4392        // the score enclosure is built from exceeds its memory budget.
4393        let spectrum_modes = CERTIFIED_SPECTRUM_MAX.saturating_sub(core.nullity());
4394        let capacity_modes = identifiable_directions.min(spectrum_modes);
4395        let budget = capacity_modes
4396            .saturating_sub(core.net.len())
4397            .min(candidates.len());
4398        if budget == 0 {
4399            // Nothing may be added at all — the only shape in which a capacity
4400            // limit is a refusal. The obstruction names the bound that binds,
4401            // and the evidence stays the COMPLETE proposal: that is what the
4402            // cascade still has to add and cannot.
4403            let obstruction = if identifiable_directions <= spectrum_modes {
4404                RefinementObstruction::IdentifiabilityCapacity {
4405                    candidate_columns,
4406                    candidate_penalized_modes,
4407                    identifiable_directions,
4408                }
4409            } else {
4410                RefinementObstruction::CertifiedSpectrumCapacity {
4411                    candidate_columns,
4412                    certified_spectrum_max: CERTIFIED_SPECTRUM_MAX,
4413                }
4414            };
4415            return Ok(NextLevelPlan::exhausted_with(
4416                NextLevelAssessment::CapacityExceeded {
4417                    obstruction,
4418                    gain_bound,
4419                },
4420                bracket,
4421                supported,
4422                extends_last,
4423            ));
4424        }
4425        let candidate_count = candidates.len();
4426        let selection = if budget == candidate_count {
4427            candidates
4428        } else {
4429            // Largest |g_j| first. The ordering is on the very terms of
4430            // `Σ_j g_j²`, so the retained subset carries the largest share of
4431            // the bound any subset of this size can; ties break on candidate
4432            // index, which `extend_net` fixes deterministically.
4433            let mut order: Vec<usize> = (0..candidates.len()).collect();
4434            order.sort_by(|&a, &b| {
4435                g[b].abs()
4436                    .partial_cmp(&g[a].abs())
4437                    .unwrap_or(std::cmp::Ordering::Equal)
4438                    .then(a.cmp(&b))
4439            });
4440            order.truncate(budget);
4441            order.sort_unstable();
4442            order.into_iter().map(|j| candidates[j]).collect()
4443        };
4444        let complete = selection.len() == candidate_count;
4445        Ok(NextLevelPlan {
4446            assessment: NextLevelAssessment::GainBound(gain_bound),
4447            gain: Some(bracket),
4448            selection,
4449            supported,
4450            complete,
4451            extends_last,
4452        })
4453    }
4454}
4455
4456/// The two scalars a gain is turned into evidence by: the incumbent's penalized
4457/// residual and the restricted degrees of freedom, both of which are properties
4458/// of the FIT rather than of the candidate set.
4459///
4460/// Passing one to a gain bracket ARMS the free screen. Hadamard on
4461/// `S ⪯ diag(X₂ᵀWX₂) + λd` bounds the Occam factor from above for nothing, so
4462/// the gain at which THAT bound breaks even is an upper bound on the true
4463/// break-even gain: a bracket whose lower end clears it proves one more level
4464/// warranted without building the refined design, and a bracket that falls
4465/// below it proves nothing — which is exactly when the exact comparison has to
4466/// run, and therefore exactly when further bracket iterations are waste.
4467/// Without one, the bracket closes as far as the Krylov space allows, because
4468/// no comparison is pending and only the number is wanted.
4469#[derive(Clone, Copy, Debug)]
4470struct EvidenceScale {
4471    rss_pen: f64,
4472    dof: f64,
4473}
4474
4475impl EvidenceScale {
4476    /// The gain at which a candidate set whose Occam factor is `occam` breaks
4477    /// even: `rss_pen·(1 − e^{−occam/dof})`, the objective decrease that set's
4478    /// own dimension already pays for. Below it the restricted likelihood
4479    /// falls, above it the likelihood rises, and at it they are equal — which
4480    /// is why this is a derivation and not a tolerance.
4481    fn break_even_gain(&self, occam: f64) -> f64 {
4482        -self.rss_pen * (-occam / self.dof).exp_m1()
4483    }
4484
4485    /// The restricted log-likelihood change a candidate set with this `gain` and
4486    /// this `occam` produces: `[dof·log(rss/(rss − gain)) − occam]/2`. Increasing
4487    /// in `gain`, decreasing in `occam` — which is what lets a lower bound on
4488    /// one and an upper bound on the other CERTIFY a positive value.
4489    fn evidence(&self, gain: f64, occam: f64) -> f64 {
4490        // A gain cannot exceed the residual it decreases — the objective is
4491        // bounded below by zero — so the ratio is clamped into `[0, 1]` rather
4492        // than allowed to hand `ln_1p` an argument below −1 and return NaN when
4493        // a certified LOWER bound on the gain lands a rounding step past it.
4494        let spent = (gain / self.rss_pen).clamp(0.0, 1.0);
4495        0.5 * (-self.dof * (-spent).ln_1p() - occam)
4496    }
4497}
4498
4499/// The candidate level a refinement gain is being certified for: its centers,
4500/// the hash grid that finds the rows each one supports, the bump radius, and the
4501/// exact ridge `λ·d_{L+1}` its columns would carry.
4502struct CandidateLevel<'a> {
4503    centers: &'a [[f64; 3]],
4504    grid: &'a HashGrid,
4505    delta: f64,
4506    ridge: f64,
4507}
4508
4509impl CandidateLevel<'_> {
4510    /// `X₂ v`, the candidate design applied to a coefficient vector.
4511    fn apply(&self, core: &Core, v: &[f64], out: &mut [f64]) {
4512        for (row, z) in core.z.iter().enumerate() {
4513            let mut value = 0.0;
4514            self.grid.for_neighbors(z, |j| {
4515                let j = j as usize;
4516                let radius = dist2(z, &self.centers[j], core.dim).sqrt() / self.delta;
4517                value += wendland(radius) * v[j];
4518            });
4519            out[row] = value;
4520        }
4521    }
4522
4523    /// `X₂ᵀ u`, the candidate design's transpose applied to a row vector.
4524    fn apply_transpose(&self, core: &Core, u: &[f64], out: &mut [f64]) {
4525        out.fill(0.0);
4526        for (row, z) in core.z.iter().enumerate() {
4527            let value = u[row];
4528            if value == 0.0 {
4529                continue;
4530            }
4531            self.grid.for_neighbors(z, |j| {
4532                let j = j as usize;
4533                let radius = dist2(z, &self.centers[j], core.dim).sqrt() / self.delta;
4534                out[j] += wendland(radius) * value;
4535            });
4536        }
4537    }
4538
4539    /// `diag(X₂ᵀWX₂) + λ·d`, the Jacobi preconditioner for the Schur operator.
4540    ///
4541    /// It is the diagonal of an UPPER bound on the operator — `I − H ⪯ I`, so
4542    /// `diag(S) ⩽ diag(X₂ᵀWX₂) + λd` — which is what a preconditioner is
4543    /// allowed to be. Forming the true `diag(S)` would cost one cascade solve
4544    /// per candidate.
4545    fn jacobi_preconditioner(&self, core: &Core) -> Vec<f64> {
4546        let mut diagonal = vec![self.ridge; self.centers.len()];
4547        for (row, z) in core.z.iter().enumerate() {
4548            let weight = core.w[row];
4549            self.grid.for_neighbors(z, |j| {
4550                let j = j as usize;
4551                let radius = dist2(z, &self.centers[j], core.dim).sqrt() / self.delta;
4552                let value = wendland(radius);
4553                diagonal[j] += weight * value * value;
4554            });
4555        }
4556        diagonal
4557    }
4558}
4559
4560/// Reusable buffers for one `S v`, so the conjugate-gradient loop allocates
4561/// nothing per iteration.
4562struct SchurWorkspace {
4563    row: Vec<f64>,
4564    fitted: Vec<f64>,
4565    column: Vec<f64>,
4566    /// Previous cascade solve, handed to the next as a warm start. The systems
4567    /// differ only in their right-hand side, so this is free accuracy per
4568    /// iteration and changes nothing about the solve's certified residual.
4569    warm: Option<Vec<f64>>,
4570}
4571
4572/// `S v = X2' W (I - H) X2 v + lambda*d*v`, with nothing dense formed.
4573///
4574/// One apply of the candidate design, ONE cascade solve for the hat matrix, one
4575/// apply of the transpose back. The solve carries its own backward-error
4576/// certificate (`CG_RTOL`), so the operator is exact to that relative accuracy
4577/// and the bracket below inherits it.
4578fn apply_candidate_schur(
4579    core: &Core,
4580    level: &CandidateLevel<'_>,
4581    lambda: f64,
4582    v: &[f64],
4583    out: &mut [f64],
4584    workspace: &mut SchurWorkspace,
4585) -> Result<(), String> {
4586    let rows = core.z.len();
4587    level.apply(core, v, &mut workspace.row);
4588    workspace.column.fill(0.0);
4589    for row in 0..rows {
4590        let weighted = core.w[row] * workspace.row[row];
4591        for entry in core.row_ptr[row]..core.row_ptr[row + 1] {
4592            workspace.column[core.col_idx[entry] as usize] += core.vals[entry] * weighted;
4593        }
4594    }
4595    let (coeff, _, _) = core.solve_coeff(lambda, &workspace.column, workspace.warm.as_deref())?;
4596    for row in 0..rows {
4597        let mut value = 0.0;
4598        for entry in core.row_ptr[row]..core.row_ptr[row + 1] {
4599            value += core.vals[entry] * coeff[core.col_idx[entry] as usize];
4600        }
4601        workspace.fitted[row] = value;
4602    }
4603    workspace.warm = Some(coeff);
4604    for row in 0..rows {
4605        workspace.row[row] = core.w[row] * (workspace.row[row] - workspace.fitted[row]);
4606    }
4607    level.apply_transpose(core, &workspace.row, out);
4608    for (target, &value) in out.iter_mut().zip(v.iter()) {
4609        *target += level.ridge * value;
4610    }
4611    Ok(())
4612}
4613
4614/// A rigorous two-sided bracket on the exact level-`(L+1)` gain, and the
4615/// evidence for it.
4616struct RefinementGainBracket {
4617    /// `2xᵀg − xᵀSx` — a lower bound for EVERY `x`, exact at `x = S⁻¹g`.
4618    lower: f64,
4619    /// The certified upper bound the refinement decision is taken on.
4620    upper: f64,
4621    /// Conjugate-gradient steps spent closing the bracket.
4622    iterations: usize,
4623    /// `Σ_j log(diag(S)_j / λd) ⩾ log det(S/λd)`, the Hadamard bound on the
4624    /// candidate level's Occam factor. `I − H ⪯ I` makes `diag(S) ⩽
4625    /// diag(X₂ᵀWX₂) + λd`, which is the Jacobi preconditioner this routine
4626    /// already forms, and Hadamard's inequality bounds a PSD determinant by its
4627    /// diagonal product — so this costs one pass over the candidate supports
4628    /// and no solve at all.
4629    hadamard_occam: f64,
4630}
4631
4632impl RefinementGainBracket {
4633    /// The comparison this bracket CERTIFIES on its own, without building
4634    /// anything: the gain read from its LOWER end and the Occam factor from its
4635    /// Hadamard upper bound. Both readings are taken in the direction that can
4636    /// only understate the evidence, so a positive `evidence` here is a proof
4637    /// that one more level earns its own Occam factor — and a non-positive one
4638    /// is no information at all, because both readings were taken against the
4639    /// level. It is the same comparison the exact route makes, evaluated on
4640    /// certified bounds instead of on a design.
4641    fn screened_comparison(&self, scale: EvidenceScale) -> RefinementCertificate {
4642        RefinementCertificate {
4643            gain: self.lower,
4644            occam: self.hadamard_occam,
4645            tolerance: scale.break_even_gain(self.hadamard_occam),
4646            evidence: scale.evidence(self.lower, self.hadamard_occam),
4647        }
4648    }
4649}
4650
4651/// The exact level-`(L+1)` gain `gᵀS⁻¹g`, bracketed.
4652///
4653/// # What the shipped bound was, and why it is the `x = 0` member of this family
4654///
4655/// Appending the candidate columns `X₂` with penalty `λd` decreases the
4656/// penalized objective by exactly `gᵀS⁻¹g`, with
4657///
4658/// ```text
4659///     g = X₂ᵀW r̂,     S = X₂ᵀW(I − H)X₂ + λd·I,     H = W^{1/2}X₁A⁻¹X₁ᵀW^{1/2}
4660/// ```
4661///
4662/// The certificate bounded that by discarding the ENTIRE data term — `S ⪰ λd·I`
4663/// gives `gᵀS⁻¹g ⩽ ‖g‖²/(λd)` — which is exactly this routine at `x = 0`. That
4664/// step is not a small conservatism where it matters most: when the candidate
4665/// level is redundant against the design already fitted, which is what the
4666/// rank-maximal regime IS, `X₂ᵀW(I − H)X₂` is the dominant term (#2759).
4667///
4668/// # The bracket
4669///
4670/// For ANY `x`, writing `r = g − Sx`,
4671///
4672/// ```text
4673///     2xᵀg − xᵀSx   ⩽   gᵀS⁻¹g   ⩽   2xᵀg − xᵀSx + ‖r‖²/(λd)
4674/// ```
4675///
4676/// The left inequality is `(x − S⁻¹g)ᵀS(x − S⁻¹g) ⩾ 0`; the right one adds
4677/// `rᵀS⁻¹r ⩽ ‖r‖²/λ_min(S)` and `λ_min(S) ⩾ λd` — the SAME structural fact the
4678/// shipped bound rests on, and the only inequality used. Both ends are computed
4679/// from an explicit `Sx`, never from a conjugate-gradient recurrence, so no
4680/// statement here depends on the iteration having behaved.
4681///
4682/// The returned upper bound is additionally floored by `‖g‖²/(λd)`, so this
4683/// certificate can never be LOOSER than the one it replaces, whatever the
4684/// iteration does.
4685///
4686/// # The stopping rule is the screen, not a tolerance
4687///
4688/// With a `screen`, iteration stops as soon as the bracket lands entirely on
4689/// one side of the break-even gain of the HADAMARD Occam bound: above it, one
4690/// more level provably earns marginal likelihood and no refined design has to
4691/// be built; below it, nothing is decided and the exact comparison has to run,
4692/// so further iterations are waste. There is no accuracy constant to pick,
4693/// because accuracy is not what is being asked for — a comparison is. The
4694/// structural ceiling is the Krylov dimension, past which the answer is exact
4695/// by construction; a stalled bracket (the gap not shrinking) is exactness
4696/// reached early and stops too.
4697fn certified_refinement_gain(
4698    core: &Core,
4699    level: &CandidateLevel<'_>,
4700    g: &[f64],
4701    lambda: f64,
4702    screen: Option<EvidenceScale>,
4703) -> Result<RefinementGainBracket, String> {
4704    let candidates = level.centers.len();
4705    let ridge = level.ridge;
4706    let energy: f64 = g.iter().map(|value| value * value).sum();
4707    let preconditioner = level.jacobi_preconditioner(core);
4708    // Hadamard on `S/λd ⪯ (diag(X₂ᵀWX₂) + λd)/λd`. Formed here because the
4709    // preconditioner IS that diagonal, so the bound is a reduction over a
4710    // vector this routine already has.
4711    let hadamard_occam: f64 = preconditioner
4712        .iter()
4713        .map(|diagonal| (diagonal / ridge).max(1.0).ln())
4714        .sum();
4715    // The `x = 0` member: rigorous on its own, and the floor every later
4716    // iterate is compared against.
4717    let zeroth = energy / ridge;
4718    if !(zeroth.is_finite() && zeroth > 0.0) || candidates == 0 {
4719        return Ok(RefinementGainBracket {
4720            lower: 0.0,
4721            upper: zeroth.max(0.0),
4722            iterations: 0,
4723            hadamard_occam,
4724        });
4725    }
4726
4727    let rows = core.z.len();
4728    let mut workspace = SchurWorkspace {
4729        row: vec![0.0_f64; rows],
4730        fitted: vec![0.0_f64; rows],
4731        column: vec![0.0_f64; core.m],
4732        warm: None,
4733    };
4734    let mut x = vec![0.0_f64; candidates];
4735    let mut residual = g.to_vec();
4736    let mut preconditioned = vec![0.0_f64; candidates];
4737    let mut direction = vec![0.0_f64; candidates];
4738    let mut operated = vec![0.0_f64; candidates];
4739    let mut certify = vec![0.0_f64; candidates];
4740    let mut rho = 0.0_f64;
4741
4742    // The Krylov space cannot exceed the rank of the operator's data term plus
4743    // one, and that term factors through the `n` rows: `X₂ᵀW(I−H)X₂` has rank at
4744    // most `n`. Past `min(candidates, n) + 1` steps the solution is exact and
4745    // the bracket has closed by construction, so this is a structural ceiling
4746    // rather than a budget — the same reading `residual_krylov_ceiling` takes.
4747    let ceiling = candidates.min(rows) + 1;
4748    let mut best = RefinementGainBracket {
4749        lower: 0.0,
4750        upper: zeroth,
4751        iterations: 0,
4752        hadamard_occam,
4753    };
4754    for iteration in 0..ceiling {
4755        for ((target, &value), &diagonal) in preconditioned
4756            .iter_mut()
4757            .zip(residual.iter())
4758            .zip(preconditioner.iter())
4759        {
4760            *target = if diagonal > 0.0 { value / diagonal } else { value };
4761        }
4762        let rho_next: f64 = residual
4763            .iter()
4764            .zip(preconditioned.iter())
4765            .map(|(&a, &b)| a * b)
4766            .sum();
4767        if !(rho_next.is_finite() && rho_next > 0.0) {
4768            break;
4769        }
4770        if iteration == 0 {
4771            direction.copy_from_slice(&preconditioned);
4772        } else {
4773            let beta = rho_next / rho;
4774            for (target, &value) in direction.iter_mut().zip(preconditioned.iter()) {
4775                *target = value + beta * *target;
4776            }
4777        }
4778        rho = rho_next;
4779        apply_candidate_schur(
4780            core,
4781            level,
4782            lambda,
4783            &direction,
4784            &mut operated,
4785            &mut workspace,
4786        )?;
4787        let curvature: f64 = direction
4788            .iter()
4789            .zip(operated.iter())
4790            .map(|(&a, &b)| a * b)
4791            .sum();
4792        if !(curvature.is_finite() && curvature > 0.0) {
4793            break;
4794        }
4795        let alpha = rho / curvature;
4796        for (target, &value) in x.iter_mut().zip(direction.iter()) {
4797            *target += alpha * value;
4798        }
4799        for (target, &value) in residual.iter_mut().zip(operated.iter()) {
4800            *target -= alpha * value;
4801        }
4802
4803        // CERTIFY FROM AN EXPLICIT `Sx`. The recurrence above is the search; it
4804        // is not evidence, and a bound recurred through `alpha`/`beta` would
4805        // inherit whatever the iteration drifted by.
4806        apply_candidate_schur(core, level, lambda, &x, &mut certify, &mut workspace)?;
4807        let mut linear = 0.0_f64;
4808        let mut quadratic = 0.0_f64;
4809        let mut defect = 0.0_f64;
4810        for ((&xi, &gi), &si) in x.iter().zip(g.iter()).zip(certify.iter()) {
4811            linear += xi * gi;
4812            quadratic += xi * si;
4813            let gap = gi - si;
4814            defect += gap * gap;
4815        }
4816        let lower = 2.0 * linear - quadratic;
4817        let upper = lower + defect / ridge;
4818        if !(lower.is_finite() && upper.is_finite() && upper >= lower) {
4819            break;
4820        }
4821        if upper < best.upper {
4822            best = RefinementGainBracket {
4823                lower: lower.max(0.0),
4824                upper,
4825                iterations: iteration + 1,
4826                hadamard_occam,
4827            };
4828        }
4829        // The screen is decided: either the whole bracket clears the Hadamard
4830        // break-even gain, or none of it does.
4831        if let Some(threshold) = screen.map(|scale| scale.break_even_gain(hadamard_occam))
4832            && (upper <= threshold || lower > threshold)
4833        {
4834            best.iterations = iteration + 1;
4835            best.lower = lower.max(0.0);
4836            best.upper = upper.min(zeroth);
4837            return Ok(best);
4838        }
4839        // Exactness reached early: the residual has stopped shrinking, so no
4840        // further step can move either end.
4841        if defect <= f64::EPSILON * energy {
4842            break;
4843        }
4844    }
4845    best.upper = best.upper.min(zeroth);
4846    Ok(best)
4847}
4848
4849/// The next level's assessment together with the centers the refinement may
4850/// take from it under the capacity budgets.
4851struct NextLevelPlan {
4852    assessment: NextLevelAssessment,
4853    /// The two-sided evidence behind the assessment's bound, when there was a
4854    /// candidate set to certify at all. `None` for an empty net or a proposal
4855    /// the center cap stopped before it was complete.
4856    gain: Option<RefinementGainBracket>,
4857    /// Centers the refinement may add; empty exactly when nothing may be added.
4858    selection: Vec<[f64; 3]>,
4859    /// The complete candidate set MINUS its exactly-zero columns — the centers
4860    /// whose bumps cover no observation. Those columns cannot move the fit or
4861    /// the evidence (their `λd` diagonal cancels between `log|A|` and
4862    /// `log|λD|₊`), so this is the set the comparison is taken on, and it is the
4863    /// complete set for every purpose the comparison has.
4864    supported: Vec<[f64; 3]>,
4865    /// Whether the selection is the complete candidate set. A partial level is
4866    /// reproducible only from its explicit centers, so the plan carries them.
4867    complete: bool,
4868    /// Whether these centers extend the current finest level rather than
4869    /// starting a new one.
4870    extends_last: bool,
4871}
4872
4873impl NextLevelPlan {
4874    /// An assessment that admits no refinement: an empty net, or a capacity
4875    /// with no room left in it.
4876    fn exhausted(assessment: NextLevelAssessment, extends_last: bool) -> Self {
4877        Self {
4878            assessment,
4879            gain: None,
4880            selection: Vec::new(),
4881            supported: Vec::new(),
4882            complete: false,
4883            extends_last,
4884        }
4885    }
4886
4887    /// An exhausted plan that still carries the evidence its bound came from.
4888    /// An exhausted plan that still carries the evidence its bound came from
4889    /// AND the candidate set that bound is over. A capacity with no room left
4890    /// is exactly where the exact comparison matters most, so it is exactly
4891    /// where the set must survive the plan.
4892    fn exhausted_with(
4893        assessment: NextLevelAssessment,
4894        gain: RefinementGainBracket,
4895        supported: Vec<[f64; 3]>,
4896        extends_last: bool,
4897    ) -> Self {
4898        Self {
4899            gain: Some(gain),
4900            supported,
4901            ..Self::exhausted(assessment, extends_last)
4902        }
4903    }
4904}
4905
4906/// Prior precision at resolution `h = h₀·2⁻ᵉ`:
4907/// `(h₀/h)^(2s−d) = 4^{e(s−d/2)}`.
4908fn level_weight(exponent: f64, sobolev_s: f64, dim: usize) -> f64 {
4909    (4.0_f64).powf(exponent * (sobolev_s - dim as f64 / 2.0))
4910}
4911
4912/// Lightweight view used during assembly, before the Core exists: shares the
4913/// exact basis-row logic with [`Core::basis_row_scaled`] so the assembled CSR
4914/// and later prediction rows cannot drift apart.
4915struct CoreScaffold<'a> {
4916    dim: usize,
4917    z_range: [f64; 3],
4918    levels: &'a [Level],
4919}
4920
4921impl CoreScaffold<'_> {
4922    fn basis_row(&self, z: &[f64; 3]) -> Vec<(usize, f64)> {
4923        let mut row = Vec::with_capacity(self.dim + 1 + self.levels.len() * 8);
4924        row.push((0, 1.0));
4925        for a in 0..self.dim {
4926            row.push((a + 1, 2.0 * z[a] / self.z_range[a] - 1.0));
4927        }
4928        for level in self.levels {
4929            let start = row.len();
4930            level.grid.for_neighbors(z, |j| {
4931                let c = &level.centers[j as usize];
4932                let r = dist2(z, c, self.dim).sqrt() / level.delta;
4933                let v = wendland(r);
4934                if v > 0.0 {
4935                    row.push((level.col_offset + j as usize, v));
4936                }
4937            });
4938            row[start..].sort_unstable_by_key(|&(col, _)| col);
4939        }
4940        row
4941    }
4942}
4943
4944impl ResidualCascadeFit {
4945    pub fn log_lambda(&self) -> f64 {
4946        self.log_lambda
4947    }
4948
4949    pub fn lambda(&self) -> f64 {
4950        gam_problem::checked_exp_log_strength(self.log_lambda)
4951            .expect("ResidualCascadeFit construction validates its private log strength")
4952    }
4953
4954    /// Number of original training rows / experimental units.
4955    pub fn training_sample_size(&self) -> usize {
4956        self.training_sample_size.get()
4957    }
4958
4959    /// Posterior `(mean, variance)` at a raw point: the sparse basis row
4960    /// dotted with the coefficients, and `σ̂²·x'(X'WX+λD)^{−1}x` through one
4961    /// certified solve.
4962    pub fn predict(&self, x: &[f64]) -> Result<(f64, f64), String> {
4963        let core = &self.core;
4964        if x.len() != core.dim || x.iter().any(|v| !v.is_finite()) {
4965            return Err(format!(
4966                "residual cascade: prediction point must be {} finite coordinates, got {x:?}",
4967                core.dim
4968            ));
4969        }
4970        let row = core.basis_row_scaled(&core.scale_point(x));
4971        let mut mean = 0.0;
4972        let mut dense_row = vec![0.0_f64; core.m];
4973        for &(c, v) in &row {
4974            mean += v * self.coeff[c];
4975            dense_row[c] += v;
4976        }
4977        let lambda = gam_problem::checked_exp_log_strength(self.log_lambda)
4978            .map_err(|error| format!("residual cascade fit: {error}"))?;
4979        let zsol = if let Some(l) = &self.predict_chol {
4980            chol_solve(l, core.m, &dense_row)
4981        } else if let Some(factor) = &self.predict_sparse {
4982            // Exact, through the same factorization the fit's log-determinant
4983            // was read off — so the posterior variance carries no iterative
4984            // backward error at all past the dense Gram cache.
4985            solve_sparse_spd(factor, &Array1::from(dense_row.clone()))
4986                .map_err(|error| {
4987                    format!("residual cascade: sparse posterior-variance solve failed: {error}")
4988                })?
4989                .to_vec()
4990        } else {
4991            core.solve_coeff(lambda, &dense_row, None)?.0
4992        };
4993        let mut quad = 0.0;
4994        for (a, b) in dense_row.iter().zip(zsol.iter()) {
4995            quad += a * b;
4996        }
4997        Ok((mean, self.sigma2 * quad))
4998    }
4999
5000    /// Number of resolution levels in the fitted cascade.
5001    pub fn num_levels(&self) -> usize {
5002        self.core.levels.len()
5003    }
5004
5005    /// Total coefficient count.
5006    pub fn num_coeffs(&self) -> usize {
5007        self.core.m
5008    }
5009
5010    /// Total centers across all fitted resolution levels.
5011    pub fn num_centers(&self) -> usize {
5012        self.core.m - self.core.nullity()
5013    }
5014
5015    /// Snapshot the fit for persistence (#1032). Assembles the factored
5016    /// precision `L` of `A = X'WX + λD` at the fit's λ (O(m³) once) and copies
5017    /// the nested geometry + coefficients, dropping all training rows. The
5018    /// resulting [`ResidualCascadeState`] is predict-complete: `from_state`
5019    /// replays the posterior mean+variance bit-for-bit.
5020    pub fn to_state(&self) -> Result<ResidualCascadeState, String> {
5021        let core = &self.core;
5022        let training_sample_size =
5023            u64::try_from(self.training_sample_size.get()).map_err(|_| {
5024                format!(
5025                    "residual cascade fit: training_sample_size {} exceeds the persistence format",
5026                    self.training_sample_size
5027                )
5028            })?;
5029        let training_sample_size =
5030            std::num::NonZeroU64::new(training_sample_size).ok_or_else(|| {
5031                "residual cascade fit: training_sample_size must be positive".to_string()
5032            })?;
5033        let lambda = gam_problem::checked_exp_log_strength(self.log_lambda)
5034            .map_err(|error| format!("residual cascade fit: {error}"))?;
5035        let predict_chol = if let Some(l) = &self.predict_chol {
5036            l.clone()
5037        } else if let Some(l) = &core.predict_chol {
5038            l.clone()
5039        } else {
5040            core.assemble_predict_factor(lambda)?
5041        };
5042        let dim = core.dim;
5043        let levels = core
5044            .levels
5045            .iter()
5046            .map(|level| {
5047                let mut centers = Vec::with_capacity(level.centers.len() * dim);
5048                for c in &level.centers {
5049                    centers.extend_from_slice(&c[..dim]);
5050                }
5051                LevelState {
5052                    h: level.h,
5053                    delta: level.delta,
5054                    weight: level.weight,
5055                    col_offset: level.col_offset as u64,
5056                    centers,
5057                }
5058            })
5059            .collect();
5060        Ok(ResidualCascadeState {
5061            training_sample_size,
5062            dim: dim as u64,
5063            metric: core.metric,
5064            z_lo: core.z_lo,
5065            z_range: core.z_range,
5066            sobolev_s: core.sobolev_s,
5067            levels,
5068            m: core.m as u64,
5069            pen_logdet_const: core.pen_logdet_const,
5070            coeff: self.coeff.clone(),
5071            log_lambda: self.log_lambda,
5072            sigma2: self.sigma2,
5073            restricted_loglik: self.restricted_loglik,
5074            rss_pen: self.rss_pen,
5075            predict_chol,
5076        })
5077    }
5078
5079    /// Rebuild a predict-capable fit from a snapshot (#1032). Validates shape,
5080    /// finiteness, the Sobolev/Wendland window, strictly-positive level weights
5081    /// and box ranges, the column accounting (`m = dim+1 + Σ centers`, matching
5082    /// `col_offset`s), positive σ², and that `predict_chol` is a valid `m × m`
5083    /// lower factor (positive pivots) — so a corrupt payload fails here, not in
5084    /// a later `predict`. The restored `Core` has empty training CSR and
5085    /// `predict_chol = Some(L)`; its `predict` reads only geometry (mean) and
5086    /// the factor (variance), replaying both exactly.
5087    pub fn from_state(state: &ResidualCascadeState) -> Result<Self, String> {
5088        let training_sample_size =
5089            usize::try_from(state.training_sample_size.get()).map_err(|_| {
5090                format!(
5091                    "residual cascade state: training_sample_size {} exceeds this platform's usize",
5092                    state.training_sample_size
5093                )
5094            })?;
5095        let dim = state.dim as usize;
5096        if !(dim == 2 || dim == 3) {
5097            return Err(format!(
5098                "residual cascade state: dim must be 2 or 3, got {dim}"
5099            ));
5100        }
5101        if !(state.sobolev_s > dim as f64 / 2.0 && state.sobolev_s <= (dim as f64 + 3.0) / 2.0) {
5102            return Err(format!(
5103                "residual cascade state: sobolev_s {} outside the Wendland window ({}, {}]",
5104                state.sobolev_s,
5105                dim as f64 / 2.0,
5106                (dim as f64 + 3.0) / 2.0
5107            ));
5108        }
5109        for a in 0..dim {
5110            if !(state.metric[a].is_finite() && state.metric[a] > 0.0) {
5111                return Err(format!(
5112                    "residual cascade state: metric axis {a} must be finite positive, got {}",
5113                    state.metric[a]
5114                ));
5115            }
5116            if !(state.z_range[a].is_finite()
5117                && state.z_range[a] > 0.0
5118                && state.z_lo[a].is_finite())
5119            {
5120                return Err(format!(
5121                    "residual cascade state: degenerate box on axis {a} (lo={}, range={})",
5122                    state.z_lo[a], state.z_range[a]
5123                ));
5124            }
5125        }
5126        let m = state.m as usize;
5127        let mut metric3 = [1.0_f64; 3];
5128        metric3[..dim].copy_from_slice(&state.metric[..dim]);
5129        let mut z_lo = [0.0_f64; 3];
5130        let mut z_range = [1.0_f64; 3];
5131        z_lo[..dim].copy_from_slice(&state.z_lo[..dim]);
5132        z_range[..dim].copy_from_slice(&state.z_range[..dim]);
5133
5134        // Rebuild the levels and their lookup grids from the flattened centers,
5135        // checking the column accounting matches the polynomial layer + blocks.
5136        let mut levels = Vec::with_capacity(state.levels.len());
5137        let mut net: Vec<[f64; 3]> = Vec::new();
5138        let mut pen_diag = vec![0.0_f64; m];
5139        let mut expected_offset = dim + 1;
5140        for (li, ls) in state.levels.iter().enumerate() {
5141            if !(ls.h.is_finite() && ls.h > 0.0 && ls.delta.is_finite() && ls.delta > 0.0) {
5142                return Err(format!(
5143                    "residual cascade state: level {li} has non-positive h/delta ({}, {})",
5144                    ls.h, ls.delta
5145                ));
5146            }
5147            if !(ls.weight.is_finite() && ls.weight > 0.0) {
5148                return Err(format!(
5149                    "residual cascade state: level {li} has non-positive prior weight {}",
5150                    ls.weight
5151                ));
5152            }
5153            if ls.centers.len() % dim != 0 {
5154                return Err(format!(
5155                    "residual cascade state: level {li} centers length {} not a multiple of dim {dim}",
5156                    ls.centers.len()
5157                ));
5158            }
5159            let n_centers = ls.centers.len() / dim;
5160            let col_offset = ls.col_offset as usize;
5161            if col_offset != expected_offset {
5162                return Err(format!(
5163                    "residual cascade state: level {li} col_offset {col_offset} ≠ expected {expected_offset}"
5164                ));
5165            }
5166            let mut grid = HashGrid::new(ls.delta, dim);
5167            let mut centers = Vec::with_capacity(n_centers);
5168            for j in 0..n_centers {
5169                let mut c = [0.0_f64; 3];
5170                for a in 0..dim {
5171                    let v = ls.centers[j * dim + a];
5172                    if !v.is_finite() {
5173                        return Err(format!(
5174                            "residual cascade state: non-finite center coordinate at level {li}, center {j}"
5175                        ));
5176                    }
5177                    c[a] = v;
5178                }
5179                grid.insert(j as u32, &c);
5180                centers.push(c);
5181                net.push(c);
5182                let col = col_offset + j;
5183                if col >= m {
5184                    return Err(format!(
5185                        "residual cascade state: level {li} column {col} exceeds m {m}"
5186                    ));
5187                }
5188                pen_diag[col] = ls.weight;
5189            }
5190            expected_offset = col_offset + n_centers;
5191            levels.push(Level {
5192                h: ls.h,
5193                delta: ls.delta,
5194                weight: ls.weight,
5195                centers,
5196                col_offset,
5197                grid,
5198            });
5199        }
5200        if expected_offset != m {
5201            return Err(format!(
5202                "residual cascade state: column accounting mismatch (dim+1+Σcenters = {expected_offset} ≠ m {m})"
5203            ));
5204        }
5205        if state.coeff.len() != m {
5206            return Err(format!(
5207                "residual cascade state: coeff length {} ≠ m {m}",
5208                state.coeff.len()
5209            ));
5210        }
5211        if state.predict_chol.len() != m * m {
5212            return Err(format!(
5213                "residual cascade state: predict_chol must be m×m = {m}² = {}, got {}",
5214                m * m,
5215                state.predict_chol.len()
5216            ));
5217        }
5218        for (i, v) in state
5219            .coeff
5220            .iter()
5221            .chain(state.predict_chol.iter())
5222            .enumerate()
5223        {
5224            if !v.is_finite() {
5225                return Err(format!("residual cascade state: non-finite entry at {i}"));
5226            }
5227        }
5228        for g in 0..m {
5229            let piv = state.predict_chol[g * m + g];
5230            if !(piv.is_finite() && piv > 0.0) {
5231                return Err(format!(
5232                    "residual cascade state: non-positive Cholesky pivot {piv} at index {g}"
5233                ));
5234            }
5235        }
5236        gam_problem::validate_log_strength(state.log_lambda)
5237            .map_err(|error| format!("residual cascade state: {error}"))?;
5238        if !(state.sigma2.is_finite()
5239            && state.sigma2 > 0.0
5240            && state.restricted_loglik.is_finite()
5241            && state.rss_pen.is_finite())
5242        {
5243            return Err(format!(
5244                "residual cascade state: invalid scalars (log_lambda={}, sigma2={}, restricted_loglik={}, rss_pen={})",
5245                state.log_lambda, state.sigma2, state.restricted_loglik, state.rss_pen
5246            ));
5247        }
5248        let core = Core {
5249            dim,
5250            metric: metric3,
5251            z_lo,
5252            z_range,
5253            sobolev_s: state.sobolev_s,
5254            levels,
5255            net,
5256            m,
5257            row_ptr: Vec::new(),
5258            col_idx: Vec::new(),
5259            vals: Vec::new(),
5260            w: Vec::new(),
5261            y: Vec::new(),
5262            z: Vec::new(),
5263            rhs: Vec::new(),
5264            ytwy: 0.0,
5265            gram_diag: Vec::new(),
5266            pen_diag,
5267            pen_logdet_const: state.pen_logdet_const,
5268            dense_gram: None,
5269            predict_chol: Some(state.predict_chol.clone()),
5270        };
5271        Ok(ResidualCascadeFit {
5272            core: Arc::new(core),
5273            training_sample_size: std::num::NonZeroUsize::new(training_sample_size)
5274                .expect("nonzero wire count remains nonzero after conversion"),
5275            predict_chol: None,
5276            // The restored core carries the dense factor itself, so
5277            // `solve_coeff` replays through it; there is no CSR design left to
5278            // assemble a sparse system from.
5279            predict_sparse: None,
5280            coeff: state.coeff.clone(),
5281            log_lambda: state.log_lambda,
5282            sigma2: state.sigma2,
5283            restricted_loglik: state.restricted_loglik,
5284            rss_pen: state.rss_pen,
5285            certificate: CascadeCertificate {
5286                solve_rel_residual: 0.0,
5287                solve_iters: 0,
5288                logdet_method: LogdetMethod::DenseExact,
5289            },
5290            refinement: None,
5291        })
5292    }
5293}
5294
5295#[derive(Clone, Copy, Debug, PartialEq)]
5296enum RefinementDecision {
5297    Converged(RefinementCertificate),
5298    Refine,
5299    Underresolved {
5300        evidence: Option<RefinementCertificate>,
5301        obstruction: RefinementObstruction,
5302    },
5303}
5304
5305/// Turn the typed next-level assessment and the comparison against it into the
5306/// only three legal refinement transitions.
5307///
5308/// `evidence` is `None` in exactly two situations, and they are opposite ones:
5309/// the screen already proved the level warranted (so the exact comparison was
5310/// skipped as redundant, and the assessment left room to take it), or a
5311/// structural cap stopped the candidate set from being formed at all (so no
5312/// comparison exists). The first can only reach a `GainBound` assessment and
5313/// the second only a `CapacityExceeded` one, which is what lets one `None`
5314/// serve both without ambiguity.
5315///
5316/// A capacity limit can still yield a fit — that is the point of computing the
5317/// comparison before consulting the budget: a level nobody should add does not
5318/// become a refusal because there was also no room for it.
5319fn decide_refinement(
5320    assessment: NextLevelAssessment,
5321    evidence: Option<RefinementCertificate>,
5322) -> RefinementDecision {
5323    match assessment {
5324        // Only an empty net certifies zero remaining gain outright: there is no
5325        // candidate set, so there is nothing to charge and nothing to buy.
5326        NextLevelAssessment::EmptyNet => {
5327            RefinementDecision::Converged(RefinementCertificate::EXHAUSTED)
5328        }
5329        NextLevelAssessment::GainBound(_) => match evidence {
5330            Some(evidence) if !evidence.warrants_refinement() => {
5331                RefinementDecision::Converged(evidence)
5332            }
5333            _ => RefinementDecision::Refine,
5334        },
5335        NextLevelAssessment::CapacityExceeded { obstruction, .. } => match evidence {
5336            Some(evidence) if !evidence.warrants_refinement() => {
5337                RefinementDecision::Converged(evidence)
5338            }
5339            evidence => RefinementDecision::Underresolved {
5340                evidence,
5341                obstruction,
5342            },
5343        },
5344    }
5345}
5346
5347/// The inputs one cascade fit is derived from, kept together so that a CANDIDATE
5348/// level can be materialized — built, solved, and compared — rather than only
5349/// bounded. The refinement decision needs a log-determinant of the candidate
5350/// Schur complement, and a log-determinant is not a quantity any matrix-free
5351/// bracket produces: it needs the whole spectrum, which is to say the design.
5352#[derive(Clone, Copy)]
5353struct CascadeRequest<'a> {
5354    xs: &'a [&'a [f64]],
5355    y: &'a [f64],
5356    w: &'a [f64],
5357    metric: &'a [f64],
5358    sobolev_s: f64,
5359}
5360
5361impl CascadeRequest<'_> {
5362    fn build(&self, plan: &[LevelPlan]) -> Result<ResidualCascadeDesign, String> {
5363        ResidualCascadeDesign::build_from_plan(
5364            self.xs,
5365            self.y,
5366            self.w,
5367            self.metric,
5368            self.sobolev_s,
5369            plan,
5370        )
5371    }
5372
5373    /// The exact nested-model comparison for ONE pending candidate set: build
5374    /// the design with the COMPLETE set appended, minimize at the incumbent's
5375    /// λ, and difference the two restricted log-likelihoods.
5376    ///
5377    /// This is available past every capacity budget the automatic route
5378    /// enforces, and that is the point. `CERTIFIED_SPECTRUM_MAX` bounds the
5379    /// λ-independent Schur eigendecomposition the score SEARCH is certified in,
5380    /// and `n − nullity` bounds the rank that search needs to have a stationary
5381    /// point at all; a single evaluation at a FIXED λ needs neither — only a
5382    /// factorization, which the sparse route supplies far wider. So the question
5383    /// "does one more level explain the data better?" has an exact answer
5384    /// exactly where the cascade used to have only a bound (#2759).
5385    ///
5386    /// At the profiled σ̂² the identity
5387    ///
5388    /// ```text
5389    ///     2·evidence = dof·log(rss_pen/rss_pen_refined) − occam
5390    /// ```
5391    ///
5392    /// holds term by term — the `rss_pen/σ̂² = dof` quadratic cancels — so the
5393    /// candidate set's Occam factor is READ OFF the two fits rather than formed
5394    /// a second time from the Schur determinant it equals. That identity is
5395    /// what fixes the comparison to ONE λ: the incumbent's, which is its own
5396    /// REML optimum. The refined design's optimum is weakly higher than its
5397    /// value there, so the comparison leans toward stopping, and
5398    /// `the_refinement_stops_where_the_evidence_turns_over_and_the_truth_agrees_2759`
5399    /// charges that lean directly — it sweeps six λ on a design strictly wider
5400    /// than the one that was minted and requires that none of them win the
5401    /// comparison back.
5402    fn candidate_level_evidence(
5403        &self,
5404        plan: &[LevelPlan],
5405        exponent: f64,
5406        extends_last: bool,
5407        supported: &[[f64; 3]],
5408        fit: &ResidualCascadeFit,
5409    ) -> Result<Option<RefinementCertificate>, String> {
5410        if supported.is_empty() {
5411            // Every candidate is an exactly zero column. There is no comparison
5412            // to make: such a set cannot move the objective and cannot charge an
5413            // Occam factor.
5414            return Ok(Some(RefinementCertificate::EXHAUSTED));
5415        }
5416        let mut refined_plan = plan.to_vec();
5417        if extends_last {
5418            // Re-assessing the finest radius: the COMPLETE set there is the
5419            // union of what capacity let the level take and what it left
5420            // behind, and asking for the whole level reproduces exactly that
5421            // union. `extend_net` is greedy over a fixed order and every center
5422            // it plants is more than `h` from every other, so no member of the
5423            // complete set can be covered by the partial selection, and no
5424            // non-member can escape being covered by one.
5425            let last = refined_plan
5426                .last_mut()
5427                .expect("the plan always carries a level");
5428            let mut centers = last.centers.take().unwrap_or_default();
5429            centers.extend_from_slice(supported);
5430            last.centers = Some(centers);
5431        } else {
5432            refined_plan.push(LevelPlan {
5433                exponent,
5434                centers: Some(supported.to_vec()),
5435            });
5436        }
5437        let refined_design = self.build(&refined_plan)?;
5438        let refined = refined_design.fit_at(fit.log_lambda, None)?;
5439        if refined.certificate.logdet_method == LogdetMethod::Slq {
5440            // Past the sparse factor's fill budget the candidate design's
5441            // log-determinant is a stochastic point estimate, and a point
5442            // estimate cannot underwrite a convergence certificate. That is not
5443            // a failure — it is the absence of a comparison, which is exactly
5444            // what `None` says, and it can only ever keep the cascade refining
5445            // or make a capacity refusal honest.
5446            log::debug!(
5447                "[cascade] candidate level at exponent={exponent} has no exact comparison: the \
5448                 log-determinant fell back to the stochastic estimate at {} columns",
5449                refined_design.core.m
5450            );
5451            return Ok(None);
5452        }
5453        level_evidence(
5454            fit,
5455            &refined,
5456            (self.y.len() - refined_design.core.nullity()) as f64,
5457        )
5458        .map(Some)
5459    }
5460}
5461
5462/// Turn one incumbent fit and one refined fit AT THE SAME λ into the comparison
5463/// the refinement decides on.
5464///
5465/// The refined design must be the incumbent's plus a candidate set, and both
5466/// must carry their own profiled σ̂²; then `rss_pen/σ̂² = dof` on both sides and
5467/// the restricted log-likelihood difference is
5468///
5469/// ```text
5470///     2·evidence = dof·log(rss_pen/rss_pen_refined) − occam
5471/// ```
5472///
5473/// with `occam = log det(S/(λd))` the candidate set's Occam factor. That is an
5474/// identity, not an approximation, so the Occam term is READ OFF the two fits
5475/// rather than formed a second time from the Schur determinant it equals.
5476fn level_evidence(
5477    fit: &ResidualCascadeFit,
5478    refined: &ResidualCascadeFit,
5479    dof: f64,
5480) -> Result<RefinementCertificate, String> {
5481    let scale = EvidenceScale {
5482        rss_pen: fit.rss_pen,
5483        dof,
5484    };
5485    // A superset design minimizes the same objective over a superset, so the
5486    // decrease is non-negative, and the Occam factor of a PSD Schur complement
5487    // is too; both are clamped against their own rounding rather than trusted
5488    // to stay on the right side of zero.
5489    let gain = (fit.rss_pen - refined.rss_pen).max(0.0);
5490    let evidence = refined.restricted_loglik - fit.restricted_loglik;
5491    let occam = (dof * (fit.rss_pen / refined.rss_pen).ln() - 2.0 * evidence).max(0.0);
5492    if !(gain.is_finite() && occam.is_finite() && evidence.is_finite() && dof > 0.0) {
5493        return Err(format!(
5494            "residual cascade refinement: the candidate level comparison is not finite \
5495             (gain {gain}, occam {occam}, evidence {evidence}, dof {dof})"
5496        ));
5497    }
5498    Ok(RefinementCertificate {
5499        gain,
5500        occam,
5501        tolerance: scale.break_even_gain(occam),
5502        evidence,
5503    })
5504}
5505
5506/// Fit the full magic-default cascade: start at `INITIAL_LEVELS`, REML-fit, and
5507/// refine (add a level, refit, re-select λ) until one more level no longer
5508/// earns its own Occam factor — until the marginal likelihood of the design
5509/// with the complete candidate set appended, at the same λ, stops rising. A
5510/// genuinely empty next-level net certifies zero remaining gain against a zero
5511/// charge; a structural capacity reached while the evidence is still rising is
5512/// a typed [`ResidualCascadeError::Underresolved`] carrying the retained work
5513/// and that comparison, never a fit.
5514pub fn fit_residual_cascade(
5515    xs: &[&[f64]],
5516    y: &[f64],
5517    w: &[f64],
5518    metric: &[f64],
5519    sobolev_s: f64,
5520) -> Result<ResidualCascadeFit, ResidualCascadeError> {
5521    let request = CascadeRequest {
5522        xs,
5523        y,
5524        w,
5525        metric,
5526        sobolev_s,
5527    };
5528    let mut plan: Vec<LevelPlan> = (0..INITIAL_LEVELS)
5529        .map(|level| LevelPlan {
5530            exponent: level as f64,
5531            centers: None,
5532        })
5533        .collect();
5534    loop {
5535        let design = request.build(&plan)?;
5536        let levels = plan.len();
5537        // Quasi-uniformity guard (issue #1032, caveat 2): if the metric has
5538        // collapsed the cloud onto a near-degenerate sheet in scaled
5539        // coordinates, the BPX iteration bound no longer holds. Refuse the
5540        // iterative solve up front with a typed signal before paying an
5541        // unbounded CG or grinding to CG_MAX_ITERS. (The guard is checked at
5542        // the root level only — refinement adds finer nets to the SAME scaled
5543        // cloud, so the aspect ratio is invariant under added levels.) The
5544        // typed computation refusal propagates through the selected cascade
5545        // route; callers must not silently replace this estimator with another
5546        // one.
5547        if levels == INITIAL_LEVELS && !design.quasi_uniformity_certified() {
5548            return Err(format!(
5549                "residual cascade: metric-scaled aspect ratio {:.3e} exceeds the \
5550                 quasi-uniformity ceiling {QUASI_UNIFORMITY_MAX_ASPECT:.0e}; the BPX \
5551                 iteration bound is not trustworthy on this (near-degenerate) metric",
5552                design.metric_scaled_aspect_ratio()
5553            )
5554            .into());
5555        }
5556        let mut fit = design.fit_reml()?;
5557        // The realized CG iteration count at this cascade depth is the runtime
5558        // tell of the BPX n-independence bound (issue #1032 caveat: a count
5559        // creeping toward CG_MAX_ITERS means the quasi-uniformity guard's static
5560        // aspect-ratio check was too lenient for this cloud). It is exposed
5561        // STRUCTURALLY rather than over stderr: the per-depth count and backward
5562        // error ride on `fit.certificate` (`solve_iters` — 0 on the dense route,
5563        // the PCG count on the iterative route — and `solve_rel_residual`), so a
5564        // caller that wants to watch the bound reads them off the returned fit
5565        // instead of scraping log lines. (A library solve never writes to
5566        // stderr.)
5567        let scale = EvidenceScale {
5568            rss_pen: fit.rss_pen,
5569            dof: (y.len() - design.core.nullity()) as f64,
5570        };
5571        // Everything the cascade could still add has to be certified, not just
5572        // the next dyadic level. When a capacity budget truncated the finest
5573        // level, the candidates it left behind at ITS radius are still
5574        // addable — so they are assessed first, and the next dyadic level only
5575        // after they pass. A fit is minted only when EVERY pending candidate
5576        // set fails to earn its own Occam factor, which is the same per-level
5577        // claim the complete-level ladder always made, asserted once per set.
5578        let last = plan.last().expect("the plan always carries a level");
5579        let mut pending: Vec<f64> = Vec::with_capacity(2);
5580        if last.centers.is_some() {
5581            pending.push(last.exponent);
5582        }
5583        pending.push(last.exponent + 1.0);
5584        let mut certified: Option<RefinementCertificate> = None;
5585        let mut refinement: Option<(f64, bool, bool, Vec<[f64; 3]>)> = None;
5586        for exponent in pending {
5587            let planned = design.plan_level_at_exponent(&fit, exponent, Some(scale))?;
5588            let (complete, extends_last) = (planned.complete, planned.extends_last);
5589            let room = !planned.selection.is_empty();
5590            // The free half of the comparison, read off the bracket the plan
5591            // already carries. When it PROVES the level warranted there is
5592            // nothing left to decide and the refined design is not built —
5593            // which matters most where building it is worst: a
5594            // certified-spectrum refusal is a memory boundary, and paying that
5595            // memory to confirm a conclusion already proved would be the exact
5596            // cost the boundary exists to avoid.
5597            let screened = planned
5598                .gain
5599                .as_ref()
5600                .map(|bracket| bracket.screened_comparison(scale))
5601                .filter(RefinementCertificate::warrants_refinement);
5602            // Two caps stop the candidate set from being FORMED rather than
5603            // merely from being taken, and both are caps on the shape of the
5604            // plan itself, so no design carrying that set exists to compare
5605            // against. Every other outcome admits the attempt.
5606            let constructible = !matches!(
5607                planned.assessment,
5608                NextLevelAssessment::EmptyNet
5609                    | NextLevelAssessment::CapacityExceeded {
5610                        obstruction: RefinementObstruction::LevelCapacity { .. }
5611                            | RefinementObstruction::CenterCapacity { .. },
5612                        ..
5613                    }
5614            );
5615            // A level is never declared SPENT on a bound: the screen can only
5616            // ever prove the positive, so an inconclusive screen sends the
5617            // decision to the design itself. That is the whole of this issue's
5618            // remaining half — a refusal must not rest on a bound while the
5619            // number is one factorization away.
5620            let evidence = match (screened, constructible) {
5621                (Some(proved), _) => Some(proved),
5622                (None, true) => {
5623                    request.candidate_level_evidence(
5624                        &plan,
5625                        exponent,
5626                        extends_last,
5627                        &planned.supported,
5628                        &fit,
5629                    )?
5630                }
5631                (None, false) => None,
5632            };
5633            // The comparison the decision below is taken on, plus the bracket
5634            // that screened it: a run record that shows only one of them cannot
5635            // say whether the exact route ran or the screen carried it.
5636            log::debug!(
5637                "[cascade] exponent={exponent} gain_bracket={} evidence={} complete={complete} \
5638                 extends_last={extends_last} room={room}",
5639                planned.gain.as_ref().map_or_else(
5640                    || "none (empty net, or a proposal a structural cap stopped)".to_string(),
5641                    |bracket| format!(
5642                        "[{:.6e}, {:.6e}] in {} cg steps, hadamard occam {:.6e}",
5643                        bracket.lower, bracket.upper, bracket.iterations, bracket.hadamard_occam
5644                    )
5645                ),
5646                evidence.map_or_else(
5647                    || "none (the candidate set was never formed)".to_string(),
5648                    |evidence| evidence.to_string()
5649                ),
5650            );
5651            match decide_refinement(planned.assessment, evidence) {
5652                RefinementDecision::Converged(spent) => {
5653                    // The binding set is the one that came CLOSEST to earning a
5654                    // level, which is the largest evidence and not the largest
5655                    // gain: gains from candidate sets of different width are not
5656                    // comparable, and comparing them is what this issue was.
5657                    if certified.is_none_or(|best| spent.evidence > best.evidence) {
5658                        certified = Some(spent);
5659                    }
5660                }
5661                RefinementDecision::Refine => {
5662                    refinement = Some((exponent, complete, extends_last, planned.selection));
5663                    break;
5664                }
5665                RefinementDecision::Underresolved {
5666                    evidence,
5667                    obstruction,
5668                } => {
5669                    return Err(ResidualCascadeError::Underresolved {
5670                        checkpoint: ResidualCascadeCheckpoint::new(fit),
5671                        evidence,
5672                        obstruction,
5673                    });
5674                }
5675            }
5676        }
5677        match refinement {
5678            None => {
5679                fit.refinement = Some(certified.unwrap_or(RefinementCertificate::EXHAUSTED));
5680                return Ok(fit);
5681            }
5682            Some((exponent, complete, extends_last, mut selection)) => {
5683                if extends_last {
5684                    let last = plan.last_mut().expect("the plan always carries a level");
5685                    let mut centers = last.centers.take().unwrap_or_default();
5686                    centers.append(&mut selection);
5687                    last.centers = Some(centers);
5688                } else {
5689                    plan.push(LevelPlan {
5690                        exponent,
5691                        centers: if complete { None } else { Some(selection) },
5692                    });
5693                }
5694            }
5695        }
5696    }
5697}
5698
5699#[cfg(test)]
5700mod refinement_decision_tests {
5701    use super::*;
5702
5703    const TOLERANCE: f64 = 0.25;
5704
5705    /// The refinement gain bracket is checked against the gain ITSELF, obtained
5706    /// by a route that shares no code with it: build the design with the
5707    /// candidate level appended, solve at the SAME fixed λ, and difference the
5708    /// two penalized objectives.
5709    ///
5710    /// This is the gate the whole of #2759 rests on. The shipped bound
5711    /// `‖g‖²/(λd)` is the `x = 0` member of the same family, so the three
5712    /// claims to establish are that the bracket CONTAINS the truth, that its
5713    /// upper end never exceeds the shipped number, and that it is materially
5714    /// tighter than it — a bound that merely reproduces `‖g‖²/(λd)` would leave
5715    /// every fixture in this issue exactly where it was.
5716    #[test]
5717    fn the_refinement_gain_bracket_contains_the_objective_decrease_it_bounds_2759() {
5718        let (x1, x2, y) = dense_fixture(24);
5719        let weights = vec![1.0; y.len()];
5720        let axes: [&[f64]; 2] = [&x1, &x2];
5721        let metric = [1.0, 1.0];
5722        let sobolev_s = 2.0;
5723        let plan: Vec<LevelPlan> = (0..3)
5724            .map(|level| LevelPlan {
5725                exponent: level as f64,
5726                centers: None,
5727            })
5728            .collect();
5729        let design =
5730            ResidualCascadeDesign::build_from_plan(&axes, &y, &weights, &metric, sobolev_s, &plan)
5731                .expect("cascade design");
5732        for log_lambda in [-4.0_f64, -1.0, 2.0] {
5733            let fit = design.fit_at(log_lambda, None).expect("fixed-lambda fit");
5734            let exponent = plan.len() as f64;
5735            let planned = design
5736                .plan_level_at_exponent(&fit, exponent, None)
5737                .expect("candidate level");
5738            let bracket = planned.gain.as_ref().expect("a complete candidate certifies");
5739            assert!(
5740                planned.complete,
5741                "the truth below is the COMPLETE candidate level; this fixture must not be \
5742                 capacity-truncated"
5743            );
5744
5745            // The independent route: the same cascade with the level appended.
5746            let mut extended = plan.clone();
5747            extended.push(LevelPlan {
5748                exponent,
5749                centers: None,
5750            });
5751            let refined = ResidualCascadeDesign::build_from_plan(
5752                &axes,
5753                &y,
5754                &weights,
5755                &metric,
5756                sobolev_s,
5757                &extended,
5758            )
5759            .expect("refined design");
5760            let refined_fit = refined
5761                .fit_at(log_lambda, None)
5762                .expect("refined fixed-lambda fit");
5763            let truth = fit.rss_pen - refined_fit.rss_pen;
5764
5765            // The solves on both sides carry `CG_RTOL` backward error, and the
5766            // difference of two objectives near each other loses that
5767            // cancellation's worth of digits; charge it rather than demand an
5768            // exact inequality on a differenced quantity.
5769            let slack = CG_RTOL * fit.rss_pen.abs().max(refined_fit.rss_pen.abs());
5770            assert!(
5771                truth >= -slack,
5772                "adding a level cannot INCREASE the penalized objective: {truth} at \
5773                 log lambda {log_lambda}"
5774            );
5775            assert!(
5776                truth <= bracket.upper + slack,
5777                "the certified bound is not an upper bound: truth {truth} exceeds \
5778                 {} at log lambda {log_lambda}",
5779                bracket.upper
5780            );
5781            assert!(
5782                truth >= bracket.lower - slack,
5783                "the bracket's lower end is not a lower bound: truth {truth} below {} at \
5784                 log lambda {log_lambda}",
5785                bracket.lower
5786            );
5787
5788            // The shipped bound, recomputed here so the comparison is against a
5789            // number and not against a memory.
5790            let shipped = shipped_zeroth_order_gain_bound(&design, &fit, exponent);
5791            assert!(
5792                bracket.upper <= shipped * (1.0 + f64::EPSILON.sqrt()),
5793                "the new certificate is looser than the one it replaces: {} against {shipped}",
5794                bracket.upper
5795            );
5796
5797            // Run to the closed bracket as well. The decision-driven bracket
5798            // above stops the moment the comparison is settled, so how tight it
5799            // happens to be there is a property of the threshold; what the
5800            // machinery can REACH is the separate claim, and the only one worth
5801            // a bar.
5802            let closed = design
5803                .plan_level_at_exponent(&fit, exponent, None)
5804                .expect("candidate level")
5805                .gain
5806                .expect("a complete candidate certifies");
5807            assert!(
5808                truth <= closed.upper + slack && truth >= closed.lower - slack,
5809                "the closed bracket [{}, {}] does not contain {truth} at log lambda \
5810                 {log_lambda}",
5811                closed.lower,
5812                closed.upper
5813            );
5814            assert!(
5815                closed.upper <= bracket.upper * (1.0 + f64::EPSILON.sqrt()),
5816                "iterating further made the bound worse: {} against {}",
5817                closed.upper,
5818                bracket.upper
5819            );
5820            assert!(
5821                closed.upper < shipped,
5822                "the closed bracket did not tighten the shipped bound at log lambda \
5823                 {log_lambda}: {} against {shipped} (truth {truth}, {} CG steps)",
5824                closed.upper,
5825                closed.iterations
5826            );
5827            // The closed bracket's own width is what says it converged; asking
5828            // it to be near the truth would be asking the same question twice.
5829            assert!(
5830                closed.upper - closed.lower <= 0.05 * closed.upper.max(f64::MIN_POSITIVE),
5831                "the bracket did not close at log lambda {log_lambda}: [{}, {}] after {} steps",
5832                closed.lower,
5833                closed.upper,
5834                closed.iterations
5835            );
5836            println!(
5837                "#2759 log_lambda={log_lambda} truth={truth:.6e} decided=[{:.6e}, {:.6e}]@{} \
5838                 closed=[{:.6e}, {:.6e}]@{} shipped={shipped:.6e} tightening={:.2}x",
5839                bracket.lower,
5840                bracket.upper,
5841                bracket.iterations,
5842                closed.lower,
5843                closed.upper,
5844                closed.iterations,
5845                shipped / closed.upper.max(f64::MIN_POSITIVE),
5846            );
5847        }
5848    }
5849
5850    /// `‖X₂ᵀW r̂‖² / (λ·d)`: the bound this issue replaces, rebuilt from the
5851    /// design so the comparison above is against a computed number.
5852    fn shipped_zeroth_order_gain_bound(
5853        design: &ResidualCascadeDesign,
5854        fit: &ResidualCascadeFit,
5855        exponent: f64,
5856    ) -> f64 {
5857        let core = &design.core;
5858        let h = core.levels[0].h * 0.5_f64.powf(exponent);
5859        let mut net = core.net.clone();
5860        let candidates = extend_net(&mut net, &core.z, core.dim, h, &core.z_range);
5861        let delta = OVERLAP * h;
5862        let mut grid = HashGrid::new(delta, core.dim);
5863        for (j, c) in candidates.iter().enumerate() {
5864            grid.insert(j as u32, c);
5865        }
5866        let residual = core.residuals(&fit.coeff);
5867        let mut g = vec![0.0_f64; candidates.len()];
5868        for (i, zi) in core.z.iter().enumerate() {
5869            let weighted = core.w[i] * residual[i];
5870            grid.for_neighbors(zi, |j| {
5871                let radius = dist2(zi, &candidates[j as usize], core.dim).sqrt() / delta;
5872                g[j as usize] += weighted * wendland(radius);
5873            });
5874        }
5875        let energy: f64 = g.iter().map(|value| value * value).sum();
5876        let lambda = fit.log_lambda.exp();
5877        energy / (lambda * level_weight(exponent, core.sobolev_s, core.dim))
5878    }
5879
5880    /// A comparison that says a candidate set buys `evidence` nats.
5881    fn comparison(evidence: f64) -> RefinementCertificate {
5882        RefinementCertificate {
5883            gain: TOLERANCE + evidence,
5884            occam: 1.0,
5885            tolerance: TOLERANCE,
5886            evidence,
5887        }
5888    }
5889
5890    #[test]
5891    fn only_an_empty_net_or_a_losing_comparison_converges() {
5892        assert_eq!(
5893            decide_refinement(NextLevelAssessment::EmptyNet, None),
5894            RefinementDecision::Converged(RefinementCertificate::EXHAUSTED)
5895        );
5896        assert_eq!(
5897            decide_refinement(NextLevelAssessment::GainBound(0.2), Some(comparison(-1.0))),
5898            RefinementDecision::Converged(comparison(-1.0))
5899        );
5900        // Exactly break-even is NOT an improvement: the finer prior has to earn
5901        // its Occam factor, and matching it is not earning it.
5902        assert_eq!(
5903            decide_refinement(NextLevelAssessment::GainBound(0.2), Some(comparison(0.0))),
5904            RefinementDecision::Converged(comparison(0.0))
5905        );
5906        assert_eq!(
5907            decide_refinement(NextLevelAssessment::GainBound(0.3), Some(comparison(1.0))),
5908            RefinementDecision::Refine
5909        );
5910        // The screen skipped the exact comparison, which it may do only when it
5911        // proved the level warranted AND there was room to take it.
5912        assert_eq!(
5913            decide_refinement(NextLevelAssessment::GainBound(0.3), None),
5914            RefinementDecision::Refine
5915        );
5916    }
5917
5918    #[test]
5919    fn capacity_reached_while_the_evidence_rises_is_underresolved() {
5920        let obstruction = RefinementObstruction::LevelCapacity {
5921            levels: MAX_LEVELS,
5922            maximum_levels: MAX_LEVELS,
5923        };
5924        assert_eq!(
5925            decide_refinement(
5926                NextLevelAssessment::CapacityExceeded {
5927                    obstruction,
5928                    gain_bound: 0.3,
5929                },
5930                Some(comparison(1.0)),
5931            ),
5932            RefinementDecision::Underresolved {
5933                evidence: Some(comparison(1.0)),
5934                obstruction,
5935            }
5936        );
5937
5938        // A cap that stopped the candidate set from being FORMED leaves nothing
5939        // to compare against, and an absent comparison can never certify the
5940        // discretization spent.
5941        let center_obstruction = RefinementObstruction::CenterCapacity {
5942            centers: MAX_CENTERS + 1,
5943            maximum_centers: MAX_CENTERS,
5944        };
5945        assert_eq!(
5946            decide_refinement(
5947                NextLevelAssessment::CapacityExceeded {
5948                    obstruction: center_obstruction,
5949                    gain_bound: f64::INFINITY,
5950                },
5951                None,
5952            ),
5953            RefinementDecision::Underresolved {
5954                evidence: None,
5955                obstruction: center_obstruction,
5956            }
5957        );
5958    }
5959
5960    /// A level nobody should add does not become a refusal because there was
5961    /// also no room for it. This is why the exact comparison runs BEFORE the
5962    /// budget is consulted, and it is the transition the whole of #2759's
5963    /// second half turns on: the rank-maximal designs refuse only because the
5964    /// criterion they are compared against charges nothing for width.
5965    #[test]
5966    fn capacity_does_not_block_a_losing_comparison() {
5967        assert_eq!(
5968            decide_refinement(
5969                NextLevelAssessment::CapacityExceeded {
5970                    obstruction: RefinementObstruction::LevelCapacity {
5971                        levels: MAX_LEVELS,
5972                        maximum_levels: MAX_LEVELS,
5973                    },
5974                    gain_bound: 0.2,
5975                },
5976                Some(comparison(-1.0)),
5977            ),
5978            RefinementDecision::Converged(comparison(-1.0))
5979        );
5980    }
5981
5982    /// A 2-D fixture small enough to stay under the dense sizing cap, with a
5983    /// response that is smooth plus a deterministic wobble so the profiled
5984    /// residual is not degenerate at any λ.
5985    fn dense_fixture(side: usize) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
5986        let mut x1 = Vec::with_capacity(side * side);
5987        let mut x2 = Vec::with_capacity(side * side);
5988        let mut y = Vec::with_capacity(side * side);
5989        for i in 0..side {
5990            for j in 0..side {
5991                let a = i as f64 / (side - 1) as f64;
5992                let b = j as f64 / (side - 1) as f64;
5993                x1.push(a);
5994                x2.push(b);
5995                y.push((2.3 * a).sin() + (1.7 * b).cos() + 0.07 * ((3 * i + 5 * j) % 7) as f64);
5996            }
5997        }
5998        (x1, x2, y)
5999    }
6000
6001    /// Column count of a `dense_fixture(side)` cascade at each level count, so
6002    /// the two width regimes the certified route now distinguishes are read off
6003    /// the design rather than guessed from the net arithmetic.
6004    #[test]
6005    fn zz_measure_cascade_width_by_level_count_2546() {
6006        for levels in 4..=8 {
6007            let (x1, x2, y) = dense_fixture(6);
6008            let weights = vec![1.0; y.len()];
6009            let axes: [&[f64]; 2] = [&x1, &x2];
6010            let design = ResidualCascadeDesign::build(&axes, &y, &weights, &[1.0, 1.0], 2.0, levels)
6011                .expect("cascade design");
6012            let m = design.core.m;
6013            println!(
6014                "#2546 levels={levels} m={m} gram_cached={} certified={}",
6015                design.core.dense_gram.is_some(),
6016                m <= CERTIFIED_SPECTRUM_MAX
6017            );
6018        }
6019        // The net is GEOMETRIC, not data-subsampled: `dense_fixture(6)` above is
6020        // 36 rows and still refines to 1725 columns at `levels = 6`. So the
6021        // identifiability the certified search needs -- every Schur mode carried
6022        // by the data -- is a race between a net set by `levels` and a sample set
6023        // by `side`, and neither the level table above nor the net arithmetic
6024        // says where it is won. Two guesses at it were wrong by 13 and by 8
6025        // columns respectively, so it is measured here instead.
6026        //
6027        // The band is swept DENSELY (every side from 45 to 64) rather than sampled,
6028        // because a coarse sample of this curve is what produced both wrong guesses
6029        // and then a third wrong claim drawn from the sample itself -- that no side
6030        // below 64 can be identified, inferred from 45, 50 and 60 all being short
6031        // by a small margin. The MARGIN is not monotone in `side` either, so
6032        // neighbouring sides disagree and only every-side settles it. Sides above
6033        // the band stay coarse: past the net discontinuity `m` falls away from `n`
6034        // and the outcome is no longer close.
6035        let mut sides: Vec<usize> = (45..=64).collect();
6036        sides.extend([70, 80, 90]);
6037        let mut identified_in_band: Vec<(usize, usize, usize)> = Vec::new();
6038        for side in sides {
6039            let (x1, x2, y) = dense_fixture(side);
6040            let weights = vec![1.0; y.len()];
6041            let axes: [&[f64]; 2] = [&x1, &x2];
6042            let design = ResidualCascadeDesign::build(&axes, &y, &weights, &[1.0, 1.0], 2.0, 6)
6043                .expect("cascade design");
6044            let m = design.core.m;
6045            let n = y.len();
6046            let nullity = design.core.nullity();
6047            let identified = m - nullity <= n - nullity;
6048            let past_cache = m > DENSE_GRAM_MAX && design.core.dense_gram.is_none();
6049            let certified = m <= CERTIFIED_SPECTRUM_MAX;
6050            println!(
6051                "#2546-IDENT side={side} n={n} m={m} nullity={nullity} margin={} \
6052                 identified={identified} past_cache={past_cache} certified={certified}",
6053                m as i64 - n as i64
6054            );
6055            if side <= 64 && identified && past_cache && certified {
6056                identified_in_band.push((side, m, n));
6057            }
6058        }
6059        println!(
6060            "#2546-IDENT sides in 45..=64 that are past the cache, inside the budget \
6061             and identified: {identified_in_band:?}"
6062        );
6063    }
6064
6065    /// The width regime this issue existed to open: PAST the dense Gram cache,
6066    /// INSIDE the certified spectrum budget. Automatic REML must certify here.
6067    ///
6068    /// Before #2546 this design was fit-capable but never certifiable, because
6069    /// the proof was gated on the Gram CACHE rather than on the spectrum it is
6070    /// actually made of, so `fit_residual_cascade` could not finish at all once
6071    /// refinement crossed 1536 columns. The assertion is the certificate itself:
6072    /// a returned fit from `fit_reml` carries the KKT and ordering proofs, and
6073    /// its log-determinant route is exact.
6074    ///
6075    /// The fixture has MORE ROWS THAN COLUMNS on purpose, and that premise is
6076    /// now about keeping this gate on ITS subject rather than about a design
6077    /// nobody could fit. A cascade whose box-filling net outruns its sample —
6078    /// 36 rows against 1725 columns, say — used to be refused at every width,
6079    /// including widths under `DENSE_GRAM_MAX` where the route was always open;
6080    /// that was a first-order-loose score-value enclosure and not a property of
6081    /// the data, and such designs certify now
6082    /// (`auto_reml_certifies_a_design_the_data_cannot_identify`). Keeping this
6083    /// fixture rank-sufficient still matters: this gate is about the WIDTH
6084    /// regime between the Gram cache and the spectrum budget, and a fixture that
6085    /// also crossed the identifiability frontier would fold two claims into one
6086    /// green.
6087    #[test]
6088    fn auto_reml_certifies_past_the_dense_gram_cache() {
6089        // The grid side is SEARCHED for rather than pinned, because the two
6090        // premises pull against each other and neither is a property of the code
6091        // under test. The width has to land strictly between the Gram cache and
6092        // the spectrum budget, and the sample has to be at least as large as the
6093        // width: a design with FEWER rows than columns is a separate claim (see
6094        // `auto_reml_certifies_a_design_the_data_cannot_identify`, which is the
6095        // gate for it) and would be measured here instead of the capability. Six levels of box-filling net set a
6096        // floor on the width, and a finer data grid adds centres of its own, so
6097        // the admissible sides are a band.
6098        //
6099        // The band is NOT where counting upward from 45 suggests, because `m` is
6100        // not monotone in `side`. Measured by
6101        // `zz_measure_cascade_width_by_level_count_2546` at `levels = 6`:
6102        //
6103        //   side=45  n=2025  m=2038   13 short
6104        //   side=47  n=2209  m=2159   IDENTIFIED, 50 to spare
6105        //   side=50  n=2500  m=2508    8 short
6106        //   side=60  n=3600  m=3628   28 short
6107        //   side=70  n=4900  m=1922   IDENTIFIED
6108        //   side=80  n=6400  m=2667   identified
6109        //   side=90  n=8100  m=3637   identified, past the spectrum budget
6110        //
6111        // `m` climbs to 3628 at side=60 and then FALLS to 1922 at side=70: once
6112        // the sample is finer than the level-6 net's own spacing the net stops
6113        // adding centres for it, so width and sample decouple and the data
6114        // overtake the spectrum. That discontinuity is why "64x64 gives 4117
6115        // columns, so the band ends below 64" was a trend argument and not a
6116        // measurement.
6117        //
6118        // Below the discontinuity, though, `m` does not track `n` at a fixed
6119        // offset, and the MARGIN `m - n` is not monotone in `side` either. Swept
6120        // exhaustively over 45..=64 by `zz_measure_cascade_width_by_level_count_2546`:
6121        //
6122        //   45:+13  46: +9  47:-50  48:-63  49:+27  50: +8  51:+22  52:+21
6123        //   53:+24  54:+19  55:+27  56:+14  57:+20  58:+18  59:+20  60:+28
6124        //   61:+20  62:+22  63:+24  64:+21
6125        //
6126        // Sides 47 and 48 are a two-point DIP of -50 and -63 in a band that is
6127        // otherwise +8 to +28, and both satisfy all three conditions. So a fine
6128        // search below 64 does succeed, and "45, 50 and 60 are all short, so no
6129        // side below 64 is identified" was a third extrapolation over the same
6130        // curve -- it samples straight across the only two sides that work.
6131        //
6132        // The reason to prefer the candidate list below is therefore COST, not
6133        // reachability: side=70 is identified by 2978 columns and hits on the first
6134        // design build, where stepping from 46 pays for two builds to reach 47 and
6135        // a reader cannot tell a deliberate choice from a lucky one.
6136        //
6137        // So the candidates are the measured ones, in cost order, with the band's
6138        // edges kept after them: the search still self-heals if DENSE_GRAM_MAX or
6139        // the spectrum budget moves, but it does not pay for two dozen design
6140        // builds to rediscover a curve that has already been sampled.
6141        let mut fixture = None;
6142        for side in [70_usize, 80, 75, 85, 90, 100, 60, 50] {
6143            let (x1, x2, y) = dense_fixture(side);
6144            let weights = vec![1.0; y.len()];
6145            let m = {
6146                let axes: [&[f64]; 2] = [&x1, &x2];
6147                ResidualCascadeDesign::build(&axes, &y, &weights, &[1.0, 1.0], 2.0, 6)
6148                    .expect("cascade design")
6149                    .core
6150                    .m
6151            };
6152            if m > DENSE_GRAM_MAX && m <= CERTIFIED_SPECTRUM_MAX && m <= y.len() {
6153                println!("#2546 certified-past-cache fixture: side={side} m={m} rows={}", y.len());
6154                fixture = Some((x1, x2, y, weights, m));
6155                break;
6156            }
6157        }
6158        let (x1, x2, y, weights, m) = fixture.expect(
6159            "no candidate grid side puts the width between DENSE_GRAM_MAX and              CERTIFIED_SPECTRUM_MAX with at least as many rows as columns -- if the caps              moved, re-run zz_measure_cascade_width_by_level_count_2546 and take the              candidates from its sweep rather than extrapolating a trend, because m is              not monotone in side",
6160        );
6161        let axes: [&[f64]; 2] = [&x1, &x2];
6162        let design = ResidualCascadeDesign::build(&axes, &y, &weights, &[1.0, 1.0], 2.0, 6)
6163            .expect("cascade design");
6164        assert_eq!(design.core.m, m);
6165        assert!(
6166            design.core.dense_gram.is_none(),
6167            "premise: the fixture must be past the dense Gram cache, got {m} columns"
6168        );
6169        let fit = design
6170            .fit_reml()
6171            .expect("a design past the Gram cache but inside the spectrum budget must certify");
6172        assert_eq!(fit.certificate.logdet_method, LogdetMethod::DenseExact);
6173        assert!(
6174            fit.log_lambda().is_finite(),
6175            "certified selection must return a finite log lambda, got {}",
6176            fit.log_lambda()
6177        );
6178    }
6179
6180    /// The spectral residual handed to the interval extension carries only
6181    /// POSITIVE modes, and no more of them than the data can identify.
6182    ///
6183    /// `B = Z'WZ` for an `n × rank` whitened design, so `rank(B) ≤ n − nullity`;
6184    /// on a box-filling cascade over a small sample almost every column is a
6185    /// void-filling centre the data cannot pin, and the arithmetic returns
6186    /// roundoff for those directions. Carrying them as zeros costs exactness
6187    /// nothing on the scalar path and real enclosure width on the interval path,
6188    /// so the invariant is pinned here rather than left to the enclosure's
6189    /// behaviour.
6190    #[test]
6191    fn the_spectral_residual_carries_no_null_modes() {
6192        let (x1, x2, y) = dense_fixture(6);
6193        let weights = vec![1.0; y.len()];
6194        let axes: [&[f64]; 2] = [&x1, &x2];
6195        let design = ResidualCascadeDesign::build(&axes, &y, &weights, &[1.0, 1.0], 2.0, 6)
6196            .expect("cascade design");
6197        let core = &design.core;
6198        let identifiable = core.y.len() - core.nullity();
6199        assert!(
6200            core.m - core.nullity() > identifiable,
6201            "premise: the fixture must be rank-deficient (Schur rank {} against {identifiable} \
6202             identifiable directions)",
6203            core.m - core.nullity()
6204        );
6205        let profile = core.reml_profile().expect("spectral profile");
6206        let CascadeResidualForm::Spectral(spectrum) = &profile.residual else {
6207            panic!("this width must carry the spectral residual form");
6208        };
6209        assert!(
6210            spectrum.eigenvalue.iter().all(|&theta| theta > 0.0),
6211            "the spectral residual kept a non-positive mode"
6212        );
6213        assert_eq!(
6214            spectrum.eigenvalue.len(),
6215            spectrum.projected_square.len(),
6216            "mode and response-energy lists must stay aligned"
6217        );
6218        assert_eq!(spectrum.penalty.len(), spectrum.eigenvalue.len());
6219        assert!(
6220            spectrum.eigenvalue.len() <= identifiable,
6221            "kept {} modes against {identifiable} directions the data can identify",
6222            spectrum.eigenvalue.len()
6223        );
6224    }
6225
6226    #[test]
6227    fn auto_reml_refuses_past_the_certified_spectrum_budget() {
6228        // Two levels finer than `auto_reml_certifies_past_the_dense_gram_cache`,
6229        // which quadruples the finest box net twice over and takes the design
6230        // past the decomposition's memory budget on the same tiny row set. It
6231        // was ONE level finer while the budget admitted 2896 columns; #2758 took
6232        // the residency from seven-plus `m²` blocks to one packed triangle, the
6233        // derived cap moved with it, and level 7 (`m = 6704`, measured) now
6234        // certifies. The width is not asserted against a literal — the premise
6235        // below compares it to the budget itself.
6236        let (x1, x2, y) = dense_fixture(6);
6237        let weights = vec![1.0; y.len()];
6238        let axes: [&[f64]; 2] = [&x1, &x2];
6239        let design = ResidualCascadeDesign::build(&axes, &y, &weights, &[1.0, 1.0], 2.0, 8)
6240            .expect("iterative-route cascade design");
6241        assert!(
6242            design.core.m > CERTIFIED_SPECTRUM_MAX,
6243            "fixture must exercise the uncertifiable route, got {} columns",
6244            design.core.m
6245        );
6246
6247        let error = match design.fit_reml() {
6248            Ok(_) => panic!("auto-REML must not claim an enclosure it cannot form"),
6249            Err(error) => error,
6250        };
6251        assert!(matches!(
6252            error,
6253            ResidualCascadeError::RemlScoreProofUnavailable {
6254                columns,
6255                certified_spectrum_max: CERTIFIED_SPECTRUM_MAX,
6256            } if columns == design.core.m
6257        ));
6258
6259        let fixed = design
6260            .fit_at(0.0, None)
6261            .expect("the same iterative design remains fit-capable at fixed lambda");
6262        assert_eq!(fixed.log_lambda(), 0.0);
6263        // Refusing the PROOF is not the same as accepting a stochastic number.
6264        // The fixed-λ fit's log-determinant is still exact, from a sparse direct
6265        // Cholesky of the same normal equations.
6266        assert_eq!(fixed.certificate.logdet_method, LogdetMethod::SparseExact);
6267    }
6268
6269    /// Process high-water resident set size in bytes, or `None` where the
6270    /// kernel does not publish one.
6271    fn read_hwm_bytes() -> Option<f64> {
6272        let status = std::fs::read_to_string("/proc/self/status").ok()?;
6273        for line in status.lines() {
6274            if let Some(rest) = line.strip_prefix("VmHWM:") {
6275                let kb: f64 = rest.trim().trim_end_matches(" kB").trim().parse().ok()?;
6276                return Some(kb * 1024.0);
6277            }
6278        }
6279        None
6280    }
6281
6282    /// `(grid side, level count)` for the two widths the residency is
6283    /// differenced over. One fixture in both arms so the per-process baseline
6284    /// cancels; both designs PAST [`DENSE_GRAM_MAX`] so the persistent Gram
6285    /// cache is absent from both readings rather than from one.
6286    const PEAK_MEMORY_ARMS: [(usize, usize); 2] = [(70, 6), (70, 7)];
6287
6288    /// Marker the per-width child prints its reading behind, and the gate finds
6289    /// it by. One definition so the writer and the reader cannot drift.
6290    const CHILD_READING_MARKER: &str = "#2546-child ";
6291
6292    /// Build the certified spectral profile at one width and report the process
6293    /// high-water mark it reached, on stdout.
6294    ///
6295    /// Printed rather than returned because the gate reads it from a CHILD
6296    /// process, which is what makes the reading attributable.
6297    fn report_certified_spectrum_peak(arm: (usize, usize)) {
6298        let (side, levels) = arm;
6299        if read_hwm_bytes().is_none() {
6300            println!("{CHILD_READING_MARKER}levels={levels} vmhwm_unavailable");
6301            return;
6302        }
6303        let (x1, x2, y) = dense_fixture(side);
6304        let weights = vec![1.0; y.len()];
6305        let axes: [&[f64]; 2] = [&x1, &x2];
6306        let design = ResidualCascadeDesign::build(&axes, &y, &weights, &[1.0, 1.0], 2.0, levels)
6307            .expect("cascade design");
6308        let m = design.core.m;
6309        assert!(
6310            design.core.dense_gram.is_none(),
6311            "arm side={side} levels={levels} is m={m}, inside DENSE_GRAM_MAX={DENSE_GRAM_MAX}: \
6312             its persistent cache would enter one reading and not the other, and the difference \
6313             the gate takes would be biased downward by exactly the term it exists to bound"
6314        );
6315        let started = std::time::Instant::now();
6316        let profile = design.core.reml_profile().expect("spectral profile");
6317        let elapsed = started.elapsed().as_secs_f64();
6318        let modes = profile.modes.len();
6319        drop(profile);
6320        let hwm = read_hwm_bytes().expect("VmHWM was readable a moment ago");
6321        println!(
6322            "{CHILD_READING_MARKER}levels={levels} m={m} modes={modes} vmhwm_bytes={hwm} \
6323             profile_seconds={elapsed:.2}"
6324        );
6325    }
6326
6327    /// Narrow arm of the peak-memory measurement. Run on its own it certifies
6328    /// that the profile builds at this width; run as a child of the gate below
6329    /// it is one of the two readings the gate differences.
6330    #[test]
6331    fn zz_child_certified_spectrum_peak_memory_narrow_2546() {
6332        report_certified_spectrum_peak(PEAK_MEMORY_ARMS[0]);
6333    }
6334
6335    /// Wide arm of the peak-memory measurement; see the narrow arm.
6336    #[test]
6337    fn zz_child_certified_spectrum_peak_memory_wide_2546() {
6338        report_certified_spectrum_peak(PEAK_MEMORY_ARMS[1]);
6339    }
6340
6341    /// Peak resident memory of the certified spectral profile against the width
6342    /// it was built at, so [`CERTIFIED_SPECTRUM_BYTES_PER_COLUMN_SQUARED`] — the
6343    /// figure that converts a memory budget into a column cap — is checked
6344    /// against what the route REALIZES rather than against what this file
6345    /// believes it allocates.
6346    ///
6347    /// Under `eigh` that distinction was the whole test: the decomposition was
6348    /// `faer`'s self-adjoint EVD and its tridiagonalization allocated workspace
6349    /// this crate never named, so the realized 6.41-6.84 `m²` blocks stood
6350    /// against an inventory of three. The route now holds one packed `f64`
6351    /// triangle it reduces in place (#2758), which IS an inventory this file can
6352    /// state — and the measurement is kept precisely so that claim is audited
6353    /// rather than asserted.
6354    ///
6355    /// Each width is measured in its OWN CHILD PROCESS, and that is the subject
6356    /// of this comment rather than an implementation note. `VmHWM` is a
6357    /// PROCESS-WIDE high-water mark, so a difference of two readings taken in one
6358    /// process is this route's marginal growth only if nothing else allocated
6359    /// between them. Under `cargo test` the crate's ~1750 tests are threads in a
6360    /// SINGLE process and that condition does not hold. Measured at `b8745892a`:
6361    ///
6362    /// ```text
6363    /// exclusive process, host load 308-424, RAYON_NUM_THREADS 1/2/4/8
6364    ///     blocks = 6.41 / 6.84 / 6.63 / 6.79      -> passes
6365    /// shared process (`cargo test`), host load 1403
6366    ///     blocks = 15.00                          -> fails
6367    /// shared process (`cargo test`), host load 68
6368    ///     passes
6369    /// ```
6370    ///
6371    /// Load is not the variable and parallelism is not the variable: the
6372    /// exclusive arms ran at loads comparable to the failing shared arm and read
6373    /// 6.4-6.8 every time, flat across an 8x parallelism range. Process
6374    /// exclusivity is the variable. A shared-process reading over-attributes
6375    /// whatever else allocated between the two samples to this route, and the
6376    /// 15.00 it produced would have condemned a correct constant — doubling the
6377    /// declared residency cuts [`CERTIFIED_SPECTRUM_MAX`] by `√2` and narrows
6378    /// the exact width regime this budget exists to open. A child process that
6379    /// builds one width and exits is exclusive by construction, under either
6380    /// harness.
6381    ///
6382    /// The gate is that the constant is not an UNDER-estimate: a residency
6383    /// smaller than the realized one would let the cap admit a width that
6384    /// overruns the budget it was derived from.
6385    ///
6386    /// BOTH ARMS ARE PAST [`DENSE_GRAM_MAX`], and that is load-bearing rather
6387    /// than incidental. The narrow arm used to sit at `m = 891`, where the
6388    /// design carries a persistent `dense_gram` cache — an `m²·8` term present
6389    /// in one reading and absent in the other, which does not cancel in the
6390    /// difference and biases the marginal DOWNWARD, i.e. in the direction that
6391    /// makes an under-declared residency look fine. Both widths now assert they
6392    /// have no cache before reporting.
6393    #[test]
6394    fn zz_measure_certified_spectrum_peak_memory_2546() {
6395        if read_hwm_bytes().is_none() {
6396            println!("#2546 VmHWM unavailable on this platform; peak memory not measured");
6397            return;
6398        }
6399        let exe = match std::env::current_exe() {
6400            Ok(exe) => exe,
6401            Err(error) => {
6402                println!("#2546 test binary path unavailable ({error}); peak memory not measured");
6403                return;
6404            }
6405        };
6406        // Two widths, and the DIFFERENCE of their high-water marks over the
6407        // difference of their `m²`: the per-process baseline (test binary,
6408        // fixture, allocator arenas) is identical in the two children and
6409        // cancels, where a single absolute reading would attribute all of it to
6410        // the narrow width.
6411        let mut readings: Vec<(usize, f64)> = Vec::new();
6412        for child in [
6413            "zz_child_certified_spectrum_peak_memory_narrow_2546",
6414            "zz_child_certified_spectrum_peak_memory_wide_2546",
6415        ] {
6416            let path = format!("residual_cascade::refinement_decision_tests::{child}");
6417            let output = std::process::Command::new(&exe)
6418                .args(["--exact", path.as_str(), "--nocapture", "--test-threads=1"])
6419                .output()
6420                .unwrap_or_else(|error| panic!("spawn per-width child {child}: {error}"));
6421            let stdout = String::from_utf8_lossy(&output.stdout);
6422            let mut reading: Option<(usize, f64)> = None;
6423            for line in stdout.lines() {
6424                // The marker is searched for ANYWHERE in the line, not stripped
6425                // from its start. Under `--nocapture` libtest prints a test's
6426                // stdout INLINE after its own `test <name> ... ` prefix, so the
6427                // child's line arrives as
6428                //
6429                //     test residual_cascade::…::…_narrow_2546 ... #2546-child m=891 …
6430                //
6431                // and a prefix match reports every child as silent while the
6432                // reading is sitting in the middle of the line.
6433                let Some(offset) = line.find(CHILD_READING_MARKER) else {
6434                    continue;
6435                };
6436                let rest = &line[offset + CHILD_READING_MARKER.len()..];
6437                let mut width: Option<usize> = None;
6438                let mut hwm: Option<f64> = None;
6439                for field in rest.split_whitespace() {
6440                    // Not `.ok()`. The scanner bans discarding an error here and
6441                    // is right: a malformed `m=` or `vmhwm_bytes=` left the
6442                    // field `None`, `reading` then stayed `None`, and the
6443                    // `unwrap_or_else(|| panic!(..))` below fired with "no child
6444                    // reading" -- blaming an ABSENT line for a line that was
6445                    // present and unparseable. The reader is sent to look for a
6446                    // missing marker that is right there.
6447                    if let Some(value) = field.strip_prefix("m=") {
6448                        width = Some(value.parse().unwrap_or_else(|error| {
6449                            panic!(
6450                                "child reading line carries an unparseable m={value:?}: \
6451                                 {error}; the marker was found, so this is a malformed \
6452                                 field, not a missing reading"
6453                            )
6454                        }));
6455                    } else if let Some(value) = field.strip_prefix("vmhwm_bytes=") {
6456                        hwm = Some(value.parse().unwrap_or_else(|error| {
6457                            panic!(
6458                                "child reading line carries an unparseable \
6459                                 vmhwm_bytes={value:?}: {error}; the marker was found, \
6460                                 so this is a malformed field, not a missing reading"
6461                            )
6462                        }));
6463                    }
6464                }
6465                if let (Some(width), Some(hwm)) = (width, hwm) {
6466                    reading = Some((width, hwm));
6467                }
6468            }
6469            let (m, hwm) = reading.unwrap_or_else(|| {
6470                panic!(
6471                    "child {child} produced no reading (status {:?}).\nstdout:\n{stdout}\nstderr:\n{}",
6472                    output.status,
6473                    String::from_utf8_lossy(&output.stderr)
6474                )
6475            });
6476            println!(
6477                "#2546 m={m} vmhwm={:.1}MiB (own process)",
6478                hwm / (1024.0 * 1024.0)
6479            );
6480            readings.push((m, hwm));
6481        }
6482        let (narrow, narrow_hwm) = readings[0];
6483        let (wide, wide_hwm) = readings[1];
6484        assert!(
6485            wide > narrow,
6486            "the widths must increase for a high-water difference to mean anything"
6487        );
6488        let square_growth = wide as f64 * wide as f64 - narrow as f64 * narrow as f64;
6489        let bytes_per_square = (wide_hwm - narrow_hwm) / square_growth;
6490        println!(
6491            "#2546 marginal bytes_per_m2={bytes_per_square:.2} \
6492             declared={CERTIFIED_SPECTRUM_BYTES_PER_COLUMN_SQUARED} \
6493             cap={CERTIFIED_SPECTRUM_MAX} budget_MiB={}",
6494            CERTIFIED_SPECTRUM_BYTES / (1024 * 1024)
6495        );
6496        assert!(
6497            bytes_per_square <= CERTIFIED_SPECTRUM_BYTES_PER_COLUMN_SQUARED as f64,
6498            "the certified route grows by {bytes_per_square:.2} bytes per m-squared against a \
6499             declared {CERTIFIED_SPECTRUM_BYTES_PER_COLUMN_SQUARED} (m {narrow} -> {wide}); the \
6500             column cap derived from CERTIFIED_SPECTRUM_BYTES is therefore an under-estimate of \
6501             the memory it admits"
6502        );
6503    }
6504
6505    /// Fill-in of the sparse direct factor, against the dense triangle it
6506    /// replaces, on the past-cache widths this route exists to serve.
6507    ///
6508    /// The sparse route is worth taking only if the AMD ordering's realized
6509    /// `nnz(L)` is far below `m(m+1)/2` — the number a dense Cholesky would
6510    /// store. That is a property of the design's sparsity, not an assumption, so
6511    /// it is measured and asserted rather than argued: a multilevel Wendland row
6512    /// touches `O(1)` bumps per level, so `A` is sparse and its factor should be
6513    /// a small fraction of the dense triangle at every width here. If fill-in
6514    /// ever made the sparse factor comparable to dense storage, this gate fails
6515    /// and the route's premise is gone.
6516    #[test]
6517    fn zz_measure_sparse_factor_fill_in_2546() {
6518        for levels in [6usize, 7] {
6519            let (x1, x2, y) = dense_fixture(6);
6520            let weights = vec![1.0; y.len()];
6521            let axes: [&[f64]; 2] = [&x1, &x2];
6522            let design = ResidualCascadeDesign::build(&axes, &y, &weights, &[1.0, 1.0], 2.0, levels)
6523                .expect("cascade design");
6524            let core = &design.core;
6525            let system = core.sparse_upper_system(1.0).expect("sparse normal equations");
6526            let nnz_a = system.compute_nnz();
6527            let nnz_l = sparse_spd_factor_nnz(&system).expect("symbolic analysis");
6528            let dense_upper = core.m * (core.m + 1) / 2;
6529            println!(
6530                "#2546 levels={levels} m={} nnz(A)={nnz_a} nnz(L)={nnz_l} dense_upper={dense_upper} \
6531                 fraction_of_dense={:.5} budget={SPARSE_FACTOR_MAX_NNZ}",
6532                core.m,
6533                nnz_l as f64 / dense_upper as f64
6534            );
6535            assert!(
6536                nnz_l * 4 < dense_upper,
6537                "sparse factor is not sparse at m={}: nnz(L)={nnz_l} against a dense triangle of \
6538                 {dense_upper}; the sparse route's premise does not hold on this design",
6539                core.m
6540            );
6541            assert!(
6542                nnz_l <= SPARSE_FACTOR_MAX_NNZ,
6543                "fill-in {nnz_l} exceeds the factor budget {SPARSE_FACTOR_MAX_NNZ} at m={}",
6544                core.m
6545            );
6546        }
6547    }
6548
6549    /// The sparse direct log-determinant and the dense one are the same number.
6550    ///
6551    /// Both are exact factorizations of the same `X'WX + λD`, so they may differ
6552    /// only by floating-point summation order. The bound is the dense Cholesky's
6553    /// own forward error on a log-determinant — `O(m)·eps` per diagonal term over
6554    /// `m` terms — not a tuned tolerance, and it is charged on a design narrow
6555    /// enough to HAVE a dense route to compare against.
6556    #[test]
6557    fn sparse_and_dense_logdets_agree() {
6558        let (x1, x2, y) = dense_fixture(6);
6559        let weights = vec![1.0; y.len()];
6560        let axes: [&[f64]; 2] = [&x1, &x2];
6561        let design = ResidualCascadeDesign::build(&axes, &y, &weights, &[1.0, 1.0], 2.0, 4)
6562            .expect("cascade design");
6563        let core = &design.core;
6564        assert!(
6565            core.dense_gram.is_some(),
6566            "premise: the comparator needs the dense route, got m = {}",
6567            core.m
6568        );
6569        for log_lambda in [-4.0_f64, 0.0, 4.0] {
6570            let lambda = log_lambda.exp();
6571            let dense = core.logdet_dense(lambda).expect("dense logdet");
6572            let factor = core
6573                .sparse_exact_factor(lambda)
6574                .expect("sparse factorization")
6575                .expect("fill-in is inside the budget on this fixture");
6576            let sparse = logdet_from_factor(&factor).expect("sparse logdet");
6577            let resolution = f64::EPSILON * core.m as f64 * dense.abs().max(1.0);
6578            assert!(
6579                (sparse - dense).abs() <= resolution,
6580                "sparse and dense log-determinants disagree at log lambda {log_lambda}: \
6581                 {sparse} versus {dense} (resolution {resolution})"
6582            );
6583        }
6584    }
6585
6586    /// The residual spectral sum and a direct factorization compute the same
6587    /// function of λ.
6588    ///
6589    /// The spectral expression reads the profiled residual and its three
6590    /// quadratic forms off the Schur decomposition; the comparator re-derives them from a
6591    /// factorization of `A = X'WX + λD`. If they ever disagree the criterion is
6592    /// representation-dependent, which is the defect the spectral form exists to remove —
6593    /// so the agreement is asserted directly rather than inferred from the
6594    /// scores that consume it.
6595    ///
6596    /// The bound is the textbook forward-error of the comparator, not a tuned
6597    /// number: the comparator's Cholesky solve carries `O(m)·eps·cond(A)`, and
6598    /// `cond(A) = (θ_max + λ)/(θ_min + λ)` is available exactly from the same
6599    /// spectrum. Nothing here is free to be widened without changing that claim.
6600    #[test]
6601    fn spectral_and_solved_residual_forms_agree() {
6602        let (x1, x2, y) = dense_fixture(6);
6603        let weights = vec![1.0; y.len()];
6604        let axes: [&[f64]; 2] = [&x1, &x2];
6605        let design = ResidualCascadeDesign::build(&axes, &y, &weights, &[1.0, 1.0], 2.0, 2)
6606            .expect("cascade design");
6607        let core = &design.core;
6608        assert!(core.dense_gram.is_some(), "fixture must take the dense route");
6609        let profile = core.reml_profile().expect("spectral profile");
6610        let CascadeResidualForm::Spectral(spectrum) = &profile.residual else {
6611            panic!("the dense route must carry the spectral residual form");
6612        };
6613
6614        // `cond(A) = (θ_max + λ)/(θ_min + λ)` is taken over EVERY Schur mode,
6615        // including the certified-null ones the spectral residual now drops: a
6616        // null mode still sits at exactly λ in `A`'s spectrum, and it is what
6617        // sets the comparator's conditioning. Reading `spectrum.eigenvalue`
6618        // instead would silently tighten the comparator's own error budget.
6619        let smallest = profile
6620            .modes
6621            .iter()
6622            .map(|mode| mode.eigenvalue)
6623            .fold(f64::INFINITY, f64::min);
6624        let largest = profile
6625            .modes
6626            .iter()
6627            .map(|mode| mode.eigenvalue)
6628            .fold(0.0_f64, f64::max);
6629
6630        for log_lambda in [-6.0_f64, -2.0, 0.0, 2.0, 6.0] {
6631            let lambda = log_lambda.exp();
6632            let (rss, penalty_energy, inverse_penalty_energy, third_energy) =
6633                spectrum.moments(lambda);
6634
6635            let (coeff, _, _) = core
6636                .solve_coeff(lambda, &core.rhs, None)
6637                .expect("first solve");
6638            let dc: Vec<f64> = coeff
6639                .iter()
6640                .zip(core.pen_diag.iter())
6641                .map(|(&c, &d)| d * c)
6642                .collect();
6643            let (u, _, _) = core.solve_coeff(lambda, &dc, None).expect("second solve");
6644            let solved = [
6645                core.rss_pen(&coeff),
6646                coeff.iter().zip(dc.iter()).map(|(&c, &v)| c * v).sum(),
6647                dc.iter().zip(u.iter()).map(|(&a, &b)| a * b).sum(),
6648                u.iter()
6649                    .zip(core.pen_diag.iter())
6650                    .map(|(&v, &d)| d * v * v)
6651                    .sum(),
6652            ];
6653            let spectral = [rss, penalty_energy, inverse_penalty_energy, third_energy];
6654            let names = ["R", "c'Dc", "(Dc)'A^-1(Dc)", "u'Du"];
6655
6656            let condition = (largest + lambda) / (smallest + lambda);
6657            // The three quadratic forms are sums of positive terms, so their
6658            // relative error is `O(m)·eps·cond(A)`. `R` is not: BOTH routes form
6659            // it by subtracting a fitted energy from an anchor energy, so its
6660            // relative error carries that cancellation's own condition number,
6661            // `anchor/|R|`. Charging the sum of the two is the honest bound.
6662            //
6663            // AND IT IS CHARGED TWICE, once per comparand. This is a comparison
6664            // of two INDEPENDENT computations of one quantity, so the gap it can
6665            // legitimately show is the sum of both forward errors — the solve's,
6666            // and the spectral route's own mode sum over the same spectrum with
6667            // the same condition number. Charging one of them treated the
6668            // spectral side as exact, which no decomposition is: at `m = 20` and
6669            // `cond(A) = 1.005` the one-sided bound is `4.46e-15` and the
6670            // measured gap `4.60e-15`, i.e. the gate was failing on the last
6671            // bit of a perfectly conditioned 20-column problem the moment the
6672            // decomposition's rounding differed. Two equal terms, not a factor
6673            // chosen to admit a number.
6674            let cancellation = spectrum.anchor_energy[0] / rss.abs().max(f64::MIN_POSITIVE);
6675            let comparands = 2.0;
6676            let bounds = [
6677                comparands * core.m as f64 * f64::EPSILON * (condition + cancellation),
6678                comparands * core.m as f64 * f64::EPSILON * condition,
6679                comparands * core.m as f64 * f64::EPSILON * condition,
6680                comparands * core.m as f64 * f64::EPSILON * condition,
6681            ];
6682            for (((&a, &b), name), bound) in
6683                spectral.iter().zip(solved.iter()).zip(names).zip(bounds)
6684            {
6685                let gap = (a - b).abs() / b.abs().max(f64::MIN_POSITIVE);
6686                assert!(
6687                    gap <= bound,
6688                    "{name} disagrees at log lambda {log_lambda}: spectral {a}, solved {b} \
6689                     (relative {gap:e} exceeds the comparator's own forward error {bound:e} \
6690                      at cond(A) = {condition:e}, cancellation = {cancellation:e})"
6691                );
6692            }
6693        }
6694    }
6695
6696    /// The cascade's own closed form and the [`AffineRemlProfile`] the search
6697    /// actually runs on are one score.
6698    ///
6699    /// `fit_reml` isolates the optimum with the affine profile (for its interval
6700    /// extension) while `criterion` and the selected `normalized_logdet` come
6701    /// from [`CascadeRemlProfile::evaluate`]. Two implementations of one
6702    /// quantity is exactly the arrangement that lets a criterion drift, so the
6703    /// two are held to agreement in value, slope and curvature here.
6704    ///
6705    /// The tolerance is the forward error of the arithmetic the two routes
6706    /// actually perform, read off THIS fixture's spectral moments at each
6707    /// evaluation point. It is not `rank·eps`, and the reason is measured:
6708    ///
6709    /// 1. THE TWO ROUTES DO NOT EVALUATE AT THE SAME LAMBDA. The cascade
6710    ///    exponentiates `rho` through `checked_exp_log_strength`, i.e. the
6711    ///    platform `exp` (sub-ulp); [`AffineRemlProfile::evaluate`] uses
6712    ///    `certified_exp_representative`, the midpoint of an outward-rounded
6713    ///    enclosure that is hundreds of ulps wide. Neither route may adopt the
6714    ///    other's: the cascade's criterion has to describe the lambda the fit
6715    ///    is actually solved at, and the affine profile's exponential has to be
6716    ///    the one its own enclosure is stated in. Over this domain the measured
6717    ///    `|lambda_affine - lambda_cascade| / lambda` runs to `2.59e-14`. A
6718    ///    RELATIVE shift `delta` in lambda IS an absolute shift `delta` in
6719    ///    `rho`, so every mode kernel moves by its own `d/drho` times `delta`,
6720    ///    and each accumulator below is charged exactly that. At
6721    ///    `rho = -2.6396` this term alone is `|curvature|·delta =
6722    ///    2.302 · 1.67e-14 = 3.8e-14`, which is the whole of the measured
6723    ///    `3.9e-14` slope disagreement that `rank·eps = 3.77e-15` was being
6724    ///    asked to cover.
6725    /// 2. The mode sums are sums of `rank` terms of SIZE, not of size one. The
6726    ///    determinant slope is `sum_i t_i` with `t_i = theta_i/(theta_i +
6727    ///    lambda)`, so its Wilkinson error is `rank·eps·sum_i t_i`; at that same
6728    ///    `rho`, `sum_i t_i = 10.71`, i.e. `4.0e-14` and not `3.8e-15`. The
6729    ///    profiled residual `R = anchor - S1` is the one subtraction of
6730    ///    near-equal positives, so its error carries the cancellation factor
6731    ///    `anchor/R` (up to `4.13` at the small-lambda end) — the same factor
6732    ///    [`spectral_and_solved_residual_forms_agree`] charges — and the score
6733    ///    multiplies it by `dof`.
6734    ///
6735    /// Every factor below is computed from the fixture at the evaluation point;
6736    /// nothing is a fitted constant, and the assertion is on ABSOLUTE
6737    /// disagreement so a normalization cannot quietly absorb a growing gap.
6738    #[test]
6739    fn affine_view_is_the_same_score_as_the_cascade_jet() {
6740        let (x1, x2, y) = dense_fixture(6);
6741        let weights = vec![1.0; y.len()];
6742        let axes: [&[f64]; 2] = [&x1, &x2];
6743        let design = ResidualCascadeDesign::build(&axes, &y, &weights, &[1.0, 1.0], 2.0, 2)
6744            .expect("cascade design");
6745        let profile = design.core.reml_profile().expect("spectral profile");
6746        let affine = profile
6747            .affine_view()
6748            .expect("affine view")
6749            .expect("the dense route must expose an affine view");
6750        let CascadeResidualForm::Spectral(spectrum) = &profile.residual else {
6751            panic!("the dense route must carry the spectral residual form");
6752        };
6753        let (lo, hi) = profile.log_lambda_domain().expect("domain");
6754        let rank = (design.core.m - design.core.nullity()) as f64;
6755        let dof = (design.core.y.len() - design.core.nullity()) as f64;
6756        let eps = f64::EPSILON;
6757
6758        for step in 0..=8 {
6759            let log_lambda = lo + (hi - lo) * step as f64 / 8.0;
6760            let cascade = profile.evaluate(log_lambda).expect("cascade jet").jet;
6761            let spectral = affine.evaluate(log_lambda).expect("affine jet");
6762
6763            // The two exponentials the two routes run, and the `rho` shift
6764            // between them.
6765            let lambda =
6766                gam_problem::checked_exp_log_strength(log_lambda).expect("cascade lambda");
6767            let affine_lambda = gam_math::score_opt::certified_exp_representative(log_lambda)
6768                .expect("affine lambda");
6769            let shift = (affine_lambda - lambda).abs() / lambda;
6770
6771            let (rss, s2, s3, s4) = spectrum.moments(lambda);
6772            let anchor = spectrum.anchor_energy[0];
6773            // The magnitudes the three determinant accumulators run over, and
6774            // the `d/drho` of each summand: `d/drho log(1 + theta/lambda) = -t`
6775            // and `d/drho t = -t(1-t)`.
6776            let mut logdet_magnitude = 0.0_f64;
6777            let mut slope_magnitude = 0.0_f64;
6778            let mut curvature_magnitude = 0.0_f64;
6779            for &theta in &spectrum.eigenvalue {
6780                let t = theta / (theta + lambda);
6781                logdet_magnitude += (1.0 + theta / lambda).ln().abs();
6782                slope_magnitude += t;
6783                curvature_magnitude += t * (1.0 - t);
6784            }
6785
6786            // Residual derivatives in `rho`, and the SUM OF MAGNITUDES of each
6787            // cancelling form — which is what a forward-error argument is
6788            // entitled to charge:
6789            //   first  = R'   = lambda S2
6790            //   second = R''  = R' - 2 lambda^2 S3
6791            //   third  = R''' = R' - 6 lambda^2 S3 + 6 lambda^3 S4.
6792            let lambda_squared = lambda * lambda;
6793            let lambda_cubed = lambda_squared * lambda;
6794            let first = lambda * s2;
6795            let second = first - 2.0 * lambda_squared * s3;
6796            let second_magnitude = first + 2.0 * lambda_squared * s3;
6797            let third_magnitude = first + 6.0 * lambda_squared * s3 + 6.0 * lambda_cubed * s4;
6798
6799            // Each route accumulates `rank` terms sequentially and rounds a
6800            // handful of elementary operations per term, so each carries
6801            // `(rank + 4)·eps` relative on its own sum; the comparison is
6802            // charged both.
6803            let sum_eps = 2.0 * (rank + 4.0) * eps;
6804            let residual_error = sum_eps * anchor + first * shift;
6805            let first_error = sum_eps * first + second_magnitude * shift;
6806            let second_error = sum_eps * second_magnitude + third_magnitude * shift;
6807            let log_first = first / rss;
6808            let log_first_error = first_error / rss + log_first.abs() * residual_error / rss;
6809            let log_second_error = second_error / rss
6810                + (second / rss).abs() * residual_error / rss
6811                + 2.0 * log_first.abs() * log_first_error;
6812
6813            let determinant_value_error = sum_eps * logdet_magnitude + slope_magnitude * shift;
6814            let determinant_slope_error = sum_eps * slope_magnitude + curvature_magnitude * shift;
6815            // The cascade forms the curvature summand as `t·(1-t)`, and `1-t`
6816            // cancels as `t -> 1`: one eps of `t` per mode. The affine route
6817            // carries the same complement as `lambda·s/h` and never subtracts.
6818            let determinant_curvature_error = sum_eps * curvature_magnitude
6819                + eps * slope_magnitude
6820                + curvature_magnitude * shift;
6821
6822            let bounds = [
6823                0.5 * (determinant_value_error
6824                    + dof * (residual_error / rss + 2.0 * eps * (rss / dof).ln().abs())),
6825                0.5 * (determinant_slope_error + dof * log_first_error),
6826                0.5 * (determinant_curvature_error + dof * log_second_error),
6827            ];
6828            for ((name, a, b), bound) in [
6829                ("value", cascade.value, spectral.value),
6830                ("derivative", cascade.derivative, spectral.derivative),
6831                ("curvature", cascade.curvature, spectral.curvature),
6832            ]
6833            .into_iter()
6834            .zip(bounds)
6835            {
6836                let gap = (a - b).abs();
6837                assert!(
6838                    gap <= bound,
6839                    "{name} disagrees at log lambda {log_lambda}: cascade {a}, affine {b} \
6840                     (absolute {gap:e} exceeds the two routes' own forward error {bound:e}, \
6841                      whose terms are the lambda shift {shift:e}, the mode sums \
6842                      {slope_magnitude:e} / {curvature_magnitude:e} / {logdet_magnitude:e}, \
6843                      and the residual cancellation anchor/R {:e})",
6844                    anchor / rss
6845                );
6846            }
6847        }
6848    }
6849
6850    #[test]
6851    fn dense_spectral_profile_matches_factorization_and_analytic_slope() {
6852        let (x1, x2, y) = dense_fixture(6);
6853        let weights = vec![1.0; y.len()];
6854        let axes: [&[f64]; 2] = [&x1, &x2];
6855        let design = ResidualCascadeDesign::build(&axes, &y, &weights, &[1.0, 1.0], 2.0, 2)
6856            .expect("cascade design");
6857        assert!(design.core.dense_gram.is_some());
6858        let profile = design.core.reml_profile().expect("spectral profile");
6859        let rank = (design.core.m - design.core.nullity()) as f64;
6860        let dof = (design.core.y.len() - design.core.nullity()) as f64;
6861
6862        for log_lambda in [-4.0, 0.0, 3.0] {
6863            let evaluation = profile.evaluate(log_lambda).expect("analytic score");
6864            let lambda = log_lambda.exp();
6865            let logdet = design.core.logdet_dense(lambda).expect("dense logdet");
6866            let coefficients = design
6867                .core
6868                .solve_coeff(lambda, &design.core.rhs, None)
6869                .expect("dense solve")
6870                .0;
6871            let rss = design.core.rss_pen(&coefficients);
6872            let direct = -0.5
6873                * (logdet - rank * log_lambda - design.core.pen_logdet_const
6874                    + dof * (rss / dof).ln());
6875            assert!(
6876                (evaluation.jet.value - direct).abs() <= f64::EPSILON.sqrt() * (1.0 + direct.abs()),
6877                "spectral/direct score mismatch at {log_lambda}: {} versus {direct}",
6878                evaluation.jet.value,
6879            );
6880
6881            // Finite differences are confined to this oracle test. The
6882            // production optimizer consumes the hand-derived score jet above.
6883            //
6884            // The comparator has to be built for a NOISY evaluator, and this
6885            // one is: `profile.evaluate` runs a spectral solve, so its value
6886            // carries roughly 1e-12 of evaluation noise rather than being exact
6887            // to the last bit.
6888            //
6889            // That moves the optimal step. `h = eps^(1/3)` is optimal only when
6890            // the sole error is representation roundoff; against noise `v` the
6891            // central-difference error is `v/h + (h²/6)·S3`, minimized at
6892            // `h ~ (3v/S3)^(1/3) ~ 1e-4` — three orders ABOVE `eps^(1/3)`.
6893            // Measured at `eps^(1/3) = 6.06e-6`: D(h) = 5.025511346842242,
6894            // D(h/2) = 5.025511611442345. The two stencils disagree by 2.6e-7
6895            // and the FINER one is FARTHER from the analytic slope. Truncation
6896            // shrinks with h and cannot do that; noise amplified by `1/h` does
6897            // exactly that. The step was too small, not too crude.
6898            //
6899            // So: `h = 1e-4`, and Richardson there. The `h²` term cancels
6900            // exactly (leaving O(h⁴) ~ 1e-16, negligible whatever `S3` is) and
6901            // the noise floor is `~3v/h ~ 3e-8`, inside the unchanged
6902            // `sqrt(eps)·(1+|S'|) ~ 9e-8` bound. The bound is not relaxed; the
6903            // comparator is made accurate enough to be charged against it.
6904            let central = |step: f64| -> f64 {
6905                let right = profile
6906                    .evaluate(log_lambda + step)
6907                    .expect("right score")
6908                    .jet
6909                    .value;
6910                let left = profile
6911                    .evaluate(log_lambda - step)
6912                    .expect("left score")
6913                    .jet
6914                    .value;
6915                (right - left) / (2.0 * step)
6916            };
6917            let step = 1.0e-4;
6918            let coarse = central(step);
6919            let fine = central(0.5 * step);
6920            let numerical_slope = (4.0 * fine - coarse) / 3.0;
6921            assert!(
6922                (evaluation.jet.derivative - numerical_slope).abs()
6923                    <= f64::EPSILON.sqrt() * (1.0 + numerical_slope.abs()),
6924                "analytic slope mismatch at {log_lambda}: {} versus {numerical_slope} \
6925                 (Richardson of h={step:e} → {coarse}, h/2 → {fine})",
6926                evaluation.jet.derivative,
6927            );
6928        }
6929    }
6930    /// A fixture whose rank is far above the quadrature's accepted step count,
6931    /// so the truncated regime is what gets measured, while staying under the
6932    /// dense cap so an exact comparator survives.
6933    fn truncated_regime_fixture() -> (Vec<f64>, Vec<f64>, Vec<f64>, usize) {
6934        let (x1, x2, y) = dense_fixture(28);
6935        (x1, x2, y, 5)
6936    }
6937
6938    fn cascade_core(
6939        side_data: (&[f64], &[f64], &[f64]),
6940        levels: usize,
6941    ) -> ResidualCascadeDesign {
6942        let (x1, x2, y) = side_data;
6943        let weights = vec![1.0; y.len()];
6944        let axes: [&[f64]; 2] = [x1, x2];
6945        ResidualCascadeDesign::build(&axes, y, &weights, &[1.0, 1.0], 2.0, levels)
6946            .expect("cascade design")
6947    }
6948
6949    /// #2503 measurement — what the beta-seeded Golub–Meurant residual quadrature
6950    /// accepts, at what step count, and how far its moments then sit from the
6951    /// exact dense eigenbasis over the whole log-lambda domain.
6952    #[test]
6953    fn zz_measure_residual_quadrature_admission_ladder_2503() {
6954        for (side, levels) in [
6955            (6usize, 2usize),
6956            (10, 3),
6957            (14, 3),
6958            (22, 3),
6959            (14, 4),
6960            (20, 5),
6961            (28, 5),
6962        ] {
6963            let (x1, x2, y) = dense_fixture(side);
6964            let design = cascade_core((&x1, &x2, &y), levels);
6965            let core = &design.core;
6966            if core.dense_gram.is_none() {
6967                println!("#2503 side={side} levels={levels} m={}: past the dense cap", core.m);
6968                continue;
6969            }
6970            let (null_chol, _) = core.null_gram_factor().expect("null factor");
6971            let (modes, exact) = core
6972                .dense_cascade_spectrum(&null_chol)
6973                .expect("dense spectrum");
6974            let domain = certified_log_lambda_domain_from_modes(&modes).expect("domain");
6975            let (spectrum, certificate) = core
6976                .iterative_residual_spectrum(&null_chol, domain)
6977                .expect("quadrature");
6978            let rank = core.m - core.nullity();
6979            let anchor = exact.anchor_energy[0];
6980            let worst = spectrum.as_ref().map(|spectrum| {
6981                let mut worst = [0.0_f64; 4];
6982                for step in 0..=192 {
6983                    let lambda =
6984                        (domain.0 + (domain.1 - domain.0) * step as f64 / 192.0).exp();
6985                    let t = exact.moments(lambda);
6986                    let g = spectrum.moments(lambda);
6987                    worst[0] = worst[0].max((g.0 - t.0).abs() / anchor.abs());
6988                    for (k, (tv, gv)) in
6989                        [(t.1, g.1), (t.2, g.2), (t.3, g.3)].into_iter().enumerate()
6990                    {
6991                        worst[k + 1] =
6992                            worst[k + 1].max((gv - tv).abs() / tv.abs().max(f64::MIN_POSITIVE));
6993                    }
6994                }
6995                worst
6996            });
6997            println!(
6998                "#2503 side={side} levels={levels} n={} m={} rank={rank} budget={} steps={} \
6999                 coarse={} tail_estimate={:.3e} target={:.3e} accepted={} invariant={} tail={:.2e} \
7000                 mass_defect={:.2e} dropped={:.2e}",
7001                y.len(),
7002                core.m,
7003                certificate.budget,
7004                certificate.steps,
7005                certificate.coarse_steps,
7006                certificate.tail_estimate,
7007                certificate.target,
7008                certificate.accepted_for_point_evaluation,
7009                certificate.invariant,
7010                certificate.relative_tail,
7011                certificate.mass_defect,
7012                certificate.dropped_mass_fraction,
7013            );
7014            match worst {
7015                Some(worst) => println!(
7016                    "#2503   versus exact dense: dR/anchor={:.3e} S2={:.3e} S3={:.3e} S4={:.3e}",
7017                    worst[0], worst[1], worst[2], worst[3]
7018                ),
7019                None => println!("#2503   no rule admitted; point evaluation is refused"),
7020            }
7021        }
7022    }
7023
7024    /// The admitted point quadrature matches the dense eigenbasis residual to the
7025    /// resolution its numerical evidence claims on this fixture.
7026    ///
7027    /// This is the substitution gate: past the dense cap the profiled residual and
7028    /// its three `log λ` derivative moments come from a Golub–Meurant rule instead
7029    /// of an exact projection, and if the two disagree anywhere in the domain the
7030    /// REML criterion is route-dependent — which is the defect the spectral form
7031    /// exists to remove.
7032    ///
7033    /// The fixture asserts its own premise. `rank ≫ steps` is what puts the rule
7034    /// in the TRUNCATED regime; at `steps == rank` the Krylov space is the whole
7035    /// space and the rule reproduces the spectral sum by dimension alone, so a gate
7036    /// that silently drifted into that case would measure arithmetic and not
7037    /// quadrature. #2503's first accuracy gate did exactly that.
7038    #[test]
7039    fn admitted_residual_quadrature_matches_the_dense_eigenbasis_2503() {
7040        let (x1, x2, y, levels) = truncated_regime_fixture();
7041        let design = cascade_core((&x1, &x2, &y), levels);
7042        let core = &design.core;
7043        assert!(
7044            core.dense_gram.is_some(),
7045            "the exact comparator needs the dense route"
7046        );
7047        let (null_chol, _) = core.null_gram_factor().expect("null factor");
7048        let (modes, exact) = core
7049            .dense_cascade_spectrum(&null_chol)
7050            .expect("dense spectrum");
7051        let domain = certified_log_lambda_domain_from_modes(&modes).expect("domain");
7052        let (spectrum, certificate) = core
7053            .iterative_residual_spectrum(&null_chol, domain)
7054            .expect("quadrature");
7055        let rank = core.m - core.nullity();
7056        assert!(
7057            certificate.accepted_for_point_evaluation,
7058            "the point quadrature must be admitted on this fixture: {certificate:?}"
7059        );
7060        assert!(
7061            certificate.steps * 2 < rank,
7062            "premise: the accepted rule must be TRUNCATED (steps {} against rank {rank}), else \
7063             this gate measures arithmetic and not quadrature",
7064            certificate.steps
7065        );
7066        assert!(
7067            certificate.tail_estimate <= certificate.target,
7068            "the extrapolated tail must reach the search resolution: {certificate:?}"
7069        );
7070        let spectrum = spectrum.expect("an admitted point rule is returned");
7071
7072        // The comparator is the exact projection, and the bound is the resolution
7073        // the evidence claims — not a widened number. `R` is charged
7074        // ABSOLUTELY against the anchor energy: `R = anchor − S₁` cancels to nine
7075        // digits at the bottom of an over-complete cascade's domain, so a relative
7076        // bound there would be a statement about cancellation.
7077        let anchor = exact.anchor_energy[0];
7078        let resolution = f64::EPSILON.sqrt();
7079        for step in 0..=192 {
7080            let log_lambda = domain.0 + (domain.1 - domain.0) * step as f64 / 192.0;
7081            let lambda = log_lambda.exp();
7082            let (r_exact, s2_exact, s3_exact, s4_exact) = exact.moments(lambda);
7083            let (r_gauss, s2_gauss, s3_gauss, s4_gauss) = spectrum.moments(lambda);
7084            assert!(
7085                (r_gauss - r_exact).abs() <= resolution * anchor.abs(),
7086                "profiled residual disagrees at log lambda {log_lambda}: {r_gauss} versus \
7087                 {r_exact} (anchor {anchor})"
7088            );
7089            for (name, gauss, exact_value) in [
7090                ("S2", s2_gauss, s2_exact),
7091                ("S3", s3_gauss, s3_exact),
7092                ("S4", s4_gauss, s4_exact),
7093            ] {
7094                assert!(
7095                    (gauss - exact_value).abs() <= resolution * exact_value.abs(),
7096                    "{name} disagrees at log lambda {log_lambda}: {gauss} versus {exact_value} \
7097                     ({certificate:?})"
7098                );
7099            }
7100        }
7101    }
7102
7103    /// The admitted quadrature and the SOLVE it replaces are the same function of
7104    /// λ, measured on the iterative route itself.
7105    ///
7106    /// Every other gate here charges the quadrature against the dense eigenbasis,
7107    /// which means charging it on designs small enough to HAVE one. This one runs
7108    /// past the dense sizing cap — where there is no eigenbasis, no `dense_gram`,
7109    /// and the only other way to obtain `S₁..S₄` is the pair of PCG solves the
7110    /// shipped route used to perform at every λ. If those two disagree, the REML
7111    /// criterion is route-dependent, which is the defect the spectral form exists
7112    /// to remove; and this is the only angle from which that can be checked where
7113    /// it actually matters.
7114    ///
7115    /// The λ is chosen where PCG is well conditioned, because the comparator is
7116    /// the thing with the error bar: `cond(B + λI) ≤ (θmax + λ)/λ`, and the solve
7117    /// carries `CG_RTOL` backward error, so its forward error on `S₁` is about
7118    /// `CG_RTOL · cond` and on `S₃, S₄` — which pass through a second solve —
7119    /// about its square. The bound below is that product with the measured
7120    /// conditioning substituted, not a widened number.
7121    #[test]
7122    fn the_quadrature_and_the_solve_it_replaces_agree_past_the_dense_cap_2503() {
7123        let (x1, x2, y) = dense_fixture(56);
7124        let design = cascade_core((&x1, &x2, &y), 6);
7125        let core = &design.core;
7126        assert!(
7127            core.dense_gram.is_none(),
7128            "premise: this fixture must be PAST the dense sizing cap (m = {}), or the solve is \
7129             not the comparator this gate is about",
7130            core.m
7131        );
7132        let (null_chol, _) = core.null_gram_factor().expect("null factor");
7133        let modes = core
7134            .iterative_cascade_spectrum(&null_chol)
7135            .expect("determinant modes");
7136        let domain = certified_log_lambda_domain_from_modes(&modes).expect("domain");
7137        let (spectrum, certificate) = core
7138            .iterative_residual_spectrum(&null_chol, domain)
7139            .expect("quadrature");
7140        let spectrum = spectrum.unwrap_or_else(|| {
7141            panic!("the past-cap quadrature must certify on this fixture: {certificate:?}")
7142        });
7143        let theta_max = spectrum.eigenvalue.iter().copied().fold(0.0, f64::max);
7144
7145        for log_lambda in [0.0_f64, 1.0, 2.0, 3.0] {
7146            let lambda = log_lambda.exp();
7147            let quadrature = spectrum.moment_sums(lambda);
7148
7149            // Exactly what `CascadeRemlProfile::evaluate` does on the solve route.
7150            let (coeff, _, _) = core
7151                .solve_coeff(lambda, &core.rhs, None)
7152                .expect("first certified solve");
7153            let dc: Vec<f64> = coeff
7154                .iter()
7155                .zip(core.pen_diag.iter())
7156                .map(|(&c, &d)| d * c)
7157                .collect();
7158            let (u, _, _) = core
7159                .solve_coeff(lambda, &dc, None)
7160                .expect("second certified solve");
7161            let anchor = spectrum.anchor_energy[0];
7162            let solved = [
7163                anchor - core.rss_pen(&coeff),
7164                coeff.iter().zip(dc.iter()).map(|(&c, &v)| c * v).sum(),
7165                dc.iter().zip(u.iter()).map(|(&a, &b)| a * b).sum(),
7166                u.iter()
7167                    .zip(core.pen_diag.iter())
7168                    .map(|(&v, &d)| d * v * v)
7169                    .sum(),
7170            ];
7171
7172            let conditioning = (theta_max + lambda) / lambda;
7173            for (k, (&got, &comparator)) in quadrature.iter().zip(solved.iter()).enumerate() {
7174                // One solve for `S_1, S_2`; two for `S_3, S_4`.
7175                let solves = if k < 2 { 1u32 } else { 2 };
7176                let bound = CG_RTOL * conditioning.powi(solves as i32) * comparator.abs();
7177                assert!(
7178                    (got - comparator).abs() <= bound,
7179                    "S{} disagrees between the quadrature and the solve it replaces at log \
7180                     lambda {log_lambda}: {got} versus {comparator} (bound {bound}, conditioning \
7181                     {conditioning}, {certificate:?})",
7182                    k + 1
7183                );
7184            }
7185        }
7186    }
7187
7188    #[test]
7189    fn state_round_trip_requires_and_preserves_training_sample_size() {
7190        let (x1, x2, y) = dense_fixture(4);
7191        let weights = vec![1.0; y.len()];
7192        let axes: [&[f64]; 2] = [&x1, &x2];
7193        let design = ResidualCascadeDesign::build(&axes, &y, &weights, &[1.0, 1.0], 2.0, 2)
7194            .expect("cascade design");
7195        let fit = design.fit_at(0.0, None).expect("fixed-lambda fit");
7196        assert_eq!(fit.training_sample_size(), y.len());
7197
7198        let state = fit.to_state().expect("persist cascade fit");
7199        assert_eq!(state.training_sample_size.get(), y.len() as u64);
7200        let restored = ResidualCascadeFit::from_state(&state).expect("restore cascade fit");
7201        assert_eq!(
7202            restored.training_sample_size(),
7203            y.len(),
7204            "prediction-only materialization must retain the original row count"
7205        );
7206
7207        let mut encoded = serde_json::to_value(&state).expect("serialize cascade state");
7208        encoded
7209            .as_object_mut()
7210            .expect("cascade state serializes as an object")
7211            .remove("training_sample_size");
7212        assert!(
7213            serde_json::from_value::<ResidualCascadeState>(encoded).is_err(),
7214            "pre-training-size cascade state must not deserialize"
7215        );
7216
7217        let mut zero = serde_json::to_value(&state).expect("serialize cascade state");
7218        zero.as_object_mut()
7219            .expect("cascade state serializes as an object")
7220            .insert("training_sample_size".to_string(), serde_json::json!(0));
7221        let error = serde_json::from_value::<ResidualCascadeState>(zero)
7222            .expect_err("zero training rows must not deserialize");
7223        assert!(
7224            error.to_string().contains("nonzero"),
7225            "zero-row rejection reported an unrelated error: {error}"
7226        );
7227    }
7228
7229    /// A Krylov space that has reached `rank(B) <= n - nullity` is invariant even
7230    /// when its numerical `tail` says otherwise, and the rule there IS the exact
7231    /// spectrum.
7232    ///
7233    /// This is the ceiling that decides whether the iterative route can ever leave
7234    /// the solve. A bounding-box-filled cascade has far more columns than the data
7235    /// can pin — measured on #2503's own `n = 800` 2-D fixture at refinement level
7236    /// 6, `rank = 7387` against `n - nullity = 797`, so 89% of the whitened Schur
7237    /// spectrum is exactly zero. Reading the invariance ceiling as `rank` made the
7238    /// test unreachable and the route fell back to the solve at full budget, which
7239    /// is exactly the wall this issue is about.
7240    ///
7241    /// The claim being gated is that the ceiling is a THEOREM and not an
7242    /// optimism: `B = Z'WZ` with `W^{1/2}Z = (I − P)W^{1/2}X₁` and `P` of rank
7243    /// `nullity`, so `rank(B) ≤ n − nullity`; `β = Z'Wy ∈ range(B)`; and the
7244    /// Krylov space cannot leave `range(B)`. The fixture puts the ceiling strictly
7245    /// below `rank` and charges the rule at the ceiling against the exact dense
7246    /// eigenbasis over the whole domain.
7247    #[test]
7248    fn a_krylov_space_at_the_rank_ceiling_reproduces_the_exact_spectrum_2503() {
7249        let (x1, x2, y) = dense_fixture(20);
7250        let design = cascade_core((&x1, &x2, &y), 5);
7251        let core = &design.core;
7252        assert!(core.dense_gram.is_some());
7253        let rank = core.m - core.nullity();
7254        let ceiling = core.residual_krylov_ceiling();
7255        assert!(
7256            ceiling < rank,
7257            "premise: this fixture must have MORE penalized columns than the data can pin \
7258             (rank {rank}, ceiling {ceiling}), or the ceiling is not being exercised"
7259        );
7260        assert_eq!(
7261            ceiling,
7262            y.len() - core.nullity(),
7263            "the ceiling must be `n - nullity` when that is the binding bound"
7264        );
7265
7266        let (null_chol, _) = core.null_gram_factor().expect("null factor");
7267        let (modes, exact) = core
7268            .dense_cascade_spectrum(&null_chol)
7269            .expect("dense spectrum");
7270        let (lo, hi) = certified_log_lambda_domain_from_modes(&modes).expect("domain");
7271        let (beta, anchor) = core.whitened_residual_rhs(&null_chol);
7272        let mass = beta.iter().map(|value| value * value).sum::<f64>();
7273        let run = core
7274            .schur_lanczos(&null_chol, &beta, ceiling, ceiling)
7275            .expect("lanczos");
7276        assert_eq!(run.alpha.len(), ceiling, "the run must reach the ceiling");
7277        assert!(
7278            run.invariant,
7279            "a run that consumed the whole reachable dimension must report invariance \
7280             (tail/scale {})",
7281            run.tail / run.spectral_scale
7282        );
7283        let rule = core
7284            .residual_gauss_rule(&run, ceiling, anchor, mass)
7285            .expect("gauss rule");
7286
7287        let resolution = f64::EPSILON.sqrt();
7288        for step in 0..=192 {
7289            let lambda = (lo + (hi - lo) * step as f64 / 192.0).exp();
7290            let (r_exact, s2, s3, s4) = exact.moments(lambda);
7291            let (r_gauss, g2, g3, g4) = rule.spectrum.moments(lambda);
7292            assert!(
7293                (r_gauss - r_exact).abs() <= resolution * anchor.abs(),
7294                "profiled residual at the ceiling disagrees at lambda {lambda}: {r_gauss} \
7295                 versus {r_exact}"
7296            );
7297            for (name, got, truth) in [("S2", g2, s2), ("S3", g3, s3), ("S4", g4, s4)] {
7298                assert!(
7299                    (got - truth).abs() <= resolution * truth.abs(),
7300                    "{name} at the ceiling disagrees at lambda {lambda}: {got} versus {truth}"
7301                );
7302            }
7303        }
7304    }
7305
7306    /// A Ritz node whose WEIGHT is roundoff must not reach the spectrum, because
7307    /// `(θ + λ)^{-k}` will amplify it by `λ^{-k}` at the bottom of the domain.
7308    ///
7309    /// The mechanism, measured: on this fixture at 96 steps one node lands at
7310    /// `θ = 2.6e-11` — above the eigenvalue roundoff floor, so that floor passes
7311    /// it — carrying weight `8.9e-27·‖β‖²`. At `λ ≈ 2.9e-11` it contributes
7312    /// `w/(θ+λ)⁴` and `S₄` comes out `3.6e7` RELATIVE off while `S₂` is still
7313    /// right to `6e-9`. #2503 read that as quadrature truncation and concluded the
7314    /// approach was refuted; it is one node of pure roundoff.
7315    ///
7316    /// The test builds BOTH rules from the SAME Lanczos run — the shipped one and
7317    /// one with the weight floor removed — so it names the mechanism rather than
7318    /// asserting a number that some other change could also produce.
7319    #[test]
7320    fn roundoff_weight_nodes_cannot_poison_the_derivative_moments_2503() {
7321        let (x1, x2, y) = dense_fixture(14);
7322        let design = cascade_core((&x1, &x2, &y), 4);
7323        let core = &design.core;
7324        assert!(core.dense_gram.is_some());
7325        let (null_chol, _) = core.null_gram_factor().expect("null factor");
7326        let (modes, exact) = core
7327            .dense_cascade_spectrum(&null_chol)
7328            .expect("dense spectrum");
7329        let (lo, hi) = certified_log_lambda_domain_from_modes(&modes).expect("domain");
7330        let (beta, anchor) = core.whitened_residual_rhs(&null_chol);
7331        let mass = beta.iter().map(|value| value * value).sum::<f64>();
7332        let steps = 96;
7333        let run = core
7334            .schur_lanczos(&null_chol, &beta, steps, core.residual_krylov_ceiling())
7335            .expect("lanczos");
7336        assert_eq!(run.alpha.len(), steps, "premise: the run must not close early");
7337        let shipped = core
7338            .residual_gauss_rule(&run, steps, anchor, mass)
7339            .expect("gauss rule");
7340
7341        // The same run, with ONLY the eigenvalue floor — what the weight floor is
7342        // being charged against.
7343        let (ritz, first) =
7344            symmetric_tridiagonal_eigen(&run.alpha, &run.beta[..steps - 1]).expect("eigen");
7345        let scale = ritz.iter().copied().map(f64::abs).fold(0.0, f64::max);
7346        let eigenvalue_floor = f64::EPSILON * steps as f64 * scale;
7347        let mut eigenvalue = Vec::with_capacity(steps);
7348        let mut projected_square = Vec::with_capacity(steps);
7349        let mut poison = None;
7350        for (&theta, &component) in ritz.iter().zip(first.iter()) {
7351            let weight = run.start_norm_sq * component * component;
7352            if theta <= eigenvalue_floor {
7353                eigenvalue.push(0.0);
7354                projected_square.push(0.0);
7355                continue;
7356            }
7357            if theta < lo.exp() {
7358                poison = Some((theta, weight / mass));
7359            }
7360            eigenvalue.push(theta);
7361            projected_square.push(weight);
7362        }
7363        let unfloored = CascadeResidualSpectrum {
7364            eigenvalue,
7365            penalty: vec![1.0; steps],
7366            projected_square,
7367            anchor_energy: [anchor],
7368        };
7369        let (theta, relative_weight) = poison.expect(
7370            "premise: the eigenvalue floor alone must leave a node below the domain's smallest \
7371             lambda, which is the node this test is about",
7372        );
7373        let component_roundoff = f64::EPSILON * steps as f64;
7374        assert!(
7375            relative_weight <= component_roundoff * component_roundoff,
7376            "premise: that node's weight must be the SQUARE of the roundoff in a Ritz vector's \
7377             first component, `(eps*m)^2 = {}` (theta {theta}, w/||beta||^2 {relative_weight})",
7378            component_roundoff * component_roundoff
7379        );
7380
7381        let bottom = lo.exp();
7382        let (_, _, _, s4_exact) = exact.moments(bottom);
7383        let (_, _, _, s4_unfloored) = unfloored.moments(bottom);
7384        let (_, _, _, s4_shipped) = shipped.spectrum.moments(bottom);
7385        let unfloored_error = (s4_unfloored - s4_exact).abs() / s4_exact.abs();
7386        let shipped_error = (s4_shipped - s4_exact).abs() / s4_exact.abs();
7387        assert!(
7388            unfloored_error > 1.0,
7389            "premise: without the weight floor S4 must be wrong by more than 100% at the domain \
7390             bottom (got {unfloored_error}); if it is not, this fixture no longer exercises the \
7391             mechanism"
7392        );
7393        assert!(
7394            shipped_error <= f64::EPSILON.sqrt(),
7395            "the weight floor must restore S4 at the domain bottom: relative error \
7396             {shipped_error} against {unfloored_error} unfloored"
7397        );
7398        assert!(
7399            shipped.dropped_mass_fraction <= f64::EPSILON,
7400            "the mass the floor dropped must itself be roundoff: {}",
7401            shipped.dropped_mass_fraction
7402        );
7403        // ...and dropping it must not disturb the moments the rule got right.
7404        for step in 0..=64 {
7405            let lambda = (lo + (hi - lo) * step as f64 / 64.0).exp();
7406            let (r_exact, s2_exact, ..) = exact.moments(lambda);
7407            let (r_gauss, s2_gauss, ..) = shipped.spectrum.moments(lambda);
7408            assert!(
7409                (r_gauss - r_exact).abs() <= f64::EPSILON.sqrt() * anchor.abs()
7410                    && (s2_gauss - s2_exact).abs() <= f64::EPSILON.sqrt() * s2_exact.abs(),
7411                "the floored rule must keep the moments it already had right at lambda {lambda}"
7412            );
7413        }
7414    }
7415
7416    /// The admission rule's load-bearing claim: when the geometric tail estimate
7417    /// over three nested Gauss rules falls below `sqrt(eps)`, the finest rule
7418    /// really is that close to the EXACT spectrum.
7419    ///
7420    /// The admission evidence is a self-comparison — it never sees the truth — so the
7421    /// inference from "the ladder has contracted" to "the rule is right" is the
7422    /// thing that has to be measured. It rests on two facts and one model: every
7423    /// Gauss rule for a completely monotone kernel under-estimates its integral,
7424    /// so the ladder rises toward the truth; the gaps are therefore all of one
7425    /// sign and the remaining error is their tail; and the tail is extrapolated
7426    /// geometrically from the last two gaps, refusing outright when they do not
7427    /// contract. This charges that inference against the dense eigenbasis at every
7428    /// budget on the PRODUCTION ladder, over three designs — including the budgets
7429    /// the admission REFUSES, where it must be the refusal that is right.
7430    #[test]
7431    fn the_quadrature_tail_estimate_bounds_the_error_against_the_exact_spectrum_2503() {
7432        for (side, levels) in [(14usize, 4usize), (20, 5), (28, 5)] {
7433            let (x1, x2, y) = dense_fixture(side);
7434            let design = cascade_core((&x1, &x2, &y), levels);
7435            let core = &design.core;
7436            assert!(core.dense_gram.is_some());
7437            let (null_chol, _) = core.null_gram_factor().expect("null factor");
7438            let (modes, exact) = core
7439                .dense_cascade_spectrum(&null_chol)
7440                .expect("dense spectrum");
7441            let (lo, hi) = certified_log_lambda_domain_from_modes(&modes).expect("domain");
7442            let (beta, anchor) = core.whitened_residual_rhs(&null_chol);
7443            let mass = beta.iter().map(|value| value * value).sum::<f64>();
7444            let rank = core.m - core.nullity();
7445            let target = f64::EPSILON.sqrt();
7446            let ceiling = core.residual_krylov_ceiling();
7447            let budget = core.residual_quadrature_budget();
7448            let mut accepted_at_least_once = false;
7449            let mut refused_at_least_once = false;
7450
7451            // The same start and the same geometric growth
7452            // `iterative_residual_spectrum` walks, so what is charged here ships.
7453            let mut steps = SLQ_LANCZOS_STEPS.min(budget);
7454            loop {
7455                let run = core
7456                    .schur_lanczos(&null_chol, &beta, steps, ceiling)
7457                    .expect("lanczos");
7458                let taken = run.alpha.len();
7459                let rule = |nodes: usize| {
7460                    core.residual_gauss_rule(&run, nodes, anchor, mass)
7461                        .expect("gauss rule")
7462                        .spectrum
7463                };
7464                let (fine, mid, coarse) = (rule(taken), rule(taken / 2), rule(taken / 4));
7465                let estimate =
7466                    residual_quadrature_tail_estimate(&fine, &mid, &coarse, taken, (lo, hi))
7467                        .expect("tail estimate");
7468
7469                let mut worst = 0.0_f64;
7470                for step in 0..=96 {
7471                    let lambda = (lo + (hi - lo) * step as f64 / 96.0).exp();
7472                    let truth = exact.moment_sums(lambda);
7473                    let got = fine.moment_sums(lambda);
7474                    for (truth, got) in truth.into_iter().zip(got) {
7475                        worst = worst.max((got - truth).abs() / truth.abs().max(f64::MIN_POSITIVE));
7476                    }
7477                }
7478
7479                if run.invariant || estimate <= target {
7480                    accepted_at_least_once = true;
7481                    assert!(
7482                        worst <= target,
7483                        "side={side} levels={levels} steps={taken}: the tail estimate was \
7484                         {estimate} (target {target}, invariant {}) but the rule is {worst} from \
7485                         the exact spectrum — the admission rule's inference is unsound",
7486                        run.invariant
7487                    );
7488                } else {
7489                    refused_at_least_once = true;
7490                }
7491                if taken >= budget {
7492                    break;
7493                }
7494                steps = (steps * 2).min(budget);
7495            }
7496            assert!(
7497                accepted_at_least_once,
7498                "side={side} levels={levels}: the growth ladder must reach an admitted point \
7499                 rule inside the budget ({budget} of rank {rank}), or the diagnostic criterion \
7500                 remains unavailable"
7501            );
7502            assert!(
7503                refused_at_least_once,
7504                "side={side} levels={levels}: the ladder must also contain a REFUSED budget, or \
7505                 the criterion is not being exercised"
7506            );
7507        }
7508    }
7509
7510    /// The Occam term the refinement decides on is READ OFF two restricted
7511    /// log-likelihoods; this checks it against the object it claims to be, by a
7512    /// route that shares no code with it.
7513    ///
7514    /// `2·evidence = dof·log(rss_pen/rss_pen_refined) − occam` is an identity
7515    /// only if `occam` really is `log det(S/(λd))` for the candidate Schur
7516    /// complement `S = X₂ᵀW(I − H)X₂ + λd·I`. So: form that `S` DENSELY, one
7517    /// column at a time, through the same matrix-free operator the gain bracket
7518    /// iterates on, take its Cholesky log-determinant, and compare. One side
7519    /// comes from two profiled REML evaluations of two different designs; the
7520    /// other from `m₂` cascade solves. Nothing is shared but the arithmetic
7521    /// they must agree on.
7522    ///
7523    /// This is the gate the second half of #2759 rests on, the way
7524    /// `the_refinement_gain_bracket_contains_the_objective_decrease_it_bounds_2759`
7525    /// is the gate the first half rests on.
7526    #[test]
7527    fn the_occam_term_read_off_the_two_fits_is_the_schur_log_determinant_2759() {
7528        let (x1, x2, y) = dense_fixture(18);
7529        let weights = vec![1.0; y.len()];
7530        let axes: [&[f64]; 2] = [&x1, &x2];
7531        let metric = [1.0, 1.0];
7532        let sobolev_s = 2.0;
7533        let plan: Vec<LevelPlan> = (0..3)
7534            .map(|level| LevelPlan {
7535                exponent: level as f64,
7536                centers: None,
7537            })
7538            .collect();
7539        let design =
7540            ResidualCascadeDesign::build_from_plan(&axes, &y, &weights, &metric, sobolev_s, &plan)
7541                .expect("cascade design");
7542        let core = &design.core;
7543        let exponent = plan.len() as f64;
7544        let dof = (y.len() - core.nullity()) as f64;
7545        let mut checked = 0_usize;
7546        for log_lambda in [-4.0_f64, -1.0, 2.0] {
7547            let fit = design.fit_at(log_lambda, None).expect("fixed-lambda fit");
7548            let mut extended = plan.clone();
7549            extended.push(LevelPlan {
7550                exponent,
7551                centers: None,
7552            });
7553            let refined_design = ResidualCascadeDesign::build_from_plan(
7554                &axes,
7555                &y,
7556                &weights,
7557                &metric,
7558                sobolev_s,
7559                &extended,
7560            )
7561            .expect("refined design");
7562            let refined = refined_design
7563                .fit_at(log_lambda, None)
7564                .expect("refined fixed-lambda fit");
7565            let comparison =
7566                level_evidence(&fit, &refined, (y.len() - refined_design.core.nullity()) as f64)
7567                    .expect("level comparison");
7568
7569            // The independent route: `S` column by column, through the same
7570            // matrix-free operator, then a dense Cholesky log-determinant.
7571            let h = core.levels[0].h * 0.5_f64.powf(exponent);
7572            let mut net = core.net.clone();
7573            let candidates = extend_net(&mut net, &core.z, core.dim, h, &core.z_range);
7574            assert!(
7575                !candidates.is_empty(),
7576                "premise: the fixture must offer a candidate level at exponent {exponent}"
7577            );
7578            let delta = OVERLAP * h;
7579            let mut grid = HashGrid::new(delta, core.dim);
7580            for (j, c) in candidates.iter().enumerate() {
7581                grid.insert(j as u32, c);
7582            }
7583            let lambda = log_lambda.exp();
7584            let ridge = lambda * level_weight(exponent, core.sobolev_s, core.dim);
7585            let level = CandidateLevel {
7586                centers: &candidates,
7587                grid: &grid,
7588                delta,
7589                ridge,
7590            };
7591            let width = candidates.len();
7592            let mut workspace = SchurWorkspace {
7593                row: vec![0.0_f64; core.z.len()],
7594                fitted: vec![0.0_f64; core.z.len()],
7595                column: vec![0.0_f64; core.m],
7596                warm: None,
7597            };
7598            let mut schur = vec![0.0_f64; width * width];
7599            let mut unit = vec![0.0_f64; width];
7600            let mut column = vec![0.0_f64; width];
7601            for j in 0..width {
7602                unit[j] = 1.0;
7603                apply_candidate_schur(core, &level, lambda, &unit, &mut column, &mut workspace)
7604                    .expect("schur apply");
7605                for (i, &value) in column.iter().enumerate() {
7606                    // `S/(λd)`: the determinant the Occam factor is of.
7607                    schur[i * width + j] = value / ridge;
7608                }
7609                unit[j] = 0.0;
7610            }
7611            // The operator is symmetric in exact arithmetic; the cascade solve
7612            // inside it carries `CG_RTOL`, so symmetrize rather than assert.
7613            for i in 0..width {
7614                for j in (i + 1)..width {
7615                    let mean = 0.5 * (schur[i * width + j] + schur[j * width + i]);
7616                    schur[i * width + j] = mean;
7617                    schur[j * width + i] = mean;
7618                }
7619            }
7620            let dense_occam = cholesky_logdet(&mut schur, width).expect("schur log-determinant");
7621
7622            // Both sides are sums of `width` logarithms of O(1) numbers built
7623            // from `CG_RTOL`-certified solves, so the agreement is charged per
7624            // mode rather than in absolute nats.
7625            let slack = 1e-6 * (width as f64) * dense_occam.abs().max(1.0);
7626            assert!(
7627                (comparison.occam - dense_occam).abs() <= slack,
7628                "the Occam term read off the two restricted likelihoods is {} but the candidate \
7629                 Schur log-determinant is {dense_occam} at log lambda {log_lambda} (width \
7630                 {width}, slack {slack})",
7631                comparison.occam
7632            );
7633
7634            // The identity the whole criterion is stated in, and the equivalence
7635            // the certificate's two readings rest on.
7636            let restated = 0.5
7637                * (dof * (fit.rss_pen / refined.rss_pen).ln() - comparison.occam);
7638            assert!(
7639                (restated - comparison.evidence).abs()
7640                    <= 1e-9 * comparison.evidence.abs().max(1.0),
7641                "2·evidence = dof·log(rss/rss_refined) − occam failed: {restated} vs {}",
7642                comparison.evidence
7643            );
7644            assert_eq!(
7645                comparison.warrants_refinement(),
7646                comparison.gain > comparison.tolerance,
7647                "the evidence reading and the break-even reading disagree: {comparison}"
7648            );
7649            assert!(
7650                comparison.occam >= 0.0 && comparison.gain >= 0.0,
7651                "a PSD Schur complement cannot charge less than nothing, and a superset design \
7652                 cannot minimize higher: {comparison}"
7653            );
7654            checked += 1;
7655        }
7656        assert_eq!(checked, 3, "every lambda in the sweep must have been charged");
7657    }
7658
7659    /// The comparison the refinement decides on differences two restricted
7660    /// log-likelihoods produced by DIFFERENT routes: the incumbent's comes from
7661    /// `fit_reml`, which normalizes the log-determinant through the certified
7662    /// λ-independent Schur eigenbasis, and the candidate's from `fit_at`, which
7663    /// factorizes `X'WX + λD` at that λ directly. The difference is a decision
7664    /// at O(1) nats while each side is O(10³), so the two routes agreeing is a
7665    /// premise of the criterion, not a nicety (#2759).
7666    ///
7667    /// Charged on both width regimes: under the dense Gram cache, where
7668    /// `fit_at` takes a dense Cholesky, and past it, where it takes the sparse
7669    /// exact factor — the routes the comparison actually meets.
7670    #[test]
7671    fn the_certified_and_fixed_lambda_routes_report_the_same_restricted_likelihood_2759() {
7672        for (side, levels, dense_arm) in [(18_usize, 3_usize, true), (44, 6, false)] {
7673            let (x1, x2, y) = dense_fixture(side);
7674            let weights = vec![1.0; y.len()];
7675            let axes: [&[f64]; 2] = [&x1, &x2];
7676            let design = ResidualCascadeDesign::build(&axes, &y, &weights, &[1.0, 1.0], 2.0, levels)
7677                .expect("cascade design");
7678            assert_eq!(
7679                design.core.dense_gram.is_some(),
7680                dense_arm,
7681                "premise: the two arms must straddle the dense Gram cache at \
7682                 {DENSE_GRAM_MAX} columns (side {side}, {levels} levels, {} columns)",
7683                design.core.m
7684            );
7685            let selected = design.fit_reml().expect("certified REML fit");
7686            let replayed = design
7687                .fit_at(selected.log_lambda, None)
7688                .expect("fixed-lambda replay");
7689            // Both sides are sums over `m` modes of O(1) logarithms; charge the
7690            // agreement per mode rather than in absolute nats.
7691            let slack = 1e-9 * (design.core.m as f64);
7692            assert!(
7693                (selected.restricted_loglik - replayed.restricted_loglik).abs() <= slack,
7694                "the certified route reports restricted log-likelihood {} and the fixed-lambda \
7695                 route {} at log lambda {} (side {side}, {} columns, slack {slack}) — the level \
7696                 comparison differences these two",
7697                selected.restricted_loglik,
7698                replayed.restricted_loglik,
7699                selected.log_lambda,
7700                design.core.m
7701            );
7702            assert!(
7703                (selected.rss_pen - replayed.rss_pen).abs()
7704                    <= CG_RTOL * selected.rss_pen.abs().max(1.0),
7705                "the two routes disagree on the penalized residual itself: {} vs {}",
7706                selected.rss_pen,
7707                replayed.rss_pen
7708            );
7709        }
7710    }
7711
7712    /// The refinement stops where the EVIDENCE turns over, and the held-out
7713    /// truth agrees with it (#2759).
7714    ///
7715    /// Two fixtures in the regime this issue is about — refined until the
7716    /// design is rank-maximal, where the candidate columns are redundant
7717    /// against the data's own row space and what they buy is penalty dilution,
7718    /// not discretization bias. The shipped `1e-3·rss_pen` bar demanded another
7719    /// level at both and refused the fit when capacity could not supply it.
7720    ///
7721    /// The claim charged here is the one that makes the criterion mean
7722    /// something, and it is charged the same way whichever way the cascade
7723    /// decides: **a strictly deeper design must not predict better on held-out
7724    /// truth when the criterion says stop, and must not predict worse when it
7725    /// says keep going.** The truth is not in the criterion, so this is an
7726    /// independent witness and not a restatement.
7727    #[test]
7728    fn the_refinement_stops_where_the_evidence_turns_over_and_the_truth_agrees_2759() {
7729        struct TestRng(u64);
7730        impl TestRng {
7731            fn uniform(&mut self) -> f64 {
7732                self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
7733                let mut z = self.0;
7734                z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
7735                z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
7736                ((z ^ (z >> 31)) >> 11) as f64 / (1_u64 << 53) as f64
7737            }
7738            fn normal(&mut self) -> f64 {
7739                let u1 = (self.uniform() + f64::EPSILON).min(1.0 - f64::EPSILON);
7740                let u2 = self.uniform();
7741                (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
7742            }
7743        }
7744        type Truth = fn(f64, f64) -> f64;
7745        let smoothness_ceiling: Truth = |a, b| {
7746            (4.0 * std::f64::consts::PI * a).sin() * (4.0 * std::f64::consts::PI * b).cos()
7747        };
7748        let planted_sine: Truth = |a, b| {
7749            (2.0 * std::f64::consts::PI * a).sin() * (2.0 * std::f64::consts::PI * b).sin()
7750        };
7751
7752        // (name, rows, noise, seed, sobolev_s, weighted, truth). The first is
7753        // `smoothness_ceiling_forces_refinement_and_certifies_residual_bias` at
7754        // a third of its rows; the second is the `#2628`/`wendland_fixture_...`
7755        // sample, whose 240 rows identify 237 directions.
7756        let fixtures: [(&str, usize, f64, u64, f64, bool, Truth); 2] = [
7757            (
7758                "smoothness-2000",
7759                2000,
7760                0.02,
7761                0x1032_000C,
7762                2.0,
7763                false,
7764                smoothness_ceiling,
7765            ),
7766            (
7767                "wendland-240",
7768                240,
7769                0.05,
7770                0x1032_0008,
7771                2.5,
7772                true,
7773                planted_sine,
7774            ),
7775        ];
7776
7777        for (name, n, noise, seed, sobolev_s, weighted, truth) in fixtures {
7778            let mut rng = TestRng(seed);
7779            let (mut x1, mut x2, mut y, mut w) = (Vec::new(), Vec::new(), Vec::new(), Vec::new());
7780            for row in 0..n {
7781                let a = rng.uniform();
7782                let b = rng.uniform();
7783                x1.push(a);
7784                x2.push(b);
7785                y.push(truth(a, b) + noise * rng.normal());
7786                w.push(if weighted && row % 7 == 0 { 0.5 } else { 1.0 });
7787            }
7788            let axes: [&[f64]; 2] = [&x1, &x2];
7789            let metric = [1.0, 1.0];
7790
7791            let held_out_rmse = |fit: &ResidualCascadeFit| -> f64 {
7792                let grid = 25_usize;
7793                let mut sse = 0.0;
7794                for i in 0..grid {
7795                    for j in 0..grid {
7796                        let px = (i as f64 + 0.5) / grid as f64;
7797                        let py = (j as f64 + 0.5) / grid as f64;
7798                        let (mean, _) = fit.predict(&[px, py]).expect("predict");
7799                        let error = mean - truth(px, py);
7800                        sse += error * error;
7801                    }
7802                }
7803                (sse / (grid * grid) as f64).sqrt()
7804            };
7805
7806            match fit_residual_cascade(&axes, &y, &w, &metric, sobolev_s) {
7807                Ok(fit) => {
7808                    let certificate =
7809                        fit.refinement.expect("a minted fit carries its comparison");
7810                    assert!(
7811                        !certificate.warrants_refinement()
7812                            && certificate.gain <= certificate.tolerance,
7813                        "{name}: a minted fit's binding candidate set must not earn a level: \
7814                         {certificate}"
7815                    );
7816                    assert!(
7817                        fit.num_levels() > INITIAL_LEVELS,
7818                        "{name}: premise — the truth must force refinement past the initial \
7819                         depth, got {} levels",
7820                        fit.num_levels()
7821                    );
7822                    assert_eq!(
7823                        fit.num_centers(),
7824                        n - fit.core.nullity(),
7825                        "{name}: premise — this fixture must stop AT the rank-maximal design, \
7826                         which is the regime #2759 is about"
7827                    );
7828
7829                    // The candidate set the FIXED relative bar would have read:
7830                    // the complete next dyadic level. Its comparison is the one
7831                    // that must show this fixture is still in the regime.
7832                    let deeper = ResidualCascadeDesign::build(
7833                        &axes,
7834                        &y,
7835                        &w,
7836                        &metric,
7837                        sobolev_s,
7838                        fit.num_levels() + 1,
7839                    )
7840                    .expect("one level deeper, complete");
7841                    let deeper_fit = deeper
7842                        .fit_at(fit.log_lambda, None)
7843                        .expect("deeper fixed-lambda fit");
7844                    let deeper_comparison = level_evidence(
7845                        &fit,
7846                        &deeper_fit,
7847                        (n - deeper.core.nullity()) as f64,
7848                    )
7849                    .expect("deeper level comparison");
7850                    assert!(
7851                        deeper_comparison.gain > 5.0 * 1e-3 * fit.rss_pen,
7852                        "{name}: premise — the minted fit must be one the fixed relative bar \
7853                         would have refused, got {deeper_comparison} against 1e-3·rss_pen {}",
7854                        1e-3 * fit.rss_pen
7855                    );
7856                    assert!(
7857                        !deeper_comparison.warrants_refinement(),
7858                        "{name}: the cascade stopped, but a strictly deeper design EARNS its \
7859                         Occam factor: {deeper_comparison}"
7860                    );
7861
7862                    // The comparison is taken at the INCUMBENT's λ, so the
7863                    // standing objection is that the deeper design would win it
7864                    // back at a λ of its own. Every λ tried is a valid witness
7865                    // FOR the deeper design — it needs only one — so a sweep
7866                    // that finds none is the strongest form this check can
7867                    // take short of a global argument. (Structurally: at the
7868                    // turnover the extra columns are redundant, so the score
7869                    // surface barely moves and its optimum does not; a level
7870                    // that mattered only through λ would have to matter and not
7871                    // matter at once.)
7872                    for step in [-3.0_f64, -2.0, -1.0, 1.0, 2.0, 3.0] {
7873                        let Ok(other) = deeper.fit_at(fit.log_lambda + step, None) else {
7874                            continue;
7875                        };
7876                        assert!(
7877                            other.restricted_loglik <= fit.restricted_loglik,
7878                            "{name}: the cascade stopped at log lambda {}, but the deeper design \
7879                             wins the comparison back at {} ({} vs {})",
7880                            fit.log_lambda,
7881                            fit.log_lambda + step,
7882                            other.restricted_loglik,
7883                            fit.restricted_loglik
7884                        );
7885                    }
7886
7887                    // The independent witness.
7888                    let stopped = held_out_rmse(&fit);
7889                    let deeper_rmse = held_out_rmse(&deeper_fit);
7890                    eprintln!(
7891                        "[2759] {name}: minted at {} levels / {} centers; {certificate}; one \
7892                         level deeper: {deeper_comparison}; held-out rmse {stopped} -> \
7893                         {deeper_rmse}",
7894                        fit.num_levels(),
7895                        fit.num_centers(),
7896                    );
7897                    assert!(
7898                        stopped < 0.2,
7899                        "{name}: premise — the cascade must resolve the planted truth before \
7900                         this comparison means anything, got rmse {stopped}"
7901                    );
7902                    assert!(
7903                        deeper_rmse >= stopped,
7904                        "{name}: the criterion stopped, but one more level IMPROVES the \
7905                         held-out error ({deeper_rmse} vs {stopped}) — then it stopped too \
7906                         early and the charge is wrong"
7907                    );
7908                }
7909                Err(ResidualCascadeError::Underresolved {
7910                    checkpoint,
7911                    evidence,
7912                    obstruction,
7913                }) => {
7914                    let evidence = evidence.expect(
7915                        "a formable candidate set must be compared, not merely bounded",
7916                    );
7917                    assert!(
7918                        evidence.warrants_refinement() && evidence.gain > evidence.tolerance,
7919                        "{name}: a refusal must carry a candidate set that still earns its own \
7920                         Occam factor: {evidence}"
7921                    );
7922                    eprintln!(
7923                        "[2759] {name}: refused at {} levels / {} centers; {evidence}; \
7924                         {obstruction}",
7925                        checkpoint.num_levels(),
7926                        checkpoint.num_centers(),
7927                    );
7928                }
7929                Err(other) => panic!("{name}: unexpected cascade outcome: {other}"),
7930            }
7931        }
7932    }
7933
7934    /// A cascade design the data cannot identify is CERTIFIABLE, and the
7935    /// certified route is what certifies it.
7936    ///
7937    /// This is the end-to-end angle on the centred-form repair in
7938    /// `AffineRemlProfile::enclose`, and it is deliberately the design three
7939    /// places in this file used to describe as impossible: `dense_fixture(6)`
7940    /// at `levels = 6` is 36 rows against 1725 columns, a Schur rank of 1722
7941    /// against 33 identifiable directions. Nothing about it is pathological —
7942    /// it is what a geometric box-filling net produces on a small sample, which
7943    /// is to say the ordinary small-`n` case.
7944    ///
7945    /// What used to happen: the score's VALUE enclosure was a natural interval
7946    /// extension whose overestimation was first order in the cell width with
7947    /// constant `rank` (`33.0·w`, measured over six decades), while the exact
7948    /// score moved by `|f'|·w` with `|f'| = 1.15e-5`. `resolution_flat_region`
7949    /// reads that range, so no cell could be retired, no cell could be
7950    /// derivative-excluded, and `maximize_score_1d` refused at 8193/8192
7951    /// subdivisions. The failure was reported — correctly — as
7952    /// `RemlScoreSearchUndecomposable`, which named the design's rank and the
7953    /// sample's identifiability and looked for all the world like a statement
7954    /// about the data.
7955    ///
7956    /// It was a statement about the enclosure. Centring the value and derivative
7957    /// forms on the cell midpoint makes the overestimation second order, and the
7958    /// same design certifies in about a second.
7959    ///
7960    /// The premise is asserted rather than assumed, because a fixture that
7961    /// drifted out of the rank-deficient regime would pass this gate while
7962    /// proving nothing.
7963    #[test]
7964    fn auto_reml_certifies_a_design_the_data_cannot_identify() {
7965        let (x1, x2, y) = dense_fixture(6);
7966        let weights = vec![1.0; y.len()];
7967        let axes: [&[f64]; 2] = [&x1, &x2];
7968        let design = ResidualCascadeDesign::build(&axes, &y, &weights, &[1.0, 1.0], 2.0, 6)
7969            .expect("cascade design");
7970        let core = &design.core;
7971        let nullity = core.nullity();
7972        let schur_rank = core.m - nullity;
7973        let identifiable = core.y.len() - nullity;
7974        assert!(
7975            schur_rank > identifiable,
7976            "premise: this fixture must be rank-deficient (Schur rank {schur_rank} against \
7977             {identifiable} identifiable directions)"
7978        );
7979        assert!(
7980            core.m <= CERTIFIED_SPECTRUM_MAX,
7981            "premise: the refusal under test must be the SEARCH's, not the spectrum budget's \
7982             ({} columns against {CERTIFIED_SPECTRUM_MAX})",
7983            core.m
7984        );
7985
7986        let fit = design.fit_reml().expect(
7987            "a rank-deficient cascade design must certify: the score is a genuine function of \
7988             log lambda on its POSITIVE modes, and refusing it was an artifact of a \
7989             first-order-loose value enclosure",
7990        );
7991        assert_eq!(fit.certificate.logdet_method, LogdetMethod::DenseExact);
7992        assert!(
7993            fit.log_lambda().is_finite(),
7994            "certified selection must return a finite log lambda, got {}",
7995            fit.log_lambda()
7996        );
7997        assert!(
7998            fit.rss_pen.is_finite() && fit.rss_pen > 0.0,
7999            "the minted fit must carry a positive penalized residual, got {}",
8000            fit.rss_pen
8001        );
8002
8003        // The certificate, not just the fit: the search must have reached a
8004        // decided location rather than been handed one, and the value ordering
8005        // must have closed. `fit_reml` already refuses
8006        // `RemlValueOrderingUnresolved` and a failed KKT, so reaching here is
8007        // that proof; this re-reads the search directly so a future change that
8008        // routes around `fit_reml` cannot make the gate vacuous.
8009        let profile = core.reml_profile().expect("spectral profile");
8010        let (lo, hi) = profile.log_lambda_domain().expect("domain");
8011        let affine = profile
8012            .affine_view()
8013            .expect("affine view")
8014            .expect("spectral residual form");
8015        let search = affine
8016            .maximize_value_ordered(lo, hi, f64::EPSILON.sqrt())
8017            .expect("the certified search must decompose this domain");
8018        assert!(
8019            !matches!(
8020                search.location,
8021                gam_math::score_opt::ScoreOptimumLocation::ResolutionFlat(_)
8022            ),
8023            "the optimum must be a decided location, not a resolution-flat region: {:?}",
8024            search.location
8025        );
8026        assert!(
8027            search.value_certificate.maximum_excess
8028                <= search.value_certificate.comparison_resolution,
8029            "the global value ordering must close: excess {} against comparison resolution {}",
8030            search.value_certificate.maximum_excess,
8031            search.value_certificate.comparison_resolution
8032        );
8033    }
8034
8035    /// Automatic REML across the identifiability frontier, both sides of it.
8036    ///
8037    /// The end-to-end gate above pins ONE rank-deficient design. This sweeps the
8038    /// frontier so a regression cannot hide in the shape of a single fixture:
8039    /// the same 36-row cloud at three net depths (rank-sufficient, then further
8040    /// and further past what 36 rows identify), plus one design at production
8041    /// row count. Every cell that is inside the certified spectrum budget must
8042    /// certify, whichever side of the frontier it sits on — the score is a
8043    /// genuine function of `log lambda` on its POSITIVE modes either way, and
8044    /// the number of columns the data cannot pin is not a reason to refuse it.
8045    ///
8046    /// The reported `deficiency` is `schur_rank − identifiable`: negative is
8047    /// rank-sufficient, positive is the regime the certified route used to
8048    /// refuse outright.
8049    #[test]
8050    fn zz_measure_auto_reml_across_the_identifiability_frontier() {
8051        for (side, levels) in [(6_usize, 4_usize), (6, 5), (6, 6), (45, 6)] {
8052            let (x1, x2, y) = dense_fixture(side);
8053            let weights = vec![1.0; y.len()];
8054            let axes: [&[f64]; 2] = [&x1, &x2];
8055            let design = ResidualCascadeDesign::build(&axes, &y, &weights, &[1.0, 1.0], 2.0, levels)
8056                .expect("cascade design");
8057            let core = &design.core;
8058            let nullity = core.nullity();
8059            let schur_rank = core.m - nullity;
8060            let identifiable = core.y.len() - nullity;
8061            let deficiency = schur_rank as i64 - identifiable as i64;
8062            if core.m > CERTIFIED_SPECTRUM_MAX {
8063                println!(
8064                    "#FRONTIER side={side} levels={levels} n={} m={} PAST THE SPECTRUM BUDGET",
8065                    core.y.len(),
8066                    core.m
8067                );
8068                continue;
8069            }
8070            let started = std::time::Instant::now();
8071            let outcome = design.fit_reml();
8072            let elapsed = started.elapsed().as_secs_f64();
8073            match &outcome {
8074                Ok(fit) => println!(
8075                    "#FRONTIER side={side} levels={levels} n={} m={} rank={schur_rank} \
8076                     identifiable={identifiable} deficiency={deficiency:+} \
8077                     CERTIFIED log_lambda={:.6} in {elapsed:.2}s",
8078                    core.y.len(),
8079                    core.m,
8080                    fit.log_lambda()
8081                ),
8082                Err(error) => println!(
8083                    "#FRONTIER side={side} levels={levels} n={} m={} rank={schur_rank} \
8084                     identifiable={identifiable} deficiency={deficiency:+} \
8085                     REFUSED in {elapsed:.2}s: {error}",
8086                    core.y.len(),
8087                    core.m
8088                ),
8089            }
8090            assert!(
8091                outcome.is_ok(),
8092                "side={side} levels={levels} (deficiency {deficiency:+}) is inside the certified \
8093                 spectrum budget and must certify; refusing a design because its net outruns its \
8094                 sample is what the first-order-loose value enclosure did"
8095            );
8096        }
8097    }
8098
8099    /// PROBE: what the certified REML search does on a cascade design the data
8100    /// cannot identify, and how loose its enclosure is while doing it.
8101    ///
8102    /// Named for what it measures rather than for the hypothesis it started
8103    /// from. `dense_cascade_spectrum` used to record that such a design "still
8104    /// spins in `AffineRemlProfile::enclose` under `maximize_score_1d` past
8105    /// 900 s", so this began as a hunt for the unbounded axis in
8106    /// `maximize_score_1d_value_ordered`'s retry loop. That loop is never
8107    /// entered: the FIRST traversal returned the typed `SubdivisionBudget`
8108    /// refusal in 5.6 s, #2546 having closed that axis. The second hypothesis —
8109    /// `subdivision_budget`'s own recommendation that "the request, not the
8110    /// budget, is what actually binds" — died on the resolution ladder below,
8111    /// which refused at every request from 1.49e-8 to 1e-3.
8112    ///
8113    /// What is left is what the probe now reports: the ladder, the end-to-end
8114    /// `fit_reml` outcome, and the cell-by-cell looseness of the enclosure
8115    /// against the bound its own derivative certifies. The last of those is
8116    /// where the defect was.
8117    #[test]
8118    fn zz_probe_rank_deficient_certified_search_and_enclosure_looseness() {
8119        let (x1, x2, y) = dense_fixture(6);
8120        let weights = vec![1.0; y.len()];
8121        let axes: [&[f64]; 2] = [&x1, &x2];
8122        let design = ResidualCascadeDesign::build(&axes, &y, &weights, &[1.0, 1.0], 2.0, 6)
8123            .expect("cascade design");
8124        let core = &design.core;
8125        let nullity = core.nullity();
8126        println!(
8127            "[PROBE] n={} m={} nullity={nullity} schur_rank={} identifiable={}",
8128            core.y.len(),
8129            core.m,
8130            core.m - nullity,
8131            core.y.len() - nullity
8132        );
8133        let profile = core.reml_profile().expect("spectral profile");
8134        let (lo, hi) = profile.log_lambda_domain().expect("domain");
8135        let affine = profile
8136            .affine_view()
8137            .expect("affine view")
8138            .expect("spectral residual form");
8139        let CascadeResidualForm::Spectral(spectrum) = &profile.residual else {
8140            panic!("expected the spectral residual form");
8141        };
8142        println!(
8143            "[PROBE] domain=[{lo:.6}, {hi:.6}] width={:.6} kept_modes={} det_modes={}",
8144            hi - lo,
8145            spectrum.eigenvalue.len(),
8146            profile.modes.len()
8147        );
8148
8149        // FALSIFIED, and the falsification points somewhere better. The ladder
8150        // below refuses at EVERY request from 1.49e-8 to 1e-3, and the failing
8151        // cell simply walks down the domain as the request coarsens. So the
8152        // request is not what binds and `subdivision_budget`'s recommendation is
8153        // not the repair here.
8154        //
8155        // What the failure record shows instead: on the terminal cell
8156        // `[-16.78595040183548, -16.784710795129012]` (width 1.2396e-3) the
8157        // enclosure reports
8158        //     score      = [36.611966585064685, 36.65287355922212]   (width 4.0907e-2)
8159        //     derivative = [-2.0454623868609193e-2, 2.045226959739566e-2]
8160        // and 4.0907e-2 / 1.2396e-3 = 33.0 EXACTLY — the number of kept modes —
8161        // while the derivative enclosure bounds the score's variation over that
8162        // same cell by 2.045e-2 * 1.2396e-3 = 2.535e-5, sixteen hundred times
8163        // smaller. Two enclosures of the same function disagree by 1600x, and
8164        // `resolution_flat_region` reads the loose one, so a cell that IS flat
8165        // at the evaluator's resolution is subdivided instead of retired.
8166        //
8167        // Only one of the two can be right, and which one it is decides whether
8168        // this is a tightness defect or a SOUNDNESS defect in a proof object.
8169        // That is what this measures, by finite differences of the same
8170        // evaluator, before anything is changed.
8171        println!("[PROBE] --- ladder: is the REQUEST what binds? ---");
8172        for request in [f64::EPSILON.sqrt(), 1.0e-6, 1.0e-4, 1.0e-3] {
8173            let started = std::time::Instant::now();
8174            let outcome = gam_math::score_opt::maximize_score_1d(
8175                lo,
8176                hi,
8177                request,
8178                |x| affine.evaluate(x),
8179                |a, b| affine.enclose(a.x, b.x),
8180            );
8181            match outcome {
8182                Ok(search) => println!(
8183                    "[PROBE] request={request:.3e} OK in {:.2}s location={:?}",
8184                    started.elapsed().as_secs_f64(),
8185                    search.location
8186                ),
8187                Err(error) => println!(
8188                    "[PROBE] request={request:.3e} ERR in {:.2}s {}",
8189                    started.elapsed().as_secs_f64(),
8190                    match &error {
8191                        gam_math::score_opt::ScoreSearchError::SubdivisionBudget {
8192                            cell_lo,
8193                            cell_hi,
8194                            subdivisions,
8195                            budget,
8196                            ..
8197                        } => format!(
8198                            "SubdivisionBudget {subdivisions}/{budget} at [{cell_lo:.9}, {cell_hi:.9}]"
8199                        ),
8200                        other => format!("{other:?}"),
8201                    }
8202                ),
8203            }
8204        }
8205
8206        println!("[PROBE] --- the profile as literals, for the gam-math capability gate ---");
8207        println!("[PROBE] dof={} rank={} null_logdet={:?} anchor={:?}",
8208            core.y.len() - nullity,
8209            spectrum.penalty.len(),
8210            profile.null_logdet,
8211            spectrum.anchor_energy[0]);
8212        println!("[PROBE] eigenvalue={:?}", spectrum.eigenvalue);
8213        println!("[PROBE] projected_square={:?}", spectrum.projected_square);
8214
8215        println!("[PROBE] --- end to end: does fit_reml certify this design now? ---");
8216        {
8217            let started = std::time::Instant::now();
8218            match design.fit_reml() {
8219                Ok(fit) => println!(
8220                    "[PROBE] fit_reml OK in {:.2}s log_lambda={} logdet={:?} rss_pen={}",
8221                    started.elapsed().as_secs_f64(),
8222                    fit.log_lambda(),
8223                    fit.certificate.logdet_method,
8224                    fit.rss_pen
8225                ),
8226                Err(error) => println!(
8227                    "[PROBE] fit_reml ERR in {:.2}s {error}",
8228                    started.elapsed().as_secs_f64()
8229                ),
8230            }
8231        }
8232
8233        println!("[PROBE] --- looseness: the enclosure against the function ---");
8234        // The terminal cell of the tightest rung, read at shrinking widths.
8235        // `evaluate` returns an ANALYTIC jet, so its derivative at an interior
8236        // point is the soundness check with signal in it; a finite difference
8237        // of two rounded values is not, because at these widths the numerator
8238        // is `2*eval_err` of noise over a `1e-5` base. The FD is printed with
8239        // its own noise bar next to it and asserted on nothing.
8240        let center = -16.785_330_598_482_25_f64;
8241        for exponent in [-1.0_f64, -2.0, -3.0, -4.0, -5.0, -6.0] {
8242            let half = 10.0_f64.powf(exponent);
8243            let (a, b) = (center - half, center + half);
8244            let width = b - a;
8245            let jet = affine.evaluate(center).expect("jet at the midpoint");
8246            let enclosure = affine.enclose(a, b).expect("enclosure");
8247            let score_width = enclosure.score.value.hi - enclosure.score.value.lo;
8248            let derivative_span = enclosure.derivative.hi.abs().max(enclosure.derivative.lo.abs());
8249            // What the derivative enclosure itself says the score can move by
8250            // across this cell. A VALUE enclosure wider than this is pure
8251            // overestimation: the two are enclosures of the same function.
8252            let mean_value_bound = derivative_span * width;
8253            let fd_noise = 2.0 * enclosure.score.evaluation_error / width;
8254            println!(
8255                "[PROBE] width={width:.3e} jet_d={:.9e} jet_dd={:.9e} \
8256                 encl_d=[{:.6e}, {:.6e}] encl_dd=[{:.6e}, {:.6e}] \
8257                 score_width={score_width:.6e} mvt_bound={mean_value_bound:.6e} \
8258                 value_looseness={:.4e} derivative_looseness={:.4e} \
8259                 flat_test={score_width:.3e}<=2eta={:.3e}? {} fd_noise={fd_noise:.3e}",
8260                jet.derivative,
8261                jet.curvature,
8262                enclosure.derivative.lo,
8263                enclosure.derivative.hi,
8264                enclosure.curvature.lo,
8265                enclosure.curvature.hi,
8266                score_width / mean_value_bound.max(f64::MIN_POSITIVE),
8267                derivative_span / jet.derivative.abs().max(f64::MIN_POSITIVE),
8268                2.0 * enclosure.score.evaluation_error,
8269                score_width <= 2.0 * enclosure.score.evaluation_error,
8270            );
8271            // SOUNDNESS, against a proof object rather than against a rounded
8272            // scalar. `evaluate`'s jet is documented as a proposal — its
8273            // residual is `energy - sum q_i/h_i`, a cancelling difference, so on
8274            // this design it carries about three digits of loss, which is
8275            // exactly why the search never treats it as evidence. The
8276            // certified statement is that the cell's range contains the
8277            // DEGENERATE-cell range at every interior point: both enclose the
8278            // same exact derivative, and inclusion is what
8279            // `certify_endpoint_derivative` relies on.
8280            let point = affine.enclose(center, center).expect("point enclosure");
8281            assert!(
8282                enclosure.derivative.lo <= point.derivative.lo
8283                    && point.derivative.hi <= enclosure.derivative.hi,
8284                "UNSOUND derivative enclosure on [{a}, {b}]: the midpoint range [{}, {}] is not \
8285                 inside the cell range [{}, {}]",
8286                point.derivative.lo,
8287                point.derivative.hi,
8288                enclosure.derivative.lo,
8289                enclosure.derivative.hi
8290            );
8291            assert!(
8292                enclosure.score.value.lo <= point.score.value.lo
8293                    && point.score.value.hi <= enclosure.score.value.hi,
8294                "UNSOUND value enclosure on [{a}, {b}]: the midpoint range [{}, {}] is not \
8295                 inside the cell range [{}, {}]",
8296                point.score.value.lo,
8297                point.score.value.hi,
8298                enclosure.score.value.lo,
8299                enclosure.score.value.hi
8300            );
8301            assert!(
8302                enclosure.curvature.lo <= point.curvature.lo
8303                    && point.curvature.hi <= enclosure.curvature.hi,
8304                "UNSOUND curvature enclosure on [{a}, {b}]: the midpoint range [{}, {}] is not \
8305                 inside the cell range [{}, {}]",
8306                point.curvature.lo,
8307                point.curvature.hi,
8308                enclosure.curvature.lo,
8309                enclosure.curvature.hi
8310            );
8311            // The rounded scalar jet is NOT asserted against the curvature, for
8312            // the same reason it is not asserted against the derivative, and the
8313            // margin is now large enough to be worth naming: at w=2e-6 the
8314            // certified curvature range is [1.2498332e-5, 1.2499254e-5] and the
8315            // jet says 1.2492615e-5, outside it by 4.6e-4 relative. That is the
8316            // scalar path's own loss, not the enclosure's -- `evaluate` forms
8317            // the residual as the cancelling `energy - sum q_i/h_i` and then
8318            // divides by it twice, while the interval path intersects that with
8319            // the well-conditioned zero-smoothing complement. Centring has made
8320            // the proof object tight enough that the rounded evaluator is the
8321            // less accurate of the two, which is exactly why the search
8322            // documents scalar derivatives as proposals and takes every
8323            // exclusion, isolation and ordering decision on the ranges.
8324        }
8325    }
8326}