Skip to main content

gam_solve/
spline_scan.rs

1//! Exact O(n) state-space polynomial smoothing spline ("the scan").
2//!
3//! The order-`m` intrinsic Gaussian prior whose penalized posterior mean is the
4//! degree-`(2m−1)` smoothing spline (penalty `λ∫(f^{(m)})²`) is a Markov process
5//! in the state `α(x) = (f, f′, …, f^{(m−1)})`: an `m`-fold integrated Wiener
6//! process. The Kalman filter + RTS smoother over the x-sorted observations
7//! therefore computes the EXACT smoothing-spline posterior — mean, derivatives,
8//! pointwise variance — and the diffuse innovations decomposition computes the
9//! EXACT restricted (REML) likelihood, all in O(n) work per smoothing-parameter
10//! trial instead of the dense O(n·k²) design/Gram + O(k³) solve per trial
11//! (Wahba 1978; Kohn & Ansley 1987; Durbin & Koopman exact diffuse init).
12//!
13//! Supported orders are `m ∈ {1, 2, 3}` (`MAX_ORDER`): `m = 1` is the
14//! random-walk / linear smoother (penalty `λ∫f′²`), `m = 2` the cubic smoother
15//! (`λ∫f″²`), `m = 3` the quintic smoother (`λ∫(f‴)²`, natural spline degree
16//! `2m−1 = 5`). The diffuse prior carries `m` improper dimensions consumed by
17//! the first `m` distinct abscissae, leaving `m − 1` *partially-diffuse leading
18//! nodes* whose smoothed moments the ordinary RTS recursion cannot reach (its
19//! predicted covariance is rank-deficient there). For `m = 2` that is the
20//! single node 0; for `m = 3` the pair {0, 1}. These are recovered exactly by a
21//! joint Gaussian conditioning of the whole leading block on the first proper
22//! smoothed node (see the smoother pass) — the exact diffuse analog of RTS, and
23//! the multi-node generalization of the `m = 2` reverse-Markov closure.
24//!
25//! Model, after sorting and pooling tied abscissae (precision-weighted):
26//!   α_{t+1} = F_t α_t + η_t,   η_t ~ N(0, q·Q(δ_t)),   q = σ_w²/σ² = 1/λ,
27//!   y_t     = H α_t + ε_t,     ε_t ~ N(0, σ²/w_t),     H = [1 0 … 0],
28//!   F(δ) = exp(δA) (nilpotent shift A),   Q(δ) the m-fold IWP noise,
29//! with a diffuse (improper, flat) prior on the first `m` states carrying the
30//! unpenalized degree-`<m` polynomial null space the spline leaves unshrunk.
31//! (`m = 2`: `F = [[1,δ],[0,1]]`, `Q = [[δ³/3,δ²/2],[δ²/2,δ]]`.)
32//!
33//! Exactness boundaries, by construction:
34//! - the diffuse dimension is `m` and is consumed by the first `m` distinct
35//!   abscissae, after which the filter is an ordinary proper Kalman filter;
36//! - the `m − 1` partially-diffuse leading nodes are recovered by exact Markov
37//!   conditioning of the whole leading block on the first proper smoothed node,
38//!   `p(α_{0..m−2} | y) = ∫ p(α_{0..m−2} | α_{m−1}, y_{0..m−2}) p(α_{m−1} | y)`
39//!   — an affine `((m−1)m)×m` Bayes update built from the flat leading prior,
40//!   the Markov increments, and the leading observations; it reduces to the
41//!   single-node reverse-Markov closure at `m = 2` and needs no diffuse RTS
42//!   recursion;
43//! - off-knot prediction is the Gaussian bridge conditional on the two
44//!   flanking smoothed states (using the exact lag-one smoothed
45//!   cross-covariance `G_t · P^s_{t+1}`), or boundary extrapolation from the
46//!   end states, which reproduces the spline's polynomial extrapolation with
47//!   growing variance — bridge-don't-sag is a theorem here.
48//!
49//! The smoothing parameter is selected by isolating every stationary interval
50//! of the concentrated diffuse restricted log-likelihood over log λ. Exact
51//! analytic score sensitivities are propagated through the filter, and global
52//! curvature bounds drive certified adaptive subdivision; both finite-domain
53//! boundaries compete exactly. σ² is profiled in closed form from the proper
54//! innovations plus the within-tie residual sum.
55
56use std::cell::RefCell;
57use std::collections::HashMap;
58
59use gam_math::score_opt::{
60    ClosedInterval, DerivativeEnclosure, ScoreJet, ScoreOptimumLocation, ScoreSample,
61    ScoreSearchResult, ScoreValueEnclosure, maximize_score_1d,
62};
63
64/// One pooled (distinct-abscissa) observation node.
65#[derive(Clone, Copy, Debug)]
66struct PooledNode {
67    x: f64,
68    /// Precision-weighted mean of the tied responses.
69    y: f64,
70    /// Total weight of the pooled ties (observation variance is `σ²/w`).
71    w: f64,
72}
73
74/// Search interval for log λ (natural log), generous on both sides.
75const LOG_LAMBDA_LO: f64 = -18.0;
76const LOG_LAMBDA_HI: f64 = 18.0;
77/// Maximum supported smoothing-spline order handled by the fixed-capacity
78/// small-matrix layer. Order `m` penalizes `∫(f^{(m)})²`; the state dimension
79/// is `m`. The exact diffuse leading-block smoother (see the smoother pass)
80/// recovers the `m − 1` partially-diffuse leading nodes for any `m`: `m = 1`
81/// has none, `m = 2` has node 0, `m = 3` has {0, 1}. Order 3 (the quintic
82/// smoothing spline, #1044) is the current cap; bumping it further only needs a
83/// wider `mat_inv` branch and the (already order-general) leading-block solve.
84const MAX_ORDER: usize = 3;
85
86/// Row-major `m × m` matrix stored in a fixed `MAX_ORDER`-capacity buffer; only
87/// the top-left `m × m` block is meaningful. Generalizing the order-2 cubic
88/// scan to order `m ∈ {1, 2, 3}` (#1034 item 2, #1044) keeps the
89/// allocation-free fixed storage of the hot filter loop while letting `m` vary
90/// at runtime.
91type Mat2 = [[f64; MAX_ORDER]; MAX_ORDER];
92type Vec2 = [f64; MAX_ORDER];
93
94/// A nearest-rounded representative carried beside an outward interval for the
95/// exact-real result of the same arithmetic expression.
96///
97/// Every elementary operation rounds both interval endpoints away from the
98/// result. `exp` and `ln` use `gam-math`'s range-reduced, directed Taylor
99/// enclosures; all other operations are IEEE basic operations. No platform
100/// libm accuracy contract is assumed. This is interval arithmetic, not a
101/// tolerance: widening is entirely source-derived from the operations the
102/// filter actually performs.
103#[derive(Clone, Copy, Debug, PartialEq)]
104struct Ball {
105    value: f64,
106    lo: f64,
107    hi: f64,
108}
109
110impl Ball {
111    const ZERO: Self = Self {
112        value: 0.0,
113        lo: 0.0,
114        hi: 0.0,
115    };
116    const ONE: Self = Self {
117        value: 1.0,
118        lo: 1.0,
119        hi: 1.0,
120    };
121
122    #[inline]
123    fn exact(value: f64) -> Self {
124        Self {
125            value,
126            lo: value,
127            hi: value,
128        }
129    }
130
131    /// Attach an independently certified exact-real enclosure to a rounded
132    /// representative.
133    #[inline]
134    fn certified(value: f64, enclosure: ClosedInterval) -> Self {
135        Self {
136            value,
137            lo: enclosure.lo,
138            hi: enclosure.hi,
139        }
140    }
141
142    #[inline]
143    fn add(self, other: Self) -> Self {
144        let enclosure = self.interval().add(other.interval());
145        Self {
146            value: self.value + other.value,
147            lo: enclosure.lo,
148            hi: enclosure.hi,
149        }
150    }
151
152    #[inline]
153    fn neg(self) -> Self {
154        Self {
155            value: -self.value,
156            lo: -self.hi,
157            hi: -self.lo,
158        }
159    }
160
161    #[inline]
162    fn sub(self, other: Self) -> Self {
163        self.add(other.neg())
164    }
165
166    #[inline]
167    fn mul(self, other: Self) -> Self {
168        let enclosure = self.interval().mul(other.interval());
169        Self {
170            value: self.value * other.value,
171            lo: enclosure.lo,
172            hi: enclosure.hi,
173        }
174    }
175
176    #[inline]
177    fn scale(self, factor: f64) -> Self {
178        self.mul(Self::exact(factor))
179    }
180
181    /// Division after the caller has proved the denominator interval positive.
182    #[inline]
183    fn div_positive(self, denominator: Self) -> Self {
184        assert!(
185            denominator.is_finite() && denominator.lo > 0.0,
186            "Ball::div_positive requires a finite, strictly positive denominator interval, got \
187             value={} lo={} hi={}",
188            denominator.value,
189            denominator.lo,
190            denominator.hi
191        );
192        let reciprocal = Self {
193            value: 1.0 / denominator.value,
194            lo: if denominator.hi == 1.0 {
195                1.0
196            } else {
197                next_down_ball(1.0 / denominator.hi)
198            },
199            hi: if denominator.lo == 1.0 {
200                1.0
201            } else {
202                next_up_ball(1.0 / denominator.lo)
203            },
204        };
205        self.mul(reciprocal)
206    }
207
208    #[inline]
209    fn ln_positive(self) -> Self {
210        assert!(
211            self.is_finite() && self.lo > 0.0,
212            "Ball::ln_positive requires a finite, strictly positive interval, got value={} lo={} \
213             hi={}",
214            self.value,
215            self.lo,
216            self.hi
217        );
218        let lo = gam_math::score_opt::certified_ln_positive(self.lo)
219            .expect("positive finite interval lower endpoint");
220        let hi = gam_math::score_opt::certified_ln_positive(self.hi)
221            .expect("positive finite interval upper endpoint");
222        Self {
223            value: self.value.ln(),
224            lo: lo.lo,
225            hi: hi.hi,
226        }
227    }
228
229    #[inline]
230    fn square(self) -> Self {
231        let hi_abs = self.lo.abs().max(self.hi.abs());
232        let lo_abs = if self.lo <= 0.0 && self.hi >= 0.0 {
233            0.0
234        } else {
235            self.lo.abs().min(self.hi.abs())
236        };
237        Self {
238            value: self.value * self.value,
239            lo: if lo_abs == 0.0 {
240                0.0
241            } else if lo_abs == 1.0 {
242                1.0
243            } else {
244                next_down_ball(lo_abs * lo_abs)
245            },
246            hi: if hi_abs == 0.0 || hi_abs == 1.0 {
247                hi_abs
248            } else {
249                next_up_ball(hi_abs * hi_abs)
250            },
251        }
252    }
253
254    #[inline]
255    fn is_finite(self) -> bool {
256        self.value.is_finite() && self.lo.is_finite() && self.hi.is_finite() && self.lo <= self.hi
257    }
258
259    #[inline]
260    fn interval(self) -> ClosedInterval {
261        ClosedInterval::new(self.lo, self.hi)
262    }
263
264    #[inline]
265    fn forward_error(self) -> f64 {
266        // One outward successor covers the subtraction roundoff even when the
267        // exact distance is subnormal and the rounded subtraction is zero.
268        // Every Ball primitive already preserves exact structural zero or
269        // moves each inexact endpoint by one representable value.
270        next_up_ball(
271            (self.value - self.lo)
272                .abs()
273                .max((self.hi - self.value).abs()),
274        )
275    }
276}
277
278type BallMat = [[Ball; MAX_ORDER]; MAX_ORDER];
279type BallVec = [Ball; MAX_ORDER];
280
281#[derive(Clone, Copy, Debug, PartialEq, Eq)]
282pub enum SplineInnovationKind {
283    Diffuse,
284    Proper,
285}
286
287/// Failure to construct a numerical proof for one spline-score evaluation.
288#[derive(Clone, Debug, PartialEq)]
289pub enum SplineScoreProofError {
290    /// Directed roundoff made an innovation interval include zero, so division
291    /// by that innovation cannot be certified.
292    InnovationContainsZero {
293        node: usize,
294        kind: SplineInnovationKind,
295        enclosure: ClosedInterval,
296    },
297    /// An innovation interval was entirely nonpositive. This indicates a
298    /// violated covariance invariant rather than loss of numerical resolution.
299    NonPositiveInnovation {
300        node: usize,
301        kind: SplineInnovationKind,
302        enclosure: ClosedInterval,
303    },
304    NonPositiveProfileResidual {
305        enclosure: ClosedInterval,
306    },
307    InvalidArithmetic {
308        context: &'static str,
309    },
310    /// A certified filter accumulator left the finite range, reported with the
311    /// node it happened at and the enclosure that did it.
312    ///
313    /// The `InvalidArithmetic{"diffuse filter accumulator"}` refusal above named
314    /// a PHASE and nothing else: not which of the eight accumulators diverged,
315    /// not at which of the ~180 nodes, and not how wide it was when it went.
316    /// Diagnosing #2614 from it required adding a print, and the two repairs
317    /// attempted before that print existed were both aimed at the wrong term —
318    /// each exact, each landing a byte-identical failure. A verdict has to
319    /// carry the quantity it was decided against (#2465), and this is that
320    /// quantity: the first accumulator to leave the finite range, where, and
321    /// the `q = e^{−ρ}` it was evaluated at.
322    AccumulatorDiverged {
323        node: usize,
324        n_proper: usize,
325        accumulator: &'static str,
326        value: f64,
327        lo: f64,
328        hi: f64,
329        q_value: f64,
330        /// This node's own contribution to that accumulator. A finite running
331        /// sum plus an infinite total means the CONTRIBUTION diverged, so this
332        /// is the term to look at, not the sum.
333        contribution_lo: f64,
334        contribution_hi: f64,
335        /// The third covariance-derivative entry `F'''` every third-order chain
336        /// rule on this path divides by `F`. Reported alongside so a wide
337        /// contribution can be told from a wide INPUT: if `F'''` is already
338        /// unbounded the covariance jet is at fault, and if it is tight while
339        /// the contribution is not, the cancellation in the chain rule is.
340        f_star_d3_lo: f64,
341        f_star_d3_hi: f64,
342        /// The same entry AFTER this node's measurement update. `F'''` above is
343        /// the PREDICTED value, so the pair localises the growth to one of the
344        /// filter's two steps: an updated entry much narrower than the
345        /// predicted one means the update contracts and the PREDICTION is
346        /// growing it, and the reverse means the update is.
347        updated_d3_lo: f64,
348        updated_d3_hi: f64,
349    },
350    InvalidInput(String),
351    MissingEndpointCertificate {
352        log_lambda: f64,
353    },
354    GlobalValueOrderingUnresolved {
355        maximum_excess: f64,
356        comparison_resolution: f64,
357    },
358    OptimumKktUncertified {
359        location: ScoreOptimumLocation,
360        bracket: ClosedInterval,
361        derivative: ClosedInterval,
362        curvature: ClosedInterval,
363    },
364    Search(String),
365    Computation(String),
366}
367
368impl std::fmt::Display for SplineScoreProofError {
369    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
370        match self {
371            Self::InnovationContainsZero {
372                node,
373                kind,
374                enclosure,
375            } => write!(
376                f,
377                "spline scan: {kind:?} innovation ball at node {node} contains zero: {enclosure:?}"
378            ),
379            Self::NonPositiveInnovation {
380                node,
381                kind,
382                enclosure,
383            } => write!(
384                f,
385                "spline scan: {kind:?} innovation ball at node {node} is nonpositive: {enclosure:?}"
386            ),
387            Self::NonPositiveProfileResidual { enclosure } => write!(
388                f,
389                "spline scan: profiled residual ball is not strictly positive: {enclosure:?}"
390            ),
391            Self::InvalidArithmetic { context } => {
392                write!(
393                    f,
394                    "spline scan: non-finite interval arithmetic in {context}"
395                )
396            }
397            Self::AccumulatorDiverged {
398                node,
399                n_proper,
400                accumulator,
401                value,
402                lo,
403                hi,
404                q_value,
405                contribution_lo,
406                contribution_hi,
407                f_star_d3_lo,
408                f_star_d3_hi,
409                updated_d3_lo,
410                updated_d3_hi,
411            } => {
412                write!(
413                    f,
414                    "spline scan: certified accumulator `{accumulator}` left the finite range at \
415                     node {node} (proper innovations so far {n_proper}, q = {q_value:.6e}): \
416                     value {value:.9e} in [{lo:.9e}, {hi:.9e}]; this node's contribution was \
417                     [{contribution_lo:.9e}, {contribution_hi:.9e}], predicted F''' was \
418                     [{f_star_d3_lo:.9e}, {f_star_d3_hi:.9e}] and updated F''' was \
419                     [{updated_d3_lo:.9e}, {updated_d3_hi:.9e}]"
420                )
421            }
422            Self::InvalidInput(reason) => f.write_str(reason),
423            Self::MissingEndpointCertificate { log_lambda } => write!(
424                f,
425                "spline scan: certified search requested an uncached endpoint {log_lambda}"
426            ),
427            Self::GlobalValueOrderingUnresolved {
428                maximum_excess,
429                comparison_resolution,
430            } => write!(
431                f,
432                "spline scan: the selected REML representative can trail another exact \
433                 candidate by {maximum_excess}, beyond the certified comparison resolution \
434                 {comparison_resolution}"
435            ),
436            Self::OptimumKktUncertified {
437                location,
438                bracket,
439                derivative,
440                curvature,
441            } => write!(
442                f,
443                "spline scan: exact-real REML KKT condition is uncertified for {location:?} \
444                 on {bracket:?} (derivative {derivative:?}, curvature {curvature:?})"
445            ),
446            Self::Search(reason) => {
447                write!(f, "spline scan: REML stationary isolation failed: {reason}")
448            }
449            Self::Computation(reason) => f.write_str(reason),
450        }
451    }
452}
453
454impl std::error::Error for SplineScoreProofError {}
455
456impl From<String> for SplineScoreProofError {
457    fn from(reason: String) -> Self {
458        Self::Computation(reason)
459    }
460}
461
462#[inline]
463fn require_positive_innovation(
464    node: usize,
465    kind: SplineInnovationKind,
466    innovation: Ball,
467) -> Result<(), SplineScoreProofError> {
468    if !innovation.is_finite() {
469        return Err(SplineScoreProofError::InvalidArithmetic {
470            context: "innovation recurrence",
471        });
472    }
473    let enclosure = innovation.interval();
474    if innovation.lo <= 0.0 && innovation.hi >= 0.0 {
475        Err(SplineScoreProofError::InnovationContainsZero {
476            node,
477            kind,
478            enclosure,
479        })
480    } else if innovation.hi < 0.0 {
481        Err(SplineScoreProofError::NonPositiveInnovation {
482            node,
483            kind,
484            enclosure,
485        })
486    } else {
487        Ok(())
488    }
489}
490
491#[inline]
492fn next_down_ball(value: f64) -> f64 {
493    if value.is_nan() || value == f64::NEG_INFINITY {
494        return value;
495    }
496    if value == 0.0 {
497        return -f64::from_bits(1);
498    }
499    let bits = value.to_bits();
500    f64::from_bits(if value > 0.0 { bits - 1 } else { bits + 1 })
501}
502
503#[inline]
504fn next_up_ball(value: f64) -> f64 {
505    if value.is_nan() || value == f64::INFINITY {
506        return value;
507    }
508    if value == 0.0 {
509        return f64::from_bits(1);
510    }
511    let bits = value.to_bits();
512    f64::from_bits(if value > 0.0 { bits + 1 } else { bits - 1 })
513}
514
515#[inline]
516fn mat_mul(a: &Mat2, b: &Mat2, m: usize) -> Mat2 {
517    let mut c = [[0.0; MAX_ORDER]; MAX_ORDER];
518    for i in 0..m {
519        for j in 0..m {
520            let mut acc = 0.0;
521            for k in 0..m {
522                acc += a[i][k] * b[k][j];
523            }
524            c[i][j] = acc;
525        }
526    }
527    c
528}
529
530#[inline]
531fn mat_t(a: &Mat2, m: usize) -> Mat2 {
532    let mut c = [[0.0; MAX_ORDER]; MAX_ORDER];
533    for i in 0..m {
534        for j in 0..m {
535            c[i][j] = a[j][i];
536        }
537    }
538    c
539}
540
541#[inline]
542fn mat_vec(a: &Mat2, v: &Vec2, m: usize) -> Vec2 {
543    let mut out = [0.0; MAX_ORDER];
544    for i in 0..m {
545        let mut acc = 0.0;
546        for j in 0..m {
547            acc += a[i][j] * v[j];
548        }
549        out[i] = acc;
550    }
551    out
552}
553
554#[inline]
555fn mat_add(a: &Mat2, b: &Mat2, m: usize) -> Mat2 {
556    let mut c = [[0.0; MAX_ORDER]; MAX_ORDER];
557    for i in 0..m {
558        for j in 0..m {
559            c[i][j] = a[i][j] + b[i][j];
560        }
561    }
562    c
563}
564
565#[inline]
566fn mat_sub(a: &Mat2, b: &Mat2, m: usize) -> Mat2 {
567    let mut c = [[0.0; MAX_ORDER]; MAX_ORDER];
568    for i in 0..m {
569        for j in 0..m {
570            c[i][j] = a[i][j] - b[i][j];
571        }
572    }
573    c
574}
575
576/// Inverse of an `m × m` (`m ∈ {1, 2, 3}`) with a hard singularity error.
577/// Closed-form cofactor inverses keep the hot-loop arithmetic exact and
578/// branch-free; order 3 is the quintic smoother's state dimension (#1044).
579fn mat_inv(a: &Mat2, m: usize, what: &str) -> Result<Mat2, String> {
580    let mut out = [[0.0; MAX_ORDER]; MAX_ORDER];
581    match m {
582        1 => {
583            let d = a[0][0];
584            if !(d.is_finite() && d.abs() > 0.0) {
585                return Err(format!("spline scan: singular 1x1 in {what} (a00={d})"));
586            }
587            out[0][0] = 1.0 / d;
588        }
589        2 => {
590            let det = a[0][0] * a[1][1] - a[0][1] * a[1][0];
591            if !(det.is_finite() && det.abs() > 0.0) {
592                return Err(format!("spline scan: singular 2x2 in {what} (det={det})"));
593            }
594            out[0][0] = a[1][1] / det;
595            out[0][1] = -a[0][1] / det;
596            out[1][0] = -a[1][0] / det;
597            out[1][1] = a[0][0] / det;
598        }
599        3 => {
600            // Cofactor / adjugate inverse. Cofactors of the 2×2 minors:
601            let c00 = a[1][1] * a[2][2] - a[1][2] * a[2][1];
602            let c01 = a[1][2] * a[2][0] - a[1][0] * a[2][2];
603            let c02 = a[1][0] * a[2][1] - a[1][1] * a[2][0];
604            let det = a[0][0] * c00 + a[0][1] * c01 + a[0][2] * c02;
605            if !(det.is_finite() && det.abs() > 0.0) {
606                return Err(format!("spline scan: singular 3x3 in {what} (det={det})"));
607            }
608            let inv_det = 1.0 / det;
609            // inv = adj/det = (cofactor matrix)ᵀ / det.
610            out[0][0] = c00 * inv_det;
611            out[0][1] = (a[0][2] * a[2][1] - a[0][1] * a[2][2]) * inv_det;
612            out[0][2] = (a[0][1] * a[1][2] - a[0][2] * a[1][1]) * inv_det;
613            out[1][0] = c01 * inv_det;
614            out[1][1] = (a[0][0] * a[2][2] - a[0][2] * a[2][0]) * inv_det;
615            out[1][2] = (a[0][2] * a[1][0] - a[0][0] * a[1][2]) * inv_det;
616            out[2][0] = c02 * inv_det;
617            out[2][1] = (a[0][1] * a[2][0] - a[0][0] * a[2][1]) * inv_det;
618            out[2][2] = (a[0][0] * a[1][1] - a[0][1] * a[1][0]) * inv_det;
619        }
620        _ => return Err(format!("spline scan: unsupported order {m} in {what}")),
621    }
622    Ok(out)
623}
624
625/// Inverse of a general dense `d × d` SPD matrix via Gauss–Jordan elimination
626/// with partial pivoting, symmetric diagonal (Jacobi) equilibration, and one
627/// iterative-refinement step. Used once per fit by the leading-block diffuse
628/// smoother (dimension `(order−1)·order ≤ 6`), so clarity over speed — it is
629/// NOT on the hot REML grid path (that runs only `run_filter`).
630///
631/// Equilibration matters at order `m ≥ 3`: the IWP process noise `Q(δ)` scales
632/// the `f^{(k)}` state components by `δ^{2m−1}` down to `δ`, so its inverse
633/// `(qQ)⁻¹` — and hence the leading-block precision `Λ` — spans many orders of
634/// magnitude (the f-component carries the `O(w)` observation term, the
635/// high-derivative components carry `O(1/(qδ^{2m−1}))` penalty mass). A bare
636/// Gauss–Jordan inverse of such a `Λ` loses `≈ ε·κ(Λ)` digits, which at heavy
637/// smoothing (small `q`) would corrupt the quintic's leading smoothed nodes.
638/// Rescaling to unit diagonal (`Λ̃ = SΛS`, `s_i = 1/√Λ_ii`) collapses that
639/// scale disparity before the elimination, then `Λ⁻¹ = S Λ̃⁻¹ S`.
640fn dense_spd_inverse(a: &[Vec<f64>], what: &str) -> Result<Vec<Vec<f64>>, String> {
641    let d = a.len();
642    // Jacobi equilibration scale s_i = 1/√Λ_ii (Λ SPD ⇒ Λ_ii > 0).
643    let s: Vec<f64> = (0..d)
644        .map(|i| {
645            let dii = a[i][i];
646            if dii.is_finite() && dii > 0.0 {
647                1.0 / dii.sqrt()
648            } else {
649                1.0
650            }
651        })
652        .collect();
653    let a_s: Vec<Vec<f64>> = (0..d)
654        .map(|i| (0..d).map(|j| s[i] * a[i][j] * s[j]).collect())
655        .collect();
656    // Gauss–Jordan inverse of the equilibrated matrix.
657    let mut inv_s = gauss_jordan_inverse(&a_s, what)?;
658    // One iterative-refinement step against the equilibrated system:
659    // X ← X + X·(I − Λ̃·X), reducing the residual to near machine precision.
660    let mut resid = vec![vec![0.0_f64; d]; d]; // R = I − Λ̃·X
661    for i in 0..d {
662        for j in 0..d {
663            let mut ax = 0.0;
664            for k in 0..d {
665                ax += a_s[i][k] * inv_s[k][j];
666            }
667            resid[i][j] = f64::from(u8::from(i == j)) - ax;
668        }
669    }
670    let mut delta = vec![vec![0.0_f64; d]; d]; // ΔX = X·R
671    for i in 0..d {
672        for j in 0..d {
673            let mut acc = 0.0;
674            for k in 0..d {
675                acc += inv_s[i][k] * resid[k][j];
676            }
677            delta[i][j] = acc;
678        }
679    }
680    for i in 0..d {
681        for j in 0..d {
682            inv_s[i][j] += delta[i][j];
683        }
684    }
685    // Un-equilibrate: Λ⁻¹ = S·Λ̃⁻¹·S.
686    Ok((0..d)
687        .map(|i| (0..d).map(|j| s[i] * inv_s[i][j] * s[j]).collect())
688        .collect())
689}
690
691/// Gauss–Jordan inverse with partial pivoting (helper for `dense_spd_inverse`).
692fn gauss_jordan_inverse(a: &[Vec<f64>], what: &str) -> Result<Vec<Vec<f64>>, String> {
693    let d = a.len();
694    let mut aug = a.to_vec();
695    let mut inv = vec![vec![0.0_f64; d]; d];
696    for i in 0..d {
697        inv[i][i] = 1.0;
698    }
699    for col in 0..d {
700        let piv = (col..d)
701            .max_by(|&i, &j| aug[i][col].abs().total_cmp(&aug[j][col].abs()))
702            .ok_or_else(|| {
703                format!("spline scan: no pivot candidate in column {col} of {d} in {what}")
704            })?;
705        let p = aug[piv][col];
706        if !(p.is_finite() && p.abs() > 0.0) {
707            return Err(format!(
708                "spline scan: singular {d}x{d} in {what} (pivot={p})"
709            ));
710        }
711        aug.swap(col, piv);
712        inv.swap(col, piv);
713        let d_piv = aug[col][col];
714        for k in 0..d {
715            aug[col][k] /= d_piv;
716            inv[col][k] /= d_piv;
717        }
718        for r in 0..d {
719            if r == col {
720                continue;
721            }
722            let f = aug[r][col];
723            if f == 0.0 {
724                continue;
725            }
726            for k in 0..d {
727                aug[r][k] -= f * aug[col][k];
728                inv[r][k] -= f * inv[col][k];
729            }
730        }
731    }
732    Ok(inv)
733}
734
735/// Factorials `k!` for `k ≤ 2·MAX_ORDER` — the only ones the order-`m`
736/// transition and process-noise formulas reference.
737#[inline]
738fn factorial(k: usize) -> f64 {
739    (1..=k).map(|v| v as f64).product::<f64>().max(1.0)
740}
741
742/// Transition `F(δ) = exp(δ·A)` of the `m`-th order integrated Wiener process,
743/// `A` the nilpotent shift: `F[i][j] = δ^{j−i}/(j−i)!` for `j ≥ i`, else 0.
744/// `m = 1 ⇒ [[1]]`; `m = 2 ⇒ [[1, δ], [0, 1]]` (the cubic case, unchanged).
745#[inline]
746fn transition(delta: f64, m: usize) -> Mat2 {
747    let mut f = [[0.0; MAX_ORDER]; MAX_ORDER];
748    for i in 0..m {
749        for j in i..m {
750            f[i][j] = delta.powi((j - i) as i32) / factorial(j - i);
751        }
752    }
753    f
754}
755
756/// Process noise `Q(δ) = ∫₀^δ e^{As} b bᵀ e^{Aᵀs} ds` (`b = e_{m−1}`) of the
757/// `m`-th order IWP at unit `q`, scaled by `q`:
758/// `Q[i][j] = q · δ^{2m−1−i−j} / ((m−1−i)! (m−1−j)! (2m−1−i−j))`.
759/// `m = 1 ⇒ [[q·δ]]`; `m = 2 ⇒ [[q·δ³/3, q·δ²/2], [q·δ²/2, q·δ]]` (unchanged).
760#[inline]
761fn process_noise(delta: f64, q: f64, m: usize) -> Mat2 {
762    let mut out = [[0.0; MAX_ORDER]; MAX_ORDER];
763    for i in 0..m {
764        for j in 0..m {
765            let p = 2 * m - 1 - i - j;
766            out[i][j] = q * delta.powi(p as i32)
767                / (factorial(m - 1 - i) * factorial(m - 1 - j) * (p as f64));
768        }
769    }
770    out
771}
772
773/// Symmetrize in place against drift from the rank-one update arithmetic.
774#[inline]
775fn symmetrize(a: &mut Mat2, m: usize) {
776    for i in 0..m {
777        for j in (i + 1)..m {
778            let off = 0.5 * (a[i][j] + a[j][i]);
779            a[i][j] = off;
780            a[j][i] = off;
781        }
782    }
783}
784
785#[inline]
786fn ball_mat_mul(a: &BallMat, b: &BallMat, m: usize) -> BallMat {
787    let mut c = [[Ball::ZERO; MAX_ORDER]; MAX_ORDER];
788    for i in 0..m {
789        for j in 0..m {
790            let mut acc = Ball::ZERO;
791            for k in 0..m {
792                acc = acc.add(a[i][k].mul(b[k][j]));
793            }
794            c[i][j] = acc;
795        }
796    }
797    c
798}
799
800#[inline]
801fn ball_mat_t(a: &BallMat, m: usize) -> BallMat {
802    let mut c = [[Ball::ZERO; MAX_ORDER]; MAX_ORDER];
803    for i in 0..m {
804        for j in 0..m {
805            c[i][j] = a[j][i];
806        }
807    }
808    c
809}
810
811#[inline]
812fn ball_mat_vec(a: &BallMat, v: &BallVec, m: usize) -> BallVec {
813    let mut out = [Ball::ZERO; MAX_ORDER];
814    for i in 0..m {
815        let mut acc = Ball::ZERO;
816        for j in 0..m {
817            acc = acc.add(a[i][j].mul(v[j]));
818        }
819        out[i] = acc;
820    }
821    out
822}
823
824#[inline]
825fn ball_mat_add(a: &BallMat, b: &BallMat, m: usize) -> BallMat {
826    let mut c = [[Ball::ZERO; MAX_ORDER]; MAX_ORDER];
827    for i in 0..m {
828        for j in 0..m {
829            c[i][j] = a[i][j].add(b[i][j]);
830        }
831    }
832    c
833}
834
835#[inline]
836fn ball_mat_sub(a: &BallMat, b: &BallMat, m: usize) -> BallMat {
837    let mut c = [[Ball::ZERO; MAX_ORDER]; MAX_ORDER];
838    for i in 0..m {
839        for j in 0..m {
840            c[i][j] = a[i][j].sub(b[i][j]);
841        }
842    }
843    c
844}
845
846#[inline]
847fn ball_transition(delta: Ball, m: usize) -> BallMat {
848    let mut f = [[Ball::ZERO; MAX_ORDER]; MAX_ORDER];
849    for i in 0..m {
850        let mut power = Ball::ONE;
851        for j in i..m {
852            if j > i {
853                power = power.mul(delta);
854            }
855            f[i][j] = power.div_positive(Ball::exact(factorial(j - i)));
856        }
857    }
858    f
859}
860
861#[inline]
862fn ball_unit_process_noise(delta: Ball, m: usize) -> BallMat {
863    let mut powers = [Ball::ONE; 2 * MAX_ORDER];
864    for exponent in 1..powers.len() {
865        powers[exponent] = powers[exponent - 1].mul(delta);
866    }
867    let mut out = [[Ball::ZERO; MAX_ORDER]; MAX_ORDER];
868    for i in 0..m {
869        for j in 0..m {
870            let exponent = 2 * m - 1 - i - j;
871            let denominator = factorial(m - 1 - i) * factorial(m - 1 - j) * exponent as f64;
872            out[i][j] = powers[exponent].div_positive(Ball::exact(denominator));
873        }
874    }
875    out
876}
877
878/// Taylor-model decomposition of one process-noise injection.
879///
880/// At a fixed certified score evaluation, `q` is one exact real number in its
881/// ball, shared by every node. Write
882///
883/// ```text
884/// q = q₀ + r_q θ_q,       θ_q ∈ [-1, 1],
885/// Q_ij = c₀,ij + ε_ij,
886/// q Q_ij = q₀ c₀,ij + (r_q c₀,ij) θ_q + q ε_ij.
887/// ```
888///
889/// The first term is the zonotope centre, the second is one distinguished
890/// generator accumulated across the whole recursion, and only `q ε` plus the
891/// floating-point error in forming the first two coefficients is an
892/// independent remainder. Treating the full interval `qQ` as a fresh constant
893/// at every node discards the identity of `θ_q`; after enough Riccati steps its
894/// box hull is wider than the score resolution even though the signed
895/// recursion is contracting.
896struct ProcessNoiseTaylor {
897    /// Ordinary interval enclosure used by the independent componentwise path.
898    enclosure: BallMat,
899    /// Centre plus deterministic-arithmetic/nonlinear remainder, with the
900    /// first-order common-`q` uncertainty removed.
901    constant: [Ball; COVARIANCE_D1_DIM],
902    /// Coefficient added to the one common normalized `q` generator.
903    shared_q: [f64; COVARIANCE_D1_DIM],
904}
905
906#[inline]
907fn ball_process_noise_taylor(delta: Ball, q: Ball, m: usize) -> ProcessNoiseTaylor {
908    let unit = ball_unit_process_noise(delta, m);
909    let q_radius = ball_radius_about_value(q);
910    let mut enclosure = [[Ball::ZERO; MAX_ORDER]; MAX_ORDER];
911    let mut constant = [Ball::ZERO; COVARIANCE_D1_DIM];
912    let mut shared_q = [0.0_f64; COVARIANCE_D1_DIM];
913
914    for i in 0..m {
915        for j in 0..m {
916            let index = i * m + j;
917            let coefficient = unit[i][j];
918            enclosure[i][j] = q.mul(coefficient);
919
920            let coefficient_center = Ball::exact(coefficient.value);
921            let center_product = Ball::exact(q.value).mul(coefficient_center);
922            let shared_product = Ball::exact(q_radius).mul(coefficient_center);
923
924            // `q·(c-c₀)` is the nonlinear/deterministic interval remainder.
925            // The other two terms charge rounding of the stored centre and
926            // shared-generator coefficients. The latter is multiplied by an
927            // unknown `θ_q`, so its signed error is symmetrized.
928            let coefficient_error = coefficient.sub(coefficient_center);
929            let center_error = center_product.sub(Ball::exact(center_product.value));
930            let shared_error = shared_product.sub(Ball::exact(shared_product.value));
931            let shared_error_radius = ball_radius_about_value(shared_error);
932            let shared_error_symmetric = Ball {
933                value: 0.0,
934                lo: -shared_error_radius,
935                hi: shared_error_radius,
936            };
937            let remainder = q
938                .mul(coefficient_error)
939                .add(center_error)
940                .add(shared_error_symmetric);
941
942            constant[index] = Ball::exact(center_product.value).add(remainder);
943            shared_q[index] = shared_product.value;
944        }
945    }
946
947    ProcessNoiseTaylor {
948        enclosure,
949        constant,
950        shared_q,
951    }
952}
953
954#[inline]
955fn ball_symmetrize(a: &mut BallMat, m: usize) {
956    for i in 0..m {
957        for j in (i + 1)..m {
958            let off = a[i][j].add(a[j][i]).scale(0.5);
959            a[i][j] = off;
960            a[j][i] = off;
961        }
962    }
963}
964
965/// `A = I − K e₀ᵀ`, built so its `(0,0)` entry is never a subtraction (#2614).
966///
967/// The expanded per-entry form of the congruence `A X Aᵀ` evaluates
968/// `X[0][0]·(1 − 2K₀ + K₀²)` — that is `X[0][0]·(1 − K₀)²` written as three
969/// terms. In the saturated regime `K₀ = P₀₀/F → 1`, so it is `1 − 1` twice
970/// over: the value collapses and the interval width does not. Measured
971/// consequence: the third covariance-derivative entry reached an enclosure of
972/// `+/-4.1e247` by node 62, growing about `10^4` per node, while `d1` and `d2`
973/// — which carry fewer such terms — stayed clean.
974///
975/// The identity that removes it is exact and involves no subtraction:
976///
977/// ```text
978///   A[0][0] = 1 − K₀ = 1 − P₀₀/F = (F − P₀₀)/F = R/F
979/// ```
980///
981/// since `F = P₀₀ + R` by construction. So that entry is `r·inv_f` directly.
982/// Away from the first column `A` is the identity; below the diagonal in the
983/// first column it is `−K_i`. Neither cancels, so building `A` and multiplying
984/// is ordinary arithmetic on well-conditioned entries.
985#[inline]
986fn ball_update_operator(gain: &BallVec, a_diag_zero: Ball, order: usize) -> BallMat {
987    let mut a = [[Ball::ZERO; MAX_ORDER]; MAX_ORDER];
988    for (i, row) in a.iter_mut().enumerate().take(order) {
989        row[i] = Ball::ONE;
990    }
991    a[0][0] = a_diag_zero;
992    for i in 1..order {
993        a[i][0] = gain[i].neg();
994    }
995    a
996}
997
998/// `A⁽ᵏ⁾ = −K⁽ᵏ⁾ e₀ᵀ`, the ρ-derivative of [`ball_update_operator`].
999///
1000/// The identity part differentiates away and only the first column survives, so
1001/// there is no cancelling entry here at all.
1002#[inline]
1003fn ball_update_operator_derivative(gain_jet: &BallVec, order: usize) -> BallMat {
1004    let mut a = [[Ball::ZERO; MAX_ORDER]; MAX_ORDER];
1005    for i in 0..order {
1006        a[i][0] = gain_jet[i].neg();
1007    }
1008    a
1009}
1010
1011/// `L · X · Rᵀ` for `Ball` matrices.
1012#[inline]
1013fn ball_congruence(l: &BallMat, x: &BallMat, r_side: &BallMat, order: usize) -> BallMat {
1014    ball_mat_mul(
1015        &ball_mat_mul(l, x, order),
1016        &ball_mat_t(r_side, order),
1017        order,
1018    )
1019}
1020
1021/// Intersect the proper innovation variance with its exact lower bound `R_t`.
1022///
1023/// `F̃_t = H P*_t Hᵀ + R_t`, and `P*` is a covariance, so `H P* Hᵀ ≥ 0` exactly
1024/// and therefore `F̃_t ≥ R_t > 0`. The componentwise interval evaluation of `P*`
1025/// forgets that: after enough rank-one updates its diagonal enclosure widens,
1026/// and once `P*[0][0].lo` drops below zero the enclosure of `F̃` reaches down
1027/// toward zero even though the true value cannot. `inv_f = 1/F̃` then has an
1028/// enclosure reaching `+∞` — and EVERY gain, every `log F̃`, and every one of
1029/// the three derivative recursions multiplies by it.
1030///
1031/// That is the observed failure, on three surfaces: `gam-solve`'s own
1032/// `spline_scan` tests refuse with `InvalidArithmetic{"diffuse filter
1033/// accumulator"}` carrying `[-inf, +inf]` derivative enclosures around exact
1034/// centre values; `gam-models`' `spline_scan_payload_round_trips_and_validates`
1035/// dies on its first line; and the Python surface reports `IntegrationError:
1036/// spline scan: non-finite interval arithmetic in proper covariance PSD
1037/// intersection` (#2614, #2616).
1038///
1039/// Restoring this bound is the same move [`intersect_proper_covariance_psd`]
1040/// already makes for the covariance diagonal, and its own comment states the
1041/// principle: "This intersection restores proof information supplied by the
1042/// statistical model; it is not a numerical tolerance." Here the information is
1043/// stronger, because `R_t = 1/w_t` is an exact input rather than a computed
1044/// quantity — the observation variance is data, not arithmetic.
1045///
1046/// The floor is capped at the ball's own `value` and `hi` so the result stays a
1047/// well-formed enclosure (`lo ≤ value ≤ hi`) even if the centre has itself gone
1048/// non-positive; an inconsistent enclosure is left inconsistent for the finite
1049/// checks downstream to reject, rather than being papered into consistency here.
1050#[inline]
1051fn intersect_innovation_above_observation_variance(
1052    innovation: &mut Ball,
1053    observation_variance: Ball,
1054) {
1055    let floor = observation_variance
1056        .lo
1057        .min(innovation.value)
1058        .min(innovation.hi);
1059    if floor.is_finite() && innovation.lo < floor {
1060        innovation.lo = floor;
1061    }
1062}
1063
1064/// Directed square root of a nonnegative enclosure.
1065#[inline]
1066fn ball_sqrt(value: Ball) -> Option<Ball> {
1067    if !(value.lo >= 0.0 && value.hi.is_finite() && value.lo <= value.hi) {
1068        return None;
1069    }
1070    Some(Ball {
1071        value: value.value.max(0.0).sqrt(),
1072        lo: next_down_ball(value.lo.sqrt()).max(0.0),
1073        hi: next_up_ball(value.hi.sqrt()),
1074    })
1075}
1076
1077/// Lower-triangular Cholesky factor `L` with `P = L Lᵀ`, in directed arithmetic.
1078///
1079/// `None` when a pivot enclosure fails to be strictly positive, which is a
1080/// statement about the enclosure and not about the matrix — the caller treats a
1081/// missing factor as an absence of evidence, never as a refusal.
1082fn ball_cholesky(covariance: &BallMat, order: usize) -> Option<BallMat> {
1083    let mut factor = [[Ball::ZERO; MAX_ORDER]; MAX_ORDER];
1084    for i in 0..order {
1085        for j in 0..=i {
1086            let mut accumulator = covariance[i][j];
1087            for k in 0..j {
1088                accumulator = accumulator.sub(factor[i][k].mul(factor[j][k]));
1089            }
1090            if i == j {
1091                factor[i][j] = ball_sqrt(accumulator)?;
1092                if !(factor[i][j].lo > 0.0) {
1093                    return None;
1094                }
1095            } else {
1096                if !(factor[j][j].lo > 0.0) {
1097                    return None;
1098                }
1099                factor[i][j] = accumulator.div_positive(factor[j][j]);
1100            }
1101        }
1102    }
1103    Some(factor)
1104}
1105
1106/// How many accumulators the per-node divergence check scans.
1107///
1108/// The second- and third-order accumulators are ordered last and left OUT of
1109/// the scan: each has a closed-form global bound the certificate substitutes
1110/// (see [`BoundSource`]), so neither can justify discarding a value and slope
1111/// that are finite. Divergence in the VALUE or in the FIRST derivative still
1112/// refuses, at the node it happened — those have no substitute.
1113const GLOBALLY_BOUNDED_FROM: usize = 4;
1114
1115/// Number of columns in the prediction prearray `[F·L, L_Q]`.
1116const PREARRAY_COLUMNS: usize = 2 * MAX_ORDER;
1117
1118/// The measurement update, performed on a CARRIED FACTOR of the covariance
1119/// instead of on the covariance.
1120///
1121/// With `P⁻ = L Lᵀ` and `L` lower triangular, `Lᵀe₀` has a single nonzero, so
1122/// the whole Kalman update is one column scaling:
1123///
1124/// ```text
1125/// L⁺ = L · diag(β, 1, …, 1),      β = √(R/F),
1126/// ```
1127///
1128/// and `P⁺ = L⁺L⁺ᵀ` is EXACTLY `P⁻ − M Mᵀ/F`. The check: `P[i][0] = L[i][0]L₀₀`
1129/// because `L[0][k] = 0` for `k ≥ 1`, so
1130///
1131/// ```text
1132/// (L⁺L⁺ᵀ)[i][j] = P[i][j] − (1 − β²)L[i][0]L[j][0]
1133///               = P[i][j] − (P₀₀/F)·M[i]M[j]/P₀₀
1134///               = P[i][j] − M[i]M[j]/F.
1135/// ```
1136///
1137/// CARRIED is the load-bearing word. Recomputing the factorization per node
1138/// from the componentwise covariance was measured and is INERT — bit-identical
1139/// divergence nodes `44/44/44/45/50/88/164` at order 2 — because the Cholesky's
1140/// own Schur complement `L₁₁² = P₁₁ − P₀₁²/P₀₀` IS the cancelling subtraction it
1141/// was meant to avoid, so factoring and immediately reconstructing recomputes
1142/// exactly the quantity whose width is the problem. Carried across nodes, that
1143/// difference is never re-formed: `L₁₁` is scaled and rotated, never subtracted,
1144/// so its enclosure tracks the size of the RESULT instead of the size of the
1145/// operands it would have been differenced out of.
1146fn ball_factor_update(factor: &BallMat, beta: Ball, order: usize) -> BallMat {
1147    let mut updated = *factor;
1148    for row in updated.iter_mut().take(order) {
1149        row[0] = row[0].mul(beta);
1150    }
1151    updated
1152}
1153
1154/// `L Lᵀ` for a lower-triangular factor.
1155fn ball_factor_gram(factor: &BallMat, order: usize) -> BallMat {
1156    let mut gram = [[Ball::ZERO; MAX_ORDER]; MAX_ORDER];
1157    for i in 0..order {
1158        for j in 0..order {
1159            let mut accumulator = Ball::ZERO;
1160            for k in 0..=i.min(j) {
1161                accumulator = accumulator.add(factor[i][k].mul(factor[j][k]));
1162            }
1163            gram[i][j] = accumulator;
1164        }
1165    }
1166    gram
1167}
1168
1169/// Re-triangularize the prediction prearray `A = [F·L⁺, L_Q]` so that the
1170/// carried factor stays `order`-wide, WITHOUT assuming exact orthogonality.
1171///
1172/// Givens rotations are applied to pairs of COLUMNS with `c` and `s` taken as
1173/// floating-point points, never intervals. That is what makes the result sound:
1174/// a 2×2 `Θ = [[c, −s], [s, c]]` built from points satisfies `ΘΘᵀ = (c²+s²)I`
1175/// EXACTLY, so applying it to every row rescales the Gram by the scalar
1176/// `c²+s²` and introduces no other error — a computed rotation that is not
1177/// quite orthogonal is a similarity scaling, not a general perturbation. The
1178/// product of those scalars is returned so the caller can divide it back out.
1179/// Had `c` and `s` been intervals, the enclosure would have ranged over
1180/// non-orthogonal `Θ`s and `L Lᵀ` would no longer have enclosed `A Aᵀ`.
1181///
1182/// The trailing columns are not exactly zeroed either, so their outer product
1183/// `D = Σ_{k ≥ order} A[:,k]A[:,k]ᵀ` is returned as `trace(D)`, which bounds
1184/// every entry of `D` because `|D[i][j]| ≤ √(D_ii·D_jj) ≤ trace(D)`.
1185fn ball_retriangularize(
1186    prearray: &mut [[Ball; PREARRAY_COLUMNS]; MAX_ORDER],
1187    order: usize,
1188    columns: usize,
1189) -> (BallMat, f64, f64) {
1190    let mut gram_scale = 1.0_f64;
1191    for i in 0..order {
1192        for k in (i + 1)..columns {
1193            let a = prearray[i][i].value;
1194            let b = prearray[i][k].value;
1195            let radius = (a * a + b * b).sqrt();
1196            if !(radius.is_finite() && radius > 0.0) {
1197                continue;
1198            }
1199            let cosine = a / radius;
1200            let sine = b / radius;
1201            gram_scale = next_up_ball(gram_scale * (cosine * cosine + sine * sine));
1202            for row in prearray.iter_mut().take(order) {
1203                let x = row[i];
1204                let y = row[k];
1205                row[i] = x.scale(cosine).add(y.scale(sine));
1206                row[k] = y.scale(cosine).sub(x.scale(sine));
1207            }
1208        }
1209    }
1210    let mut factor = [[Ball::ZERO; MAX_ORDER]; MAX_ORDER];
1211    for i in 0..order {
1212        for j in 0..=i {
1213            factor[i][j] = prearray[i][j];
1214        }
1215    }
1216    let mut trailing = 0.0_f64;
1217    for row in prearray.iter().take(order) {
1218        for entry in row.iter().take(columns).skip(order) {
1219            let magnitude = entry.lo.abs().max(entry.hi.abs());
1220            trailing = next_up_ball(trailing + next_up_ball(magnitude * magnitude));
1221        }
1222    }
1223    (factor, trailing, gram_scale)
1224}
1225
1226/// Intersect an enclosure with an independently derived enclosure of the same/// Intersect an enclosure with an independently derived enclosure of the same
1227/// quantity.
1228///
1229/// Both bound the same real number, so their intersection does too, and it is
1230/// no wider than either. A bound is never moved past the nearest-rounded
1231/// `value`, which keeps `lo <= value <= hi` an invariant of the type rather
1232/// than something each caller has to re-establish.
1233#[inline]
1234fn intersect_with_independent_enclosure(entry: &mut Ball, evidence: Ball) {
1235    let floor = evidence.lo.min(entry.value);
1236    if floor.is_finite() && entry.lo < floor {
1237        entry.lo = floor;
1238    }
1239    let ceiling = evidence.hi.max(entry.value);
1240    if ceiling.is_finite() && entry.hi > ceiling {
1241        entry.hi = ceiling;
1242    }
1243}
1244
1245/// Intersect the FIRST-derivative covariance with the two-sided range the
1246/// covariance itself gives it: `0 ⪯ −dP/dρ ⪯ P`.
1247///
1248/// The file already uses the left half — "`P` is operator monotone increasing in
1249/// the process-noise scale `q = e^{−ρ}`, so `dP/dρ ⪯ 0`" — to carry a Cholesky
1250/// factor of `−dP/dρ`. The right half comes from the same map being operator
1251/// CONCAVE in `q`, which is what actually bounds the derivative:
1252///
1253/// * the measurement update `P ↦ P − PHᵀ(HPHᵀ+R)⁻¹HP = (P⁻¹ + HᵀR⁻¹H)⁻¹` is the
1254///   parallel sum, which is operator concave and operator monotone;
1255/// * the prediction `P ↦ FPFᵀ + qQ` is affine and jointly monotone in `(P, q)`;
1256/// * the seed is affine in `q`.
1257///
1258/// A composition of operator-concave monotone maps with affine inner maps is
1259/// operator concave, so `q ↦ P_t(q)` is. Concavity at `q` against `0` gives
1260/// `P_t(0) ⪰ P_t(q) − q·dP_t/dq`, i.e.
1261///
1262/// ```text
1263///   0 ⪯ −dP/dρ = q·dP/dq ⪯ P(q) − P(0) ⪯ P(q),
1264/// ```
1265///
1266/// the last step because `P_t(0) ⪰ 0` is a covariance. Only the DIAGONAL of a
1267/// semidefinite ordering transfers entrywise, so that is what is intersected
1268/// here; [`intersect_covariance_minors`] then carries it to the off-diagonals
1269/// through `|X_ij| ≤ √(X_ii X_jj)`, applied to `−dP/dρ`, which is the PSD
1270/// matrix of the pair.
1271///
1272/// Measured need (#2614, order 3, ρ = −16.6135, dgp_2300): at node 40 the value
1273/// covariance `P⁺₀₀` holds width `8.4e−3` while `dP⁺₀₀/dρ` — a quantity this
1274/// bound pins into `[−0.74, 0]` — carries width `4.97e5`, and by node 80 it is
1275/// `2.5e149`. Every gain jet is a column of that matrix divided by `R`, so the
1276/// mean jet, `v′`, and `Σ v²/F̃`'s derivative inherit it directly.
1277fn intersect_derivative_covariance_below_its_own_covariance(
1278    derivative: &mut BallMat,
1279    covariance: &BallMat,
1280    order: usize,
1281) {
1282    for i in 0..order {
1283        intersect_with_exact_range(&mut derivative[i][i], -covariance[i][i].hi, 0.0);
1284    }
1285    let mut negated = [[Ball::ZERO; MAX_ORDER]; MAX_ORDER];
1286    for i in 0..order {
1287        for j in 0..order {
1288            negated[i][j] = derivative[i][j].neg();
1289        }
1290    }
1291    intersect_covariance_minors(&mut negated, order);
1292    for i in 0..order {
1293        for j in 0..order {
1294            intersect_with_independent_enclosure(&mut derivative[i][j], negated[i][j].neg());
1295        }
1296    }
1297}
1298
1299/// Intersect an enclosure with an exact two-sided range of the same quantity.
1300///
1301/// Same rule as [`intersect_with_independent_enclosure`], for evidence that is a
1302/// pair of bounds rather than a computed ball: bounds move only inward and never
1303/// past the nearest-rounded `value`, so `lo <= value <= hi` stays an invariant of
1304/// the type.
1305#[inline]
1306fn intersect_with_exact_range(entry: &mut Ball, lo: f64, hi: f64) {
1307    let floor = lo.min(entry.value);
1308    if floor.is_finite() && entry.lo < floor {
1309        entry.lo = floor;
1310    }
1311    let ceiling = hi.max(entry.value);
1312    if ceiling.is_finite() && entry.hi > ceiling {
1313        entry.hi = ceiling;
1314    }
1315}
1316
1317/// Intersect the two ORDER-ONE accumulators with the exact ranges the Gaussian
1318/// model gives them, at every node.
1319///
1320/// Once the diffuse rank is consumed, the accumulated pair over the data prefix
1321/// `y` seen so far IS the restricted Gaussian pair for that prefix: with
1322/// `V = R + qG` (`R = diag(1/w)`, `G ⪰ 0` the process-noise Gram, `q = e^{−ρ}`),
1323/// `X` the diffuse polynomial design and
1324/// `P = V⁻¹ − V⁻¹X(XᵀV⁻¹X)⁻¹XᵀV⁻¹` its restricted inverse,
1325///
1326/// ```text
1327///   Σ v²/F̃ = yᵀPy,     Σ log F̃ = log|V| + log|XᵀV⁻¹X|.
1328/// ```
1329///
1330/// `dV/dρ = −qG = −(V − R)` and `dP/dρ = −P(dV/dρ)P = P − PRP`, so
1331///
1332/// ```text
1333///   d/dρ Σ v²/F̃  = yᵀPy − yᵀPRPy,
1334///   d/dρ Σ log F̃ = tr(P dV/dρ) = −(tr(PV) − tr(PR)).
1335/// ```
1336///
1337/// Every piece of that is signed by a semidefinite fact and nothing else:
1338/// `R ⪰ 0` gives `yᵀPRPy ≥ 0`; `V − R = qG ⪰ 0` with `PVP = P` gives
1339/// `yᵀPRPy ≤ yᵀPVPy = yᵀPy` and `tr(PR) ≤ tr(PV)`; and `tr(PV) = n_proper`
1340/// exactly. Hence, EXACTLY,
1341///
1342/// ```text
1343///   0 ≤ Σ v²/F̃ ≤ yᵀR⁻¹y = Σ w y²,     0 ≤ d/dρ Σ v²/F̃ ≤ Σ v²/F̃,
1344///   −n_proper ≤ d/dρ Σ log F̃ ≤ 0,
1345/// ```
1346///
1347/// the first ceiling because `P ⪯ V⁻¹ ⪯ R⁻¹`.
1348///
1349/// Why this is the intersection that matters (#2614, measured): at smoothing
1350/// order 2 on the #2300 nodes the search refuses at `ρ = −18` with a certified
1351/// criterion DERIVATIVE ball of `±1.95e91` and a cell value enclosure of
1352/// `±1.58e83` — both finite, so no accumulator check fires, and both useless:
1353/// `strict_sign` cannot sign a `±1e91` interval, so the search can neither
1354/// bracket a stationary point nor exclude one, and reports `Unresolved`. The
1355/// true derivative is bounded by `½(n_proper + ν)`, i.e. by `178` on that
1356/// fixture. The entire `1e89` excess is dependency loss in the jet recursions,
1357/// and it is the running SUM that carries it forward, so the bound is applied
1358/// per node rather than once at the end: a partial sum is the same quantity for
1359/// its own prefix, which is what makes that legitimate.
1360///
1361/// This restores proof information supplied by the statistical model; it is not
1362/// a numerical tolerance, and it cannot make a false statement true, because
1363/// each bound is a property of the exact real number the enclosure encloses.
1364#[inline]
1365fn intersect_first_order_accumulator_exact_ranges(
1366    quadratic: &mut Ball,
1367    quadratic_d1: &mut Ball,
1368    log_determinant_d1: &mut Ball,
1369    weighted_energy: Ball,
1370    n_proper: usize,
1371) {
1372    intersect_with_exact_range(quadratic, 0.0, weighted_energy.hi);
1373    intersect_with_exact_range(quadratic_d1, 0.0, quadratic.hi);
1374    intersect_with_exact_range(log_determinant_d1, -(n_proper as f64), 0.0);
1375}
1376
1377/// Intersect the observed coordinate of the UPDATED covariance with the exact
1378/// range of its own defining map.
1379///
1380/// At the observed coordinate the Kalman update is the scalar map
1381///
1382/// ```text
1383/// P⁺₀₀ = P₀₀·R/(P₀₀ + R),
1384/// ```
1385///
1386/// whose partial derivatives `(R/F)²` and `(P₀₀/F)²` are both strictly positive
1387/// on `P₀₀ ≥ 0 < R`. A function monotone in each argument attains its range over
1388/// a box at the corners, so `[g(P.lo, R.lo), g(P.hi, R.hi)]` is the EXACT range
1389/// of this update over the input enclosure. Anything wider is dependency loss,
1390/// not information about the filter.
1391///
1392/// The componentwise Joseph evaluation loses exactly that dependency: `A = R/F`
1393/// and `K = P₀₀/F` are functions of the same `P₀₀` that appears in the middle
1394/// factor, and interval arithmetic ranges the three independently. Measured on
1395/// the #2300 nodes (order 1, ρ = 0, node 136, where `R = 1/9`, `P₀₀ = 0.062`,
1396/// `F = 0.173`): the map's true width factor is `(R/F)² = 0.41`, while the
1397/// componentwise width factor is
1398///
1399/// ```text
1400/// (R/F)² + 4·P₀₀·R²/F³ = 0.41 + 0.59 = 1.00,
1401/// ```
1402///
1403/// so the contraction is cancelled EXACTLY and the per-node rounding then
1404/// accumulates without ever being pulled back — the measured 1.28× per node
1405/// that carries `P⁺₀₀`'s width from `1.6e-14` at node 8 to `2.4e2` at node 140
1406/// on a value of `0.04`. With the range form the width contracts by `0.41` per
1407/// node against an additive `Q` term, which has a bounded fixed point.
1408///
1409/// Only `lo`/`hi` move, and only inward, and never past `value`: this is an
1410/// intersection of two valid enclosures of one quantity, so it cannot make a
1411/// false statement true.
1412#[inline]
1413fn intersect_observed_covariance_exact_range(
1414    updated: &mut Ball,
1415    predicted: Ball,
1416    observation_variance: Ball,
1417) {
1418    let corner = |p: f64, r: f64| -> Option<Ball> {
1419        let p = Ball::exact(p.max(0.0));
1420        let r = Ball::exact(r);
1421        let f = p.add(r);
1422        (f.lo > 0.0).then(|| p.mul(r).div_positive(f))
1423    };
1424    if let Some(low) = corner(predicted.lo, observation_variance.lo) {
1425        let floor = low.lo.min(updated.value);
1426        if floor.is_finite() && updated.lo < floor {
1427            updated.lo = floor;
1428        }
1429    }
1430    if let Some(high) = corner(predicted.hi, observation_variance.hi) {
1431        let ceiling = high.hi.max(updated.value);
1432        if ceiling.is_finite() && updated.hi > ceiling {
1433            updated.hi = ceiling;
1434        }
1435    }
1436}
1437
1438/// Intersect a general entry of the UPDATED covariance with the exact range of
1439/// the Schur complement that defines it.
1440///
1441/// [`intersect_observed_covariance_exact_range`] is this argument at the
1442/// observed coordinate, where the map collapses to one scalar variable. The
1443/// general entry is
1444///
1445/// ```text
1446/// P⁺[i][j] = P[i][j] − P[i][0]·P[0][j] / (P[0][0] + R),
1447/// ```
1448///
1449/// whose partials with respect to `(a, b, c, d) = (P[i][j], P[i][0], P[0][j],
1450/// P[0][0])` are `1`, `−c/F`, `−b/F` and `bc/F²`. Whenever `b` and `c` have
1451/// DEFINITE sign the sign of every partial is fixed over the whole box, so the
1452/// range is attained at two corners and two evaluations bound it exactly. `R`
1453/// enters like `d` and moves with it.
1454///
1455/// Treating the four as independent is conservative — they are entries of one
1456/// PSD matrix — so this is a valid enclosure, and it is the tightest available
1457/// without carrying that correlation. What it removes is the evaluation-order
1458/// dependency: `F` appears in three factors of the componentwise form and
1459/// `P[0][0]` in four, and interval arithmetic ranges each occurrence
1460/// separately. When `b` or `c` straddles zero the monotonicity argument does
1461/// not hold and nothing is claimed.
1462fn intersect_updated_covariance_exact_range(
1463    updated: &mut Ball,
1464    entry: Ball,
1465    row: Ball,
1466    column: Ball,
1467    observed: Ball,
1468    observation_variance: Ball,
1469) {
1470    // `Some(true)` = nonnegative throughout, `Some(false)` = nonpositive
1471    // throughout, `None` = straddles zero and the partials change sign.
1472    let definite_sign = |ball: Ball| -> Option<bool> {
1473        if ball.lo >= 0.0 {
1474            Some(true)
1475        } else if ball.hi <= 0.0 {
1476            Some(false)
1477        } else {
1478            None
1479        }
1480    };
1481    let (Some(row_nonnegative), Some(column_nonnegative)) =
1482        (definite_sign(row), definite_sign(column))
1483    else {
1484        return;
1485    };
1486    // `∂/∂d = bc/F²` is nonnegative exactly when `b` and `c` agree in sign.
1487    let product_nonnegative = row_nonnegative == column_nonnegative;
1488    let corner = |minimizing: bool| -> Option<Ball> {
1489        // `∂/∂a = 1`.
1490        let a = Ball::exact(if minimizing { entry.lo } else { entry.hi });
1491        // `∂/∂b = −c/F`: decreasing in `b` when `c ≥ 0`.
1492        let b = Ball::exact(if minimizing == column_nonnegative {
1493            row.hi
1494        } else {
1495            row.lo
1496        });
1497        // `∂/∂c = −b/F`, symmetrically.
1498        let c = Ball::exact(if minimizing == row_nonnegative {
1499            column.hi
1500        } else {
1501            column.lo
1502        });
1503        let take_low = minimizing == product_nonnegative;
1504        let d = Ball::exact(if take_low { observed.lo } else { observed.hi }.max(0.0));
1505        let variance = Ball::exact(if take_low {
1506            observation_variance.lo
1507        } else {
1508            observation_variance.hi
1509        });
1510        let f = d.add(variance);
1511        (f.lo > 0.0).then(|| a.sub(b.mul(c).div_positive(f)))
1512    };
1513    if let Some(low) = corner(true) {
1514        let floor = low.lo.min(updated.value);
1515        if floor.is_finite() && updated.lo < floor {
1516            updated.lo = floor;
1517        }
1518    }
1519    if let Some(high) = corner(false) {
1520        let ceiling = high.hi.max(updated.value);
1521        if ceiling.is_finite() && updated.hi > ceiling {
1522            updated.hi = ceiling;
1523        }
1524    }
1525}
1526
1527/// Intersect a covariance enclosure with the exact 2×2 minors of the PSD
1528/// constraint it satisfies.
1529///
1530/// [`intersect_proper_covariance_psd`] uses the 1×1 minors — `P[i][i] ≥ 0` —
1531/// and stops there. The 2×2 minors are equally exact and two-sided:
1532///
1533/// ```text
1534/// P[i][i]·P[j][j] − P[i][j]² ≥ 0   ⇒   |P[i][j]| ≤ √(P[i][i]·P[j][j]),
1535/// ```
1536///
1537/// which bounds every off-diagonal by the diagonals rather than letting it
1538/// drift on its own. This matters because only the OBSERVED direction is
1539/// contracted by an update: the componentwise recursion has nothing that pulls
1540/// an unobserved covariance back, and the transition then mixes that drift into
1541/// the observed entry as `P₀₀ + 2δP₀₁ + δ²P₁₁`.
1542///
1543/// The bound is rounded up by one ulp so that a rounded square root can never
1544/// claim more than the minor supports.
1545fn intersect_covariance_minors(covariance: &mut BallMat, order: usize) {
1546    let sqrt_upper = |value: f64| -> f64 {
1547        if !(value.is_finite() && value > 0.0) {
1548            return value;
1549        }
1550        let root = value.sqrt();
1551        if root * root >= value {
1552            root
1553        } else {
1554            f64::from_bits(root.to_bits() + 1)
1555        }
1556    };
1557    for i in 0..order {
1558        for j in 0..order {
1559            if i == j {
1560                continue;
1561            }
1562            let diagonal_product = Ball::exact(covariance[i][i].hi.max(0.0))
1563                .mul(Ball::exact(covariance[j][j].hi.max(0.0)));
1564            if !diagonal_product.is_finite() {
1565                continue;
1566            }
1567            let bound = sqrt_upper(diagonal_product.hi);
1568            if !bound.is_finite() {
1569                continue;
1570            }
1571            let entry = &mut covariance[i][j];
1572            let floor = (-bound).min(entry.value);
1573            if entry.lo < floor {
1574                entry.lo = floor;
1575            }
1576            let ceiling = bound.max(entry.value);
1577            if entry.hi > ceiling {
1578                entry.hi = ceiling;
1579            }
1580        }
1581    }
1582}
1583
1584/// Intersect a proper covariance enclosure with its exact PSD invariant.
1585///
1586/// Once the diffuse rank is exhausted, `P*` is the conditional covariance of
1587/// the state. Its diagonal is therefore nonnegative at every measurement
1588/// update and prediction. A componentwise interval evaluation of
1589/// `P - PH'(HPH' + R)⁻¹HP` forgets that dependency and can widen a diagonal
1590/// through zero after repeated rank-one subtractions, even though the exact
1591/// innovation is bounded below by the positive observation variance `R`.
1592///
1593/// This intersection restores proof information supplied by the statistical
1594/// model; it is not a numerical tolerance. A wholly negative or non-finite
1595/// diagonal still signals an inconsistent enclosure and fails closed.
1596#[inline]
1597fn intersect_proper_covariance_psd(
1598    covariance: &mut BallMat,
1599    order: usize,
1600) -> Result<(), SplineScoreProofError> {
1601    for (index, row) in covariance.iter_mut().enumerate().take(order) {
1602        let diagonal = &mut row[index];
1603        if !diagonal.is_finite() || diagonal.hi < 0.0 {
1604            return Err(SplineScoreProofError::InvalidArithmetic {
1605                context: "proper covariance PSD intersection",
1606            });
1607        }
1608        diagonal.lo = diagonal.lo.max(0.0);
1609    }
1610    Ok(())
1611}
1612
1613/// Per-node filter storage needed by the RTS backward pass.
1614struct FilterStep {
1615    /// Filtered mean `a_{t|t}` and proper covariance `P*_{t|t}`.
1616    a_filt: Vec2,
1617    p_filt: Mat2,
1618    /// One-step prediction `a_{t|t-1}`, proper covariance `P*_{t|t-1}` (for t ≥ 1).
1619    a_pred: Vec2,
1620    p_pred: Mat2,
1621}
1622
1623/// Output of one full filter pass at a fixed `q = 1/λ` (run at unit σ²).
1624struct FilterPass {
1625    steps: Vec<FilterStep>,
1626    /// Σ over proper steps of `log F̃_t` (innovation variances at σ²=1).
1627    sum_log_f: f64,
1628    /// First three analytic derivatives of `sum_log_f` with respect to
1629    /// `rho = log lambda` (`q = exp(-rho)`). Endpoint pairs linearly
1630    /// interpolate the third order under a global `L5` bound, certifying the
1631    /// λ→∞ tail at fourth-order width `(|V′|/L₅)^{1/4}` (#2300/#2614).
1632    sum_log_f_d1: f64,
1633    sum_log_f_d2: f64,
1634    sum_log_f_d3: f64,
1635    /// Σ over proper steps of `v_t² / F̃_t`.
1636    sum_v2_over_f: f64,
1637    /// First three analytic `rho` derivatives of `sum_v2_over_f`.
1638    sum_v2_over_f_d1: f64,
1639    sum_v2_over_f_d2: f64,
1640    sum_v2_over_f_d3: f64,
1641    /// Number of proper (non-diffuse) innovations.
1642    n_proper: usize,
1643}
1644
1645/// The scalar criterion accumulators with directed-rounding enclosures.
1646#[derive(Debug)]
1647struct BallFilterPass {
1648    sum_log_f: Ball,
1649    sum_log_f_d1: Ball,
1650    sum_log_f_d2: Ball,
1651    sum_log_f_d3: Ball,
1652    sum_v2_over_f: Ball,
1653    sum_v2_over_f_d1: Ball,
1654    sum_v2_over_f_d2: Ball,
1655    sum_v2_over_f_d3: Ball,
1656    n_proper: usize,
1657}
1658
1659/// One forward pass of the exact diffuse filter.
1660///
1661/// `RECORD_STEPS` selects whether the per-node filtered/predicted states are
1662/// retained. They are needed ONLY by the RTS backward smoother in
1663/// [`fit_spline_scan_at`]; the profiled REML criterion
1664/// ([`concentrated_criterion_jet`]) reads nothing but the scalar accumulators.
1665/// Recording them unconditionally made every criterion evaluation of the
1666/// certified log-lambda search allocate, fill, and immediately drop
1667/// `n * size_of::<FilterStep>()` bytes — at the biobank scale this fast path
1668/// exists for (n = 1e6, 192 B per node) that is 192 MB of write traffic per
1669/// evaluation, thrown away.
1670fn run_filter<const RECORD_STEPS: bool>(
1671    nodes: &[PooledNode],
1672    q: f64,
1673    order: usize,
1674) -> Result<FilterPass, String> {
1675    let n = nodes.len();
1676    let mut steps = Vec::with_capacity(if RECORD_STEPS { n } else { 0 });
1677    // Exact diffuse initialization (Durbin–Koopman): P = P* + κ·P_∞, κ → ∞.
1678    // The order-`m` polynomial null space (degree < m) is fully diffuse: the
1679    // diffuse rank starts at `order`, consumed by the first `order` distinct
1680    // abscissae.
1681    let mut a: Vec2 = [0.0; MAX_ORDER];
1682    let mut a_d1: Vec2 = [0.0; MAX_ORDER];
1683    let mut a_d2: Vec2 = [0.0; MAX_ORDER];
1684    let mut a_d3: Vec2 = [0.0; MAX_ORDER];
1685    let mut p_star: Mat2 = [[0.0; MAX_ORDER]; MAX_ORDER];
1686    let mut p_star_d1: Mat2 = [[0.0; MAX_ORDER]; MAX_ORDER];
1687    let mut p_star_d2: Mat2 = [[0.0; MAX_ORDER]; MAX_ORDER];
1688    let mut p_star_d3: Mat2 = [[0.0; MAX_ORDER]; MAX_ORDER];
1689    let mut p_inf: Mat2 = [[0.0; MAX_ORDER]; MAX_ORDER];
1690    for i in 0..order {
1691        p_inf[i][i] = 1.0;
1692    }
1693    let mut diffuse_rank = order;
1694    let mut sum_log_f = 0.0;
1695    let mut sum_log_f_d1 = 0.0;
1696    let mut sum_log_f_d2 = 0.0;
1697    let mut sum_log_f_d3 = 0.0;
1698    let mut sum_v2_over_f = 0.0;
1699    let mut sum_v2_over_f_d1 = 0.0;
1700    let mut sum_v2_over_f_d2 = 0.0;
1701    let mut sum_v2_over_f_d3 = 0.0;
1702    let mut n_proper = 0usize;
1703    for t in 0..n {
1704        let a_pred = a;
1705        let p_pred = p_star;
1706        let r = 1.0 / nodes[t].w;
1707        let v = nodes[t].y - a[0];
1708        let v_d1 = -a_d1[0];
1709        let v_d2 = -a_d2[0];
1710        let v_d3 = -a_d3[0];
1711        // H = [1 0 … 0] ⇒ M = P·H' is the first column, F = M[0] (+ r).
1712        let mut m_star: Vec2 = [0.0; MAX_ORDER];
1713        let mut m_star_d1: Vec2 = [0.0; MAX_ORDER];
1714        let mut m_star_d2: Vec2 = [0.0; MAX_ORDER];
1715        let mut m_star_d3: Vec2 = [0.0; MAX_ORDER];
1716        for i in 0..order {
1717            m_star[i] = p_star[i][0];
1718            m_star_d1[i] = p_star_d1[i][0];
1719            m_star_d2[i] = p_star_d2[i][0];
1720            m_star_d3[i] = p_star_d3[i][0];
1721        }
1722        let f_star = m_star[0] + r;
1723        let f_star_d1 = m_star_d1[0];
1724        let f_star_d2 = m_star_d2[0];
1725        let f_star_d3 = m_star_d3[0];
1726        let mut proper_update = diffuse_rank == 0;
1727        if diffuse_rank > 0 {
1728            let mut m_inf: Vec2 = [0.0; MAX_ORDER];
1729            for i in 0..order {
1730                m_inf[i] = p_inf[i][0];
1731            }
1732            let f_inf = m_inf[0];
1733            if !f_inf.is_finite() {
1734                return Err(format!(
1735                    "spline scan: non-finite diffuse innovation variance at node {t}: {f_inf}"
1736                ));
1737            } else if f_inf > 0.0 {
1738                // Exact diffuse update (Koopman 1997): the κ→∞ limit of the
1739                // standard update; the diffuse step contributes −½·log F_∞ to
1740                // the restricted likelihood and consumes one diffuse dimension.
1741                for i in 0..order {
1742                    let k_inf = m_inf[i] / f_inf;
1743                    a[i] += k_inf * v;
1744                    a_d1[i] += k_inf * v_d1;
1745                    a_d2[i] += k_inf * v_d2;
1746                    a_d3[i] += k_inf * v_d3;
1747                }
1748                let mut p_new = p_star;
1749                let mut p_new_d1 = p_star_d1;
1750                let mut p_new_d2 = p_star_d2;
1751                let mut p_new_d3 = p_star_d3;
1752                for i in 0..order {
1753                    for j in 0..order {
1754                        p_new[i][j] += -m_inf[i] * m_star[j] / f_inf - m_star[i] * m_inf[j] / f_inf
1755                            + m_inf[i] * m_inf[j] * f_star / (f_inf * f_inf);
1756                        p_new_d1[i][j] += -m_inf[i] * m_star_d1[j] / f_inf
1757                            - m_star_d1[i] * m_inf[j] / f_inf
1758                            + m_inf[i] * m_inf[j] * f_star_d1 / (f_inf * f_inf);
1759                        p_new_d2[i][j] += -m_inf[i] * m_star_d2[j] / f_inf
1760                            - m_star_d2[i] * m_inf[j] / f_inf
1761                            + m_inf[i] * m_inf[j] * f_star_d2 / (f_inf * f_inf);
1762                        p_new_d3[i][j] += -m_inf[i] * m_star_d3[j] / f_inf
1763                            - m_star_d3[i] * m_inf[j] / f_inf
1764                            + m_inf[i] * m_inf[j] * f_star_d3 / (f_inf * f_inf);
1765                    }
1766                }
1767                p_star = p_new;
1768                p_star_d1 = p_new_d1;
1769                p_star_d2 = p_new_d2;
1770                p_star_d3 = p_new_d3;
1771                symmetrize(&mut p_star, order);
1772                symmetrize(&mut p_star_d1, order);
1773                symmetrize(&mut p_star_d2, order);
1774                symmetrize(&mut p_star_d3, order);
1775                for i in 0..order {
1776                    for j in 0..order {
1777                        p_inf[i][j] -= m_inf[i] * m_inf[j] / f_inf;
1778                    }
1779                }
1780                symmetrize(&mut p_inf, order);
1781                diffuse_rank -= 1;
1782                if diffuse_rank == 0 {
1783                    p_inf = [[0.0; MAX_ORDER]; MAX_ORDER];
1784                }
1785            } else if f_inf == 0.0 {
1786                // Diffuse direction orthogonal to H: this observation is an
1787                // ordinary proper update of P* even though diffuse rank remains.
1788                proper_update = true;
1789            } else {
1790                return Err(format!(
1791                    "spline scan: non-positive diffuse innovation variance at node {t}: {f_inf}"
1792                ));
1793            }
1794        }
1795        if proper_update {
1796            if !(f_star.is_finite() && f_star > 0.0) {
1797                return Err(format!(
1798                    "spline scan: non-positive or non-finite proper innovation variance \
1799                     at node {t}: {f_star}"
1800                ));
1801            }
1802            let inv_f = 1.0 / f_star;
1803            // Quotient jets in the recursive Leibniz form: for s = num/f,
1804            //   s_k = (num_k − Σ_{j=1..k} C(k,j)·s_{k−j}·f_j) / f,
1805            // which is exactly the closed inv_f² / inv_f³ expansion used
1806            // before, extended to third order.
1807            let mut gain = [0.0; MAX_ORDER];
1808            let mut gain_d1 = [0.0; MAX_ORDER];
1809            let mut gain_d2 = [0.0; MAX_ORDER];
1810            let mut gain_d3 = [0.0; MAX_ORDER];
1811            for i in 0..order {
1812                gain[i] = m_star[i] * inv_f;
1813                gain_d1[i] = (m_star_d1[i] - gain[i] * f_star_d1) * inv_f;
1814                gain_d2[i] =
1815                    (m_star_d2[i] - 2.0 * gain_d1[i] * f_star_d1 - gain[i] * f_star_d2) * inv_f;
1816                gain_d3[i] = (m_star_d3[i]
1817                    - 3.0 * gain_d2[i] * f_star_d1
1818                    - 3.0 * gain_d1[i] * f_star_d2
1819                    - gain[i] * f_star_d3)
1820                    * inv_f;
1821            }
1822            let a_old_d1 = a_d1;
1823            let a_old_d2 = a_d2;
1824            let a_old_d3 = a_d3;
1825            for i in 0..order {
1826                a[i] += gain[i] * v;
1827                a_d1[i] = a_old_d1[i] + gain_d1[i] * v + gain[i] * v_d1;
1828                a_d2[i] = a_old_d2[i] + gain_d2[i] * v + 2.0 * gain_d1[i] * v_d1 + gain[i] * v_d2;
1829                a_d3[i] = a_old_d3[i]
1830                    + gain_d3[i] * v
1831                    + 3.0 * gain_d2[i] * v_d1
1832                    + 3.0 * gain_d1[i] * v_d2
1833                    + gain[i] * v_d3;
1834            }
1835            // The VALUE covariance through the Joseph form, the same expression
1836            // the certified pass runs (#2614).
1837            //
1838            // `P − M Mᵀ/F` subtracts near-equal quantities: on the #2300 nodes
1839            // at `ρ = −24` both terms are `1.84e5` and their difference is `1`,
1840            // so the result is accurate to `F/R` times the rounding of its own
1841            // size. `P⁺ = A P Aᵀ + R K Kᵀ` with `A = I − K e₀ᵀ` is the same
1842            // quantity as a sum of positive contributions at the observed
1843            // coordinate. This is not only the certified pass's concern: the two
1844            // passes must run the same expression, or the enclosure would be
1845            // certifying arithmetic the scalar path does not perform.
1846            let mut a_operator = [[0.0; MAX_ORDER]; MAX_ORDER];
1847            for (i, row) in a_operator.iter_mut().enumerate().take(order) {
1848                row[i] = 1.0;
1849            }
1850            a_operator[0][0] = r * inv_f;
1851            for i in 1..order {
1852                a_operator[i][0] = -gain[i];
1853            }
1854            let a_operator_t = mat_t(&a_operator, order);
1855            let mut p_new = mat_mul(&mat_mul(&a_operator, &p_star, order), &a_operator_t, order);
1856            for i in 0..order {
1857                for j in 0..order {
1858                    p_new[i][j] += gain[i] * gain[j] * r;
1859                }
1860            }
1861            let mut p_new_d1 = p_star_d1;
1862            let mut p_new_d2 = p_star_d2;
1863            let mut p_new_d3 = p_star_d3;
1864            for i in 0..order {
1865                for j in 0..order {
1866                    let mm = m_star[i] * m_star[j];
1867                    let mm_d1 = m_star_d1[i] * m_star[j] + m_star[i] * m_star_d1[j];
1868                    let mm_d2 = m_star_d2[i] * m_star[j]
1869                        + 2.0 * m_star_d1[i] * m_star_d1[j]
1870                        + m_star[i] * m_star_d2[j];
1871                    let mm_d3 = m_star_d3[i] * m_star[j]
1872                        + 3.0 * m_star_d2[i] * m_star_d1[j]
1873                        + 3.0 * m_star_d1[i] * m_star_d2[j]
1874                        + m_star[i] * m_star_d3[j];
1875                    let s0 = mm * inv_f;
1876                    let s1 = (mm_d1 - s0 * f_star_d1) * inv_f;
1877                    let s2 = (mm_d2 - 2.0 * s1 * f_star_d1 - s0 * f_star_d2) * inv_f;
1878                    let s3 = (mm_d3 - 3.0 * s2 * f_star_d1 - 3.0 * s1 * f_star_d2 - s0 * f_star_d3)
1879                        * inv_f;
1880                    p_new_d1[i][j] -= s1;
1881                    p_new_d2[i][j] -= s2;
1882                    p_new_d3[i][j] -= s3;
1883                }
1884            }
1885            p_star = p_new;
1886            p_star_d1 = p_new_d1;
1887            p_star_d2 = p_new_d2;
1888            p_star_d3 = p_new_d3;
1889            symmetrize(&mut p_star, order);
1890            symmetrize(&mut p_star_d1, order);
1891            symmetrize(&mut p_star_d2, order);
1892            symmetrize(&mut p_star_d3, order);
1893
1894            let vv = v * v;
1895            let vv_d1 = 2.0 * v * v_d1;
1896            let vv_d2 = 2.0 * (v_d1 * v_d1 + v * v_d2);
1897            let vv_d3 = 2.0 * (v * v_d3 + 3.0 * v_d1 * v_d2);
1898            let logf_d1 = f_star_d1 * inv_f;
1899            let logf_d2 = f_star_d2 * inv_f - logf_d1 * logf_d1;
1900            let logf_d3 = f_star_d3 * inv_f - 3.0 * (f_star_d2 * inv_f) * logf_d1
1901                + 2.0 * logf_d1 * logf_d1 * logf_d1;
1902            sum_log_f += f_star.ln();
1903            sum_log_f_d1 += logf_d1;
1904            sum_log_f_d2 += logf_d2;
1905            sum_log_f_d3 += logf_d3;
1906            let t0 = vv * inv_f;
1907            let t1 = (vv_d1 - t0 * f_star_d1) * inv_f;
1908            let t2 = (vv_d2 - 2.0 * t1 * f_star_d1 - t0 * f_star_d2) * inv_f;
1909            let t3 = (vv_d3 - 3.0 * t2 * f_star_d1 - 3.0 * t1 * f_star_d2 - t0 * f_star_d3) * inv_f;
1910            sum_v2_over_f += t0;
1911            sum_v2_over_f_d1 += t1;
1912            sum_v2_over_f_d2 += t2;
1913            sum_v2_over_f_d3 += t3;
1914            n_proper += 1;
1915        }
1916        if RECORD_STEPS {
1917            steps.push(FilterStep {
1918                a_filt: a,
1919                p_filt: p_star,
1920                a_pred,
1921                p_pred,
1922            });
1923        }
1924        // Predict to the next node.
1925        if t + 1 < n {
1926            let delta = nodes[t + 1].x - nodes[t].x;
1927            let f_t = transition(delta, order);
1928            a = mat_vec(&f_t, &a, order);
1929            a_d1 = mat_vec(&f_t, &a_d1, order);
1930            a_d2 = mat_vec(&f_t, &a_d2, order);
1931            a_d3 = mat_vec(&f_t, &a_d3, order);
1932            let f_t_t = mat_t(&f_t, order);
1933            let q_noise = process_noise(delta, q, order);
1934            let mut p_next = mat_add(
1935                &mat_mul(&mat_mul(&f_t, &p_star, order), &f_t_t, order),
1936                &q_noise,
1937                order,
1938            );
1939            let mut p_next_d1 = mat_sub(
1940                &mat_mul(&mat_mul(&f_t, &p_star_d1, order), &f_t_t, order),
1941                &q_noise,
1942                order,
1943            );
1944            let mut p_next_d2 = mat_add(
1945                &mat_mul(&mat_mul(&f_t, &p_star_d2, order), &f_t_t, order),
1946                &q_noise,
1947                order,
1948            );
1949            // d^k q / d rho^k = (−1)^k q, so the noise term alternates sign.
1950            let mut p_next_d3 = mat_sub(
1951                &mat_mul(&mat_mul(&f_t, &p_star_d3, order), &f_t_t, order),
1952                &q_noise,
1953                order,
1954            );
1955            symmetrize(&mut p_next, order);
1956            symmetrize(&mut p_next_d1, order);
1957            symmetrize(&mut p_next_d2, order);
1958            symmetrize(&mut p_next_d3, order);
1959            p_star = p_next;
1960            p_star_d1 = p_next_d1;
1961            p_star_d2 = p_next_d2;
1962            p_star_d3 = p_next_d3;
1963            if diffuse_rank > 0 {
1964                let mut pi_next =
1965                    mat_mul(&mat_mul(&f_t, &p_inf, order), &mat_t(&f_t, order), order);
1966                symmetrize(&mut pi_next, order);
1967                p_inf = pi_next;
1968            }
1969        }
1970    }
1971    Ok(FilterPass {
1972        steps,
1973        sum_log_f,
1974        sum_log_f_d1,
1975        sum_log_f_d2,
1976        sum_log_f_d3,
1977        sum_v2_over_f,
1978        sum_v2_over_f_d1,
1979        sum_v2_over_f_d2,
1980        sum_v2_over_f_d3,
1981        n_proper,
1982    })
1983}
1984
1985/// Directed-rounding twin of [`run_filter`] used by automatic REML selection.
1986///
1987/// The recurrence follows the production diffuse filter operation for
1988/// operation, but every scalar carries an outward interval. Its cost is
1989/// `O(n·order³)` (the same fixed-size covariance propagations as the scalar
1990/// pass), with no data-dependent refinement knobs. A denominator is used only
1991/// after its innovation ball is proved strictly positive; if the ball contains
1992/// zero, the typed proof refusal is returned at that exact node.
1993fn run_filter_ball(
1994    nodes: &[PooledNode],
1995    q: Ball,
1996    order: usize,
1997) -> Result<BallFilterPass, SplineScoreProofError> {
1998    run_filter_ball_traced(nodes, q, order, None)
1999}
2000
2001/// One `(node, quantity, ball)` record of the `d3` recursion, for the caller
2002/// that asked to see it.
2003///
2004/// A refusal names the accumulator that left the finite range; it cannot show
2005/// how the width GOT there, and that is the difference between "the recursion
2006/// grows" and "the interval evaluation cannot see a cancellation the exact
2007/// arithmetic has". Only a caller that passes a sink pays for this.
2008type BallTraceRecord = (usize, &'static str, Ball);
2009
2010/// Stacked mean blocks carried as ONE zonotope: `(a, a′)`.
2011const MEAN_BLOCKS: usize = 2;
2012/// Capacity of that stacked state; the ACTIVE dimension is `2·order`.
2013const MEAN_DIM: usize = MEAN_BLOCKS * MAX_ORDER;
2014/// Capacity of the `vec(dP/dρ)` state; the ACTIVE dimension is `order²`.
2015const COVARIANCE_D1_DIM: usize = MAX_ORDER * MAX_ORDER;
2016/// Generators retained before the lowest-correlation directions are folded
2017/// into an axis-aligned set.
2018///
2019/// Folding is a sound outer reduction: the axis box contains the discarded
2020/// generators and is itself a zonotope, so it keeps being transformed by the
2021/// true map rather than by its absolute value. The cap only bounds the work:
2022/// `dim²·CAP` per node, `O(n)` overall. Reduction chooses the generators for
2023/// which a box loses the least signed correlation; it never assumes that age
2024/// alone implies contraction.
2025const ZONOTOPE_GENERATOR_CAP: usize = 240;
2026/// `γ_{dim+2}` with room to spare: `2·(d+2)·u` with `u = ε/2` is `11ε` at
2027/// `d = 9`, and this charges `32ε` for every floating-point dot product a
2028/// zonotope forms.
2029const ZONOTOPE_ROUNDOFF: f64 = 32.0 * f64::EPSILON;
2030
2031/// Radius of a ball ABOUT ITS REPRESENTATIVE, which is what a zonotope centred
2032/// on that representative must absorb. Not `(hi−lo)/2`: the representative is
2033/// not required to be the midpoint.
2034#[inline]
2035fn ball_radius_about_value(ball: Ball) -> f64 {
2036    let above = ball.hi - ball.value;
2037    let below = ball.value - ball.lo;
2038    next_up_ball(above.max(below).max(0.0))
2039}
2040
2041/// A linear recursion's state as a ZONOTOPE — a centre plus a list of error
2042/// GENERATORS — rather than as componentwise intervals.
2043///
2044/// This is not a tightening heuristic. Two of this filter's recursions cannot
2045/// be carried in a box AT ANY WIDTH, and the reason is arithmetic rather than
2046/// numerical. Per node the mean does update-then-predict, `a ← T(A a + K y)`
2047/// with `A = I − K e₀ᵀ`, so the fused per-node map at order 2 is
2048///
2049/// ```text
2050///   B = T A = [[R/F − δ·K₁ ,  δ ],
2051///              [   −K₁     ,  1 ]]
2052/// ```
2053///
2054/// Measured on the #2300 nodes at the ρ the certified search refused at
2055/// (`−13.841116908`): `R/F = 0.0754`, `K₁ ≈ 41.5`, `δ = 4/179`, so `δ·K₁ = 0.930`
2056/// and
2057///
2058/// ```text
2059///   true B : trace 0.145 , det 0.0754  ⇒ |eigenvalues| = √0.0754 = 0.2746
2060///   |B|    : trace 1.855 , det −0.0754 ⇒ ρ(|B|)        = 1.894
2061/// ```
2062///
2063/// **The map contracts at 0.27 per node and its entrywise absolute value
2064/// expands at 1.894.** A componentwise interval carries widths through `|B|`,
2065/// because `a₀` reaches the next `a₀` by two paths — the row-0 update, and row 1
2066/// followed by the transition — whose widths ADD as `0.0754 + 0.930` where the
2067/// exact map SUBTRACTS to `−0.855`. Over 180 nodes that is `1.894¹⁸⁰ ≈ 10⁵⁰`,
2068/// and the traced widths reproduced it exactly: `w(a₀)` ran `1e−11 → 1e20` from
2069/// node 10 to node 113 at a clean factor of 2.0 per node, while every
2070/// covariance quantity in the same pass stayed flat (`w(P₀₀) = 5.6e−13`,
2071/// `w(F) = 1e−10`, `w(Σ log F) = 7.4e−10`).
2072///
2073/// The covariance's FIRST DERIVATIVE has the same defect one tensor rank up.
2074/// `dP⁺ = A·dP·Aᵀ` and `dP⁻ = F·dP·Fᵀ − Q` make `vec(dP)` a linear recursion
2075/// with map `B ⊗ B`, whose true spectral radius is `0.27² = 0.075` and whose
2076/// componentwise companion `|B| ⊗ |B|` is `1.894² = 3.59`. Measured at
2077/// `ρ = −12.5466`, order 2: `w(dP₁₁)` runs `1.4e−2 → 7.5e2` over nodes 40..80
2078/// while `w(P₀₀)` holds at `7e−12`.
2079///
2080/// Two rearrangements do NOT help and are recorded so they are not retried:
2081/// fusing `T` and `A` first leaves `ρ(|TA|)` at the same 1.894 — the
2082/// cancellation is between entries of the product, not between the factors —
2083/// and rescaling the state cannot help either, since `ρ(|D⁻¹BD|) ≥ ρ(|B|)` for
2084/// every diagonal `D` and the natural step scaling `D = diag(1, δ)` attains it
2085/// exactly.
2086///
2087/// A zonotope keeps the cancellation because it transforms each GENERATOR by
2088/// the true map, so generator norms follow `0.27` per node and a generator is
2089/// below `ε` relative after ~35 nodes. The value covariance escaped the same
2090/// problem by being carried as a Cholesky factor; neither the mean nor `dP`
2091/// has PSD structure enough to exploit that way, but both are exactly LINEAR
2092/// with coefficients this filter already encloses tightly, which is the
2093/// hypothesis a zonotope needs and the Riccati update itself does not satisfy.
2094///
2095/// `N` is the capacity; `dim` is how much of it the current smoothing order
2096/// uses, and every loop stops there, so an order-1 scan pays `1`-dimensional
2097/// work out of a `9`-wide array.
2098#[derive(Clone, Debug)]
2099struct Zonotope<const N: usize> {
2100    center: [f64; N],
2101    /// Coefficient of the one normalized uncertainty variable shared by every
2102    /// occurrence of the certified process-noise scale `q`.
2103    ///
2104    /// This generator is structural, not part of the disposable remainder
2105    /// basis: compaction may fold independent roundoff generators, but it must
2106    /// never turn one common scalar into independent per-node errors.
2107    shared_q: [f64; N],
2108    generators: Vec<[f64; N]>,
2109    dim: usize,
2110}
2111
2112impl<const N: usize> Zonotope<N> {
2113    fn zeroed(dim: usize) -> Self {
2114        // `N` is the array capacity and `dim` indexes into it, so a `dim > N`
2115        // is an out-of-bounds every loop in this file would then commit. A
2116        // `debug_assert!` compiles to nothing in the release profile the
2117        // scan actually ships in, which is precisely where the bound stops
2118        // being checked by anything else -- hence the workspace ban.
2119        assert!(dim <= N, "zonotope dim {dim} exceeds its capacity {N}");
2120        Self {
2121            center: [0.0; N],
2122            shared_q: [0.0; N],
2123            generators: Vec::new(),
2124            dim,
2125        }
2126    }
2127
2128    /// One coordinate as an ordinary ball, for the consumers that need a scalar.
2129    fn coordinate(&self, index: usize) -> Ball {
2130        let mut radius = self.shared_q[index].abs();
2131        for generator in &self.generators {
2132            radius = next_up_ball(radius + generator[index].abs());
2133        }
2134        let value = self.center[index];
2135        Ball {
2136            value,
2137            lo: next_down_ball(value - radius),
2138            hi: next_up_ball(value + radius),
2139        }
2140    }
2141
2142    /// `x ← M x + b`, exactly on the generators, with every floating-point and
2143    /// interval radius charged into FRESH axis-aligned generators.
2144    ///
2145    /// The fresh radii are appended as `radius·eᵢ` rather than held in a
2146    /// separate box field, because a box propagated as `|M|·box` would grow at
2147    /// `ρ(|M|)` per node — reintroducing the exact defect this type exists to
2148    /// remove, on a quantity too small to notice until it is `1e11`.
2149    fn apply(&mut self, map: &[[Ball; N]; N], constant: &[Ball; N]) -> bool {
2150        self.apply_with_shared_q(map, constant, &[0.0; N])
2151    }
2152
2153    /// `x ← Mx + b + g_q θ_q`, where the SAME `θ_q ∈ [-1, 1]` is carried by
2154    /// every process-noise injection in the complete filter pass.
2155    ///
2156    /// The interval part of `b` contains only nonlinear and floating-point
2157    /// remainder. It therefore enters as fresh independent generators, while
2158    /// `g_q` is accumulated onto the distinguished generator after that
2159    /// generator has followed the signed map `M`.
2160    fn apply_with_shared_q(
2161        &mut self,
2162        map: &[[Ball; N]; N],
2163        constant: &[Ball; N],
2164        shared_q_constant: &[f64; N],
2165    ) -> bool {
2166        let dim = self.dim;
2167        let mut generator_column_sum = [0.0f64; N];
2168        for (sum, &coordinate) in generator_column_sum
2169            .iter_mut()
2170            .zip(self.shared_q.iter())
2171            .take(dim)
2172        {
2173            *sum = coordinate.abs();
2174        }
2175        for generator in &self.generators {
2176            for j in 0..dim {
2177                generator_column_sum[j] =
2178                    next_up_ball(generator_column_sum[j] + generator[j].abs());
2179            }
2180        }
2181
2182        let mut next_center = [0.0f64; N];
2183        let mut next_shared_q = [0.0f64; N];
2184        let mut fresh_radius = [0.0f64; N];
2185        for i in 0..dim {
2186            let mut center = constant[i].value;
2187            let mut shared_q = shared_q_constant[i];
2188            // Everything the roundoff of the dot products — the centre's and
2189            // every generator's — is charged against, summed once.
2190            let mut magnitude = constant[i].value.abs() + shared_q_constant[i].abs();
2191            let mut radius = ball_radius_about_value(constant[i]);
2192            for j in 0..dim {
2193                let coefficient = map[i][j].value;
2194                center += coefficient * self.center[j];
2195                shared_q += coefficient * self.shared_q[j];
2196                magnitude = next_up_ball(
2197                    magnitude
2198                        + (coefficient * self.center[j]).abs()
2199                        + coefficient.abs() * generator_column_sum[j],
2200                );
2201                radius = next_up_ball(
2202                    radius
2203                        + ball_radius_about_value(map[i][j])
2204                            * (self.center[j].abs() + generator_column_sum[j]),
2205                );
2206            }
2207            next_center[i] = center;
2208            next_shared_q[i] = shared_q;
2209            fresh_radius[i] = next_up_ball(
2210                (radius + ZONOTOPE_ROUNDOFF * magnitude) * (1.0 + 64.0 * f64::EPSILON),
2211            );
2212        }
2213
2214        for generator in self.generators.iter_mut() {
2215            let previous = *generator;
2216            for i in 0..dim {
2217                let mut coordinate = 0.0f64;
2218                for j in 0..dim {
2219                    coordinate += map[i][j].value * previous[j];
2220                }
2221                generator[i] = coordinate;
2222            }
2223        }
2224
2225        self.center = next_center;
2226        self.shared_q = next_shared_q;
2227        for i in 0..dim {
2228            if fresh_radius[i] > 0.0 {
2229                let mut axis = [0.0f64; N];
2230                axis[i] = fresh_radius[i];
2231                self.generators.push(axis);
2232            }
2233        }
2234        self.compact();
2235        self.center[..dim].iter().all(|value| value.is_finite())
2236            && self.shared_q[..dim].iter().all(|value| value.is_finite())
2237            && self
2238                .generators
2239                .iter()
2240                .all(|generator| generator[..dim].iter().all(|value| value.is_finite()))
2241    }
2242
2243    /// Reduce the lowest-correlation generators to one axis-aligned set once
2244    /// the list is over the cap.
2245    ///
2246    /// Age is not a sound proxy for dispensability here.  The order-3 closed
2247    /// loop contracts through a rotation of its dominant directions; after ten
2248    /// nodes an old generator can still be large and strongly non-axis-aligned.
2249    /// Folding it merely because it is old discards precisely that correlation
2250    /// and sends its width through `|M|`, recreating the wrapping effect this
2251    /// zonotope exists to avoid.
2252    ///
2253    /// The reduction score `||g||₁ - ||g||∞` is zero for an axis generator and
2254    /// grows with the correlation that an axis box would discard.  Therefore
2255    /// the lowest-scoring generators are the loss-minimizing ones to fold, and
2256    /// the correlation-bearing generators remain explicit.  The folded box is
2257    /// still an outer zonotope: each coordinate radius is the outward-rounded
2258    /// sum of the folded generators' absolute coordinates.
2259    fn compact(&mut self) {
2260        if self.generators.len() <= ZONOTOPE_GENERATOR_CAP {
2261            return;
2262        }
2263        let dim = self.dim;
2264        let reduction_score = |generator: &[f64; N]| {
2265            let mut l1 = 0.0_f64;
2266            let mut linf = 0.0_f64;
2267            for &coordinate in generator.iter().take(dim) {
2268                let magnitude = coordinate.abs();
2269                l1 += magnitude;
2270                linf = linf.max(magnitude);
2271            }
2272            (l1 - linf).max(0.0)
2273        };
2274        self.generators
2275            .sort_by(|left, right| reduction_score(left).total_cmp(&reduction_score(right)));
2276        let fold = self.generators.len() - ZONOTOPE_GENERATOR_CAP / 2;
2277        let retained = self.generators.split_off(fold);
2278        let mut folded = [0.0f64; N];
2279        for generator in &self.generators {
2280            for i in 0..dim {
2281                folded[i] = next_up_ball(folded[i] + generator[i].abs());
2282            }
2283        }
2284        let mut next = Vec::with_capacity(retained.len() + dim);
2285        for i in 0..dim {
2286            if folded[i] > 0.0 {
2287                let mut axis = [0.0f64; N];
2288                axis[i] = folded[i];
2289                next.push(axis);
2290            }
2291        }
2292        next.extend(retained);
2293        self.generators = next;
2294    }
2295}
2296
2297/// The identity map over the first `dim` coordinates.
2298fn zonotope_identity_map<const N: usize>(dim: usize) -> [[Ball; N]; N] {
2299    let mut map = [[Ball::ZERO; N]; N];
2300    for (i, row) in map.iter_mut().enumerate().take(dim) {
2301        row[i] = Ball::ONE;
2302    }
2303    map
2304}
2305
2306/// Write an `order × order` block of the stacked MEAN map at block row/column
2307/// `(block_row, block_column)`. The mean layout is `block·order + i`.
2308fn mean_set_block(
2309    map: &mut [[Ball; MEAN_DIM]; MEAN_DIM],
2310    block_row: usize,
2311    block_column: usize,
2312    block: &BallMat,
2313    order: usize,
2314) {
2315    for i in 0..order {
2316        for j in 0..order {
2317            map[block_row * order + i][block_column * order + j] = block[i][j];
2318        }
2319    }
2320}
2321
2322/// The congruence `X ↦ L·X·Rᵀ` as a linear map on `vec(X)`, i.e. `L ⊗ R`.
2323///
2324/// This is what makes `dP` carryable: the derivative's update and prediction
2325/// are congruences by matrices built from the VALUE covariance, which this
2326/// filter already encloses to `1e−12`, so the map's own entries are tight and
2327/// only the state needs the generators. The `vec` layout is `i·order + j`.
2328fn zonotope_congruence_map(
2329    left: &BallMat,
2330    right: &BallMat,
2331    order: usize,
2332) -> [[Ball; COVARIANCE_D1_DIM]; COVARIANCE_D1_DIM] {
2333    let mut map = [[Ball::ZERO; COVARIANCE_D1_DIM]; COVARIANCE_D1_DIM];
2334    for i in 0..order {
2335        for j in 0..order {
2336            for k in 0..order {
2337                for l in 0..order {
2338                    map[i * order + j][k * order + l] = left[i][k].mul(right[j][l]);
2339                }
2340            }
2341        }
2342    }
2343    map
2344}
2345
2346/// Project a matrix-valued zonotope onto the exact symmetric subspace.
2347///
2348/// Covariances and every one of their parameter derivatives are symmetric
2349/// exact-real matrices. If `x` is the witness point in the incoming zonotope,
2350/// then `Sx = x` for the symmetrizer `S`; applying `S` to every generator
2351/// therefore preserves that witness while deleting enclosure directions that
2352/// violate a model identity.
2353fn project_symmetric_zonotope(state: &mut Zonotope<COVARIANCE_D1_DIM>, order: usize) -> bool {
2354    if order == 1 {
2355        return true;
2356    }
2357    let mut projection = [[Ball::ZERO; COVARIANCE_D1_DIM]; COVARIANCE_D1_DIM];
2358    for i in 0..order {
2359        for j in 0..order {
2360            let row = i * order + j;
2361            if i == j {
2362                projection[row][row] = Ball::ONE;
2363            } else {
2364                projection[row][i * order + j] = Ball::exact(0.5);
2365                projection[row][j * order + i] = Ball::exact(0.5);
2366            }
2367        }
2368    }
2369    state.apply(&projection, &[Ball::ZERO; COVARIANCE_D1_DIM])
2370}
2371
2372/// Read `vec(X)` back out as a matrix.
2373fn zonotope_to_matrix(state: &Zonotope<COVARIANCE_D1_DIM>, order: usize) -> BallMat {
2374    let mut out = [[Ball::ZERO; MAX_ORDER]; MAX_ORDER];
2375    for i in 0..order {
2376        for j in 0..order {
2377            out[i][j] = state.coordinate(i * order + j);
2378        }
2379    }
2380    out
2381}
2382
2383/// Apply one scalar-observation Riccati update to a covariance zonotope.
2384///
2385/// Applying the Joseph map with an interval-valued gain is sound but useless:
2386/// the gain is a function of the SAME covariance, while a generic interval
2387/// matrix application ranges the two independently.  Its resulting first-order
2388/// error is fed back into the next gain and recreates componentwise Riccati
2389/// wrapping.
2390///
2391/// Instead linearize the Riccati map at the zonotope centre and enclose its
2392/// exact, signed second-order remainder.  With centre column `c`, centre
2393/// innovation `f`, `k = c/f`, covariance perturbation `E`, observation-variance
2394/// perturbation `dr`, `d = E₀₀ + dr`, and
2395///
2396/// ```text
2397/// u = E e₀ - k d,
2398/// ```
2399///
2400/// direct expansion gives the IDENTITY
2401///
2402/// ```text
2403/// U(C + E, r + dr)
2404///   = A (C + E) Aᵀ + (r + dr) k kᵀ - u uᵀ / (f + d),
2405///   A = I - k e₀ᵀ.
2406/// ```
2407///
2408/// Thus every existing generator follows the contracting derivative map
2409/// `E ↦ AEAᵀ`; only the genuinely quadratic remainder becomes fresh interval
2410/// error.  This preserves the covariance/gain dependency rather than treating
2411/// it as uncertainty.
2412fn covariance_zonotope_measurement_update(
2413    state: &mut Zonotope<COVARIANCE_D1_DIM>,
2414    r: Ball,
2415    order: usize,
2416) -> bool {
2417    let centre_r = Ball::exact(r.value);
2418    let centre_f = Ball::exact(state.center[0]).add(centre_r);
2419    if !(centre_f.is_finite() && centre_f.lo > 0.0) {
2420        return false;
2421    }
2422
2423    let mut centre_gain = [Ball::ZERO; MAX_ORDER];
2424    let mut column_error = [Ball::ZERO; MAX_ORDER];
2425    for i in 0..order {
2426        centre_gain[i] = Ball::exact(state.center[i * order]).div_positive(centre_f);
2427        let coordinate = state.coordinate(i * order);
2428        let radius = ball_radius_about_value(coordinate);
2429        column_error[i] = Ball {
2430            value: 0.0,
2431            lo: -radius,
2432            hi: radius,
2433        };
2434    }
2435    let r_error = Ball {
2436        value: 0.0,
2437        lo: next_down_ball(r.lo - r.value),
2438        hi: next_up_ball(r.hi - r.value),
2439    };
2440    let denominator_error = column_error[0].add(r_error);
2441    let denominator = centre_f.add(denominator_error);
2442    if !(denominator.is_finite() && denominator.lo > 0.0) {
2443        return false;
2444    }
2445
2446    let mut remainder_vector = [Ball::ZERO; MAX_ORDER];
2447    for i in 0..order {
2448        remainder_vector[i] = column_error[i].sub(centre_gain[i].mul(denominator_error));
2449    }
2450    let operator = ball_update_operator(&centre_gain, centre_r.div_positive(centre_f), order);
2451    let mut constant = [Ball::ZERO; COVARIANCE_D1_DIM];
2452    for i in 0..order {
2453        for j in 0..order {
2454            let quadratic_remainder = remainder_vector[i]
2455                .mul(remainder_vector[j])
2456                .div_positive(denominator)
2457                .neg();
2458            constant[i * order + j] = r
2459                .mul(centre_gain[i])
2460                .mul(centre_gain[j])
2461                .add(quadratic_remainder);
2462        }
2463    }
2464    state.apply(
2465        &zonotope_congruence_map(&operator, &operator, order),
2466        &constant,
2467    )
2468}
2469
2470/// Diagonal names for the traced covariance jets, indexed by state coordinate.
2471const D3_DIAGONAL_NAMES: [&str; MAX_ORDER] = ["d3_upd_00", "d3_upd_11", "d3_upd_22"];
2472const D2_DIAGONAL_NAMES: [&str; MAX_ORDER] = ["d2_upd_00", "d2_upd_11", "d2_upd_22"];
2473const D1_DIAGONAL_NAMES: [&str; MAX_ORDER] = ["d1_upd_00", "d1_upd_11", "d1_upd_22"];
2474const P_DIAGONAL_NAMES: [&str; MAX_ORDER] = ["p_upd_00", "p_upd_11", "p_upd_22"];
2475/// Kalman gain coordinates, so a caller can rebuild the closed-loop map.
2476const GAIN_NAMES: [&str; MAX_ORDER] = ["gain_0", "gain_1", "gain_2"];
2477/// Full PREDICTED covariance, so a caller can weigh that map by the matrix the
2478/// Riccati recursion makes a Lyapunov matrix for it.
2479const P_NEXT_ENTRY_NAMES: [[&str; MAX_ORDER]; MAX_ORDER] = [
2480    ["p_next_00", "p_next_01", "p_next_02"],
2481    ["p_next_10", "p_next_11", "p_next_12"],
2482    ["p_next_20", "p_next_21", "p_next_22"],
2483];
2484
2485fn run_filter_ball_traced(
2486    nodes: &[PooledNode],
2487    q: Ball,
2488    order: usize,
2489    mut trace: Option<&mut Vec<BallTraceRecord>>,
2490) -> Result<BallFilterPass, SplineScoreProofError> {
2491    // `(a, a′)` together, `vec(P)` once the covariance becomes proper, and
2492    // `vec(dP/dρ)`, each as a zonotope; see `Zonotope` for why a componentwise
2493    // enclosure of these contracting signed recursions is impossible at any
2494    // width.
2495    let mut mean = Zonotope::<MEAN_DIM>::zeroed(MEAN_BLOCKS * order);
2496    // Start at the exact zero proper covariance and carry it even while the
2497    // diffuse rank is being consumed. Seeding only after the diffuse phase
2498    // would already have boxed the first `order - 1` occurrences of the common
2499    // process-noise scale, so their correlation could never be recovered.
2500    let mut covariance = Zonotope::<COVARIANCE_D1_DIM>::zeroed(order * order);
2501    let mut covariance_d1 = Zonotope::<COVARIANCE_D1_DIM>::zeroed(order * order);
2502    let mut a_d2: BallVec = [Ball::ZERO; MAX_ORDER];
2503    let mut a_d3: BallVec = [Ball::ZERO; MAX_ORDER];
2504    let mut p_star: BallMat = [[Ball::ZERO; MAX_ORDER]; MAX_ORDER];
2505    let mut p_star_d2: BallMat = [[Ball::ZERO; MAX_ORDER]; MAX_ORDER];
2506    let mut p_star_d3: BallMat = [[Ball::ZERO; MAX_ORDER]; MAX_ORDER];
2507    let mut p_inf: BallMat = [[Ball::ZERO; MAX_ORDER]; MAX_ORDER];
2508    for i in 0..order {
2509        p_inf[i][i] = Ball::ONE;
2510    }
2511    let mut diffuse_rank = order;
2512    // A CARRIED Cholesky factor of the proper covariance, once the diffuse rank
2513    // is consumed and there is a proper covariance to factor. The componentwise
2514    // recursion still runs; this is a second, independent enclosure of the same
2515    // matrix whose update and prediction contain no cancelling subtraction, and
2516    // the two are intersected. `None` means no evidence, never a refusal.
2517    let mut carried_factor: Option<BallMat> = None;
2518    let mut sum_log_f = Ball::ZERO;
2519    let mut sum_log_f_d1 = Ball::ZERO;
2520    let mut sum_log_f_d2 = Ball::ZERO;
2521    let mut sum_log_f_d3 = Ball::ZERO;
2522    let mut sum_v2_over_f = Ball::ZERO;
2523    let mut sum_v2_over_f_d1 = Ball::ZERO;
2524    let mut sum_v2_over_f_d2 = Ball::ZERO;
2525    let mut sum_v2_over_f_d3 = Ball::ZERO;
2526    let mut n_proper = 0usize;
2527    // `Σ w y²` over the prefix — the exact ceiling on the accumulated
2528    // innovations quadratic (see
2529    // [`intersect_first_order_accumulator_exact_ranges`]). Accumulated at EVERY
2530    // node, including the ones that consume diffuse rank, because the quadratic
2531    // it bounds is the restricted form for the whole prefix and not only for the
2532    // nodes that contributed a proper innovation.
2533    let mut weighted_energy = Ball::ZERO;
2534
2535    for t in 0..nodes.len() {
2536        let r = Ball::ONE.div_positive(Ball::exact(nodes[t].w));
2537        weighted_energy = weighted_energy.add(
2538            Ball::exact(nodes[t].y)
2539                .square()
2540                .mul(Ball::exact(nodes[t].w)),
2541        );
2542        let v = Ball::exact(nodes[t].y).sub(mean.coordinate(0));
2543        let v_d1 = mean.coordinate(order).neg();
2544        let v_d2 = a_d2[0].neg();
2545        let v_d3 = a_d3[0].neg();
2546        // `dP/dρ` materialized from its zonotope for this node's consumers.
2547        let p_star_d1 = zonotope_to_matrix(&covariance_d1, order);
2548        let mut m_star: BallVec = [Ball::ZERO; MAX_ORDER];
2549        let mut m_star_d1: BallVec = [Ball::ZERO; MAX_ORDER];
2550        let mut m_star_d2: BallVec = [Ball::ZERO; MAX_ORDER];
2551        let mut m_star_d3: BallVec = [Ball::ZERO; MAX_ORDER];
2552        for i in 0..order {
2553            m_star[i] = p_star[i][0];
2554            m_star_d1[i] = p_star_d1[i][0];
2555            m_star_d2[i] = p_star_d2[i][0];
2556            m_star_d3[i] = p_star_d3[i][0];
2557        }
2558        let mut f_star = m_star[0].add(r);
2559        intersect_innovation_above_observation_variance(&mut f_star, r);
2560        let f_star_d1 = m_star_d1[0];
2561        let f_star_d2 = m_star_d2[0];
2562        let f_star_d3 = m_star_d3[0];
2563
2564        let mut proper_update = diffuse_rank == 0;
2565        if diffuse_rank > 0 {
2566            let mut m_inf: BallVec = [Ball::ZERO; MAX_ORDER];
2567            for i in 0..order {
2568                m_inf[i] = p_inf[i][0];
2569            }
2570            let f_inf = m_inf[0];
2571            if f_inf.lo == 0.0 && f_inf.hi == 0.0 {
2572                // The observation is exactly orthogonal to the remaining
2573                // diffuse subspace. It receives an ordinary proper update
2574                // without consuming diffuse rank.
2575                proper_update = true;
2576            } else {
2577                require_positive_innovation(t, SplineInnovationKind::Diffuse, f_inf)?;
2578            }
2579            if !proper_update {
2580                let inv_f_inf = Ball::ONE.div_positive(f_inf);
2581                let inv_f_inf_sq = inv_f_inf.square();
2582                let mut gain_inf: BallVec = [Ball::ZERO; MAX_ORDER];
2583                for i in 0..order {
2584                    gain_inf[i] = m_inf[i].mul(inv_f_inf);
2585                    a_d2[i] = a_d2[i].add(gain_inf[i].mul(v_d2));
2586                    a_d3[i] = a_d3[i].add(gain_inf[i].mul(v_d3));
2587                }
2588                // `a⁺ = A_inf·a + K_inf·y` and `a′⁺ = A_inf·a′`, with
2589                // `A_inf = I − K_inf e₀ᵀ`: the diffuse update is the same
2590                // operator on both blocks and the jet block carries no `y`,
2591                // because `v′ = −a′₀` has no data term to pick up.
2592                // `K_inf[0] = M_inf[0]/F_inf = 1` identically, since `F_inf` IS
2593                // `M_inf[0]`, so the operator's own diagonal entry is an exact
2594                // zero rather than a `1 − K₀` subtraction.
2595                let a_inf = ball_update_operator(&gain_inf, Ball::ZERO, order);
2596                let mut diffuse_map = zonotope_identity_map::<MEAN_DIM>(MEAN_BLOCKS * order);
2597                mean_set_block(&mut diffuse_map, 0, 0, &a_inf, order);
2598                mean_set_block(&mut diffuse_map, 1, 1, &a_inf, order);
2599                let mut diffuse_constant = [Ball::ZERO; MEAN_DIM];
2600                let y_node = Ball::exact(nodes[t].y);
2601                for i in 0..order {
2602                    diffuse_constant[i] = gain_inf[i].mul(y_node);
2603                }
2604                if !mean.apply(&diffuse_map, &diffuse_constant) {
2605                    return Err(SplineScoreProofError::InvalidArithmetic {
2606                        context: "diffuse mean zonotope",
2607                    });
2608                }
2609                let mut p_new = p_star;
2610                let mut p_new_d2 = p_star_d2;
2611                let mut p_new_d3 = p_star_d3;
2612                for i in 0..order {
2613                    for j in 0..order {
2614                        let inf_product = m_inf[i].mul(m_inf[j]);
2615                        let subtract_left = m_inf[i].mul(m_star[j]).mul(inv_f_inf);
2616                        let subtract_right = m_star[i].mul(m_inf[j]).mul(inv_f_inf);
2617                        let add_star = inf_product.mul(f_star).mul(inv_f_inf_sq);
2618                        p_new[i][j] = p_new[i][j]
2619                            .sub(subtract_left)
2620                            .sub(subtract_right)
2621                            .add(add_star);
2622
2623                        let subtract_left_d2 = m_inf[i].mul(m_star_d2[j]).mul(inv_f_inf);
2624                        let subtract_right_d2 = m_star_d2[i].mul(m_inf[j]).mul(inv_f_inf);
2625                        p_new_d2[i][j] = p_new_d2[i][j]
2626                            .sub(subtract_left_d2)
2627                            .sub(subtract_right_d2)
2628                            .add(inf_product.mul(f_star_d2).mul(inv_f_inf_sq));
2629
2630                        let subtract_left_d3 = m_inf[i].mul(m_star_d3[j]).mul(inv_f_inf);
2631                        let subtract_right_d3 = m_star_d3[i].mul(m_inf[j]).mul(inv_f_inf);
2632                        p_new_d3[i][j] = p_new_d3[i][j]
2633                            .sub(subtract_left_d3)
2634                            .sub(subtract_right_d3)
2635                            .add(inf_product.mul(f_star_d3).mul(inv_f_inf_sq));
2636                    }
2637                }
2638                // The diffuse covariance update is the fixed-gain Joseph map
2639                //
2640                //     P⁺ = A_inf P A_infᵀ + r K_inf K_infᵀ.
2641                //
2642                // It is affine in the proper covariance, so the value
2643                // zonotope can and must follow it from the exact zero state.
2644                // Waiting until diffuse rank reaches zero would box the first
2645                // process-noise injections before the shared-q generator even
2646                // existed.
2647                let mut covariance_constant = [Ball::ZERO; COVARIANCE_D1_DIM];
2648                for i in 0..order {
2649                    for j in 0..order {
2650                        covariance_constant[i * order + j] = r.mul(gain_inf[i]).mul(gain_inf[j]);
2651                    }
2652                }
2653                if !covariance.apply(
2654                    &zonotope_congruence_map(&a_inf, &a_inf, order),
2655                    &covariance_constant,
2656                ) || !project_symmetric_zonotope(&mut covariance, order)
2657                {
2658                    return Err(SplineScoreProofError::InvalidArithmetic {
2659                        context: "diffuse covariance zonotope",
2660                    });
2661                }
2662                let covariance_p_new = zonotope_to_matrix(&covariance, order);
2663                for i in 0..order {
2664                    for j in 0..order {
2665                        intersect_with_independent_enclosure(
2666                            &mut p_new[i][j],
2667                            covariance_p_new[i][j],
2668                        );
2669                    }
2670                }
2671                // `D1+ = A_inf*D1*A_inf'` EXACTLY: expanding that congruence
2672                // gives `D1[i][j] - c_j D1[i][0] - c_i D1[0][j] + c_i c_j D1[0][0]`
2673                // with `c = M_inf/F_inf`, which is the diffuse update term for
2674                // term. The other jets keep the expanded form because they are
2675                // not carried as zonotopes.
2676                if !covariance_d1.apply(
2677                    &zonotope_congruence_map(&a_inf, &a_inf, order),
2678                    &[Ball::ZERO; COVARIANCE_D1_DIM],
2679                ) || !project_symmetric_zonotope(&mut covariance_d1, order)
2680                {
2681                    return Err(SplineScoreProofError::InvalidArithmetic {
2682                        context: "diffuse covariance-derivative zonotope",
2683                    });
2684                }
2685                p_star = p_new;
2686                p_star_d2 = p_new_d2;
2687                p_star_d3 = p_new_d3;
2688                ball_symmetrize(&mut p_star, order);
2689                ball_symmetrize(&mut p_star_d2, order);
2690                ball_symmetrize(&mut p_star_d3, order);
2691                for i in 0..order {
2692                    for j in 0..order {
2693                        p_inf[i][j] = p_inf[i][j].sub(m_inf[i].mul(m_inf[j]).mul(inv_f_inf));
2694                    }
2695                }
2696                ball_symmetrize(&mut p_inf, order);
2697                diffuse_rank -= 1;
2698                if diffuse_rank == 0 {
2699                    p_inf = [[Ball::ZERO; MAX_ORDER]; MAX_ORDER];
2700                    intersect_proper_covariance_psd(&mut p_star, order)?;
2701                    carried_factor = ball_cholesky(&p_star, order);
2702                }
2703            }
2704        }
2705
2706        if proper_update {
2707            require_positive_innovation(t, SplineInnovationKind::Proper, f_star)?;
2708            let inv_f = Ball::ONE.div_positive(f_star);
2709            let mut gain = [Ball::ZERO; MAX_ORDER];
2710            for i in 0..order {
2711                gain[i] = m_star[i].mul(inv_f);
2712            }
2713            // The VALUE covariance through the same Joseph form as its jets.
2714            //
2715            // `P⁺ = P − M Mᵀ/F` is the textbook update and it is a subtraction
2716            // of near-equal quantities exactly where this filter lives. At
2717            // `ρ = −24` on the #2300 nodes the predicted `P₀₀` is `1.84e5` and
2718            // `M₀²/F` is `1.84e5`; their difference is `1.0`, so the enclosure
2719            // of a quantity of size one is charged the rounding of quantities
2720            // `1.8e5` times larger, and the ratio is `F/R = P₀₀/R + 1`, which
2721            // GROWS with the process noise.
2722            //
2723            // The Joseph form `P⁺ = A P Aᵀ + R K Kᵀ` (`A = I − K e₀ᵀ`) is the
2724            // same quantity, and at the observed coordinate it is
2725            //
2726            //     (A P Aᵀ)₀₀ + R K₀² = (R/F)²P₀₀ + R P₀₀²/F²
2727            //                        = P₀₀ R (R + P₀₀)/F² = P₀₀ R/F,
2728            //
2729            // a sum of two POSITIVE terms — the exact `P⁺₀₀`, with no
2730            // subtraction anywhere in it. `A[0][0] = R/F` is itself formed
2731            // exactly rather than as `1 − K₀` (see `ball_update_operator`).
2732            //
2733            // The measurement that forced this (#2614, per-node trace at
2734            // `ρ = −24`, order 2). `P⁺₀₀` holds the VALUE `0.99999456` at every
2735            // node while its enclosure WIDTH runs `1.6e-1, 2.3e0, 3.1e1, 3.8e2,
2736            // 4.7e3, 5.9e4, 9.2e5` over nodes 6..12 — a factor of ~13 per node,
2737            // on a quantity of size one. `F` inherits it, so `inv_f`'s enclosure
2738            // width reaches `1.0` at node 13 against a value of `5.4e-6`; at
2739            // that node `A[0][0]` stops being a contraction and EVERY derivative
2740            // jet leaves the finite range in a single step (`d3⁺₀₀` width
2741            // `1.7e-3 → 4.5e19`). The `d3` recursion was never the defect: its
2742            // own congruence contracts by `(R/F)² ≈ 3e-11` per node exactly as
2743            // intended, and its VALUES are bit-stable throughout. It diverges
2744            // because the value covariance it multiplies by had already lost its
2745            // enclosure.
2746            let a_operator = ball_update_operator(&gain, r.mul(inv_f), order);
2747            let mut component_p_new = ball_congruence(&a_operator, &p_star, &a_operator, order);
2748            for i in 0..order {
2749                for j in 0..order {
2750                    component_p_new[i][j] = component_p_new[i][j].add(gain[i].mul(gain[j]).mul(r));
2751                }
2752            }
2753            // Carry the covariance through the Riccati map's exact centred
2754            // Taylor form. Its linear part is the signed Joseph congruence and
2755            // its remainder is quadratic in the covariance radius; see
2756            // `covariance_zonotope_measurement_update`.
2757            if !covariance_zonotope_measurement_update(&mut covariance, r, order)
2758                || !project_symmetric_zonotope(&mut covariance, order)
2759            {
2760                return Err(SplineScoreProofError::InvalidArithmetic {
2761                    context: "proper covariance zonotope update",
2762                });
2763            }
2764            let mut p_new = zonotope_to_matrix(&covariance, order);
2765            for i in 0..order {
2766                for j in 0..order {
2767                    intersect_with_independent_enclosure(&mut p_new[i][j], component_p_new[i][j]);
2768                }
2769            }
2770            // ... and then tightened to the EXACT range of the map each entry
2771            // is, which the componentwise evaluation above cannot see because
2772            // `A`, `K` and the middle factor all carry the same `P₀₀`.
2773            for i in 0..order {
2774                for j in 0..order {
2775                    if i == j {
2776                        // On the DIAGONAL the subtracted term is `M[i]²/F`, a
2777                        // SQUARE. The general corner form ranges `P[i][0]` and
2778                        // `P[0][j]` independently, so when that entry straddles
2779                        // zero it not only doubles the width but admits a
2780                        // NEGATIVE product — an upper bound larger than
2781                        // `P[i][i]` itself — and its monotonicity argument then
2782                        // bails out entirely. `Ball::square` returns `[0, max²]`
2783                        // for a straddling interval, which is the exact range.
2784                        intersect_with_independent_enclosure(
2785                            &mut p_new[i][i],
2786                            p_star[i][i].sub(m_star[i].square().div_positive(f_star)),
2787                        );
2788                    }
2789                    intersect_updated_covariance_exact_range(
2790                        &mut p_new[i][j],
2791                        p_star[i][j],
2792                        m_star[i],
2793                        m_star[j],
2794                        m_star[0],
2795                        r,
2796                    );
2797                }
2798            }
2799            // The first column of the UPDATED covariance is EXACTLY `R·K`, and
2800            // the file already relies on that identity for every derivative
2801            // gain: `M⁺ = P⁺e₀ = M − M·P₀₀/F = M·(F − P₀₀)/F = M·R/F = R·K`.
2802            // The VALUE covariance was not using it. `P⁺[0][j] = P[0][j]·R/F`
2803            // is a PRODUCT — no subtraction, and no shared variable appearing
2804            // twice, which is what the corner form above cannot exploit: it
2805            // ranges `P[0][j]` as both `a` and `c` independently even though
2806            // they are one entry, and so cannot see the factorization at all.
2807            //
2808            // Measured at order 2, ρ = −13.8411 (the log-lambda the #2300
2809            // search refuses at), node 31: `P⁺₀₁` carries value `41.56` with
2810            // width `1.06e3`, and `P⁺₁₁` value `1.14e4` with width `3.55e8`,
2811            // while `P⁺₀₀` — the one entry that already had its exact range —
2812            // is `0.92 ± 0.99`. The unobserved entries are the runaway, and the
2813            // transition feeds them straight back into the observed one as
2814            // `P₀₀ + 2δP₀₁ + δ²P₁₁`.
2815            for i in 0..order {
2816                let exact = gain[i].mul(r);
2817                intersect_with_independent_enclosure(&mut p_new[i][0], exact);
2818                if i != 0 {
2819                    intersect_with_independent_enclosure(&mut p_new[0][i], exact);
2820                }
2821            }
2822            intersect_observed_covariance_exact_range(&mut p_new[0][0], m_star[0], r);
2823            // An update can only remove variance: `P⁺ = P⁻ − M Mᵀ/F ⪯ P⁻`, so
2824            // every diagonal is bounded above by the one it came from. The
2825            // corner form above cannot see this, because it ranges `P[i][0]`
2826            // and `P[0][j]` independently even when they are the same entry.
2827            for i in 0..order {
2828                let ceiling = p_star[i][i].hi.max(p_new[i][i].value);
2829                if p_new[i][i].hi > ceiling {
2830                    p_new[i][i].hi = ceiling;
2831                }
2832            }
2833            intersect_covariance_minors(&mut p_new, order);
2834            // The same update through the CARRIED factor, where it is one
2835            // column scaling and contains no subtraction at all.
2836            if carried_factor.is_none() {
2837                carried_factor = ball_cholesky(&p_star, order);
2838            }
2839            if let (Some(factor), Some(beta)) = (carried_factor, ball_sqrt(r.mul(inv_f))) {
2840                let updated_factor = ball_factor_update(&factor, beta, order);
2841                let gram = ball_factor_gram(&updated_factor, order);
2842                for i in 0..order {
2843                    for j in 0..order {
2844                        intersect_with_independent_enclosure(&mut p_new[i][j], gram[i][j]);
2845                    }
2846                }
2847                carried_factor = Some(updated_factor);
2848            } else {
2849                carried_factor = None;
2850            }
2851
2852            // Derivative covariances through the JOSEPH form, which for the
2853            // derivative jets is not a reformulation but an exact cancellation
2854            // (#2614).
2855            //
2856            // Writing `A = I − K e₀ᵀ`, the update is `P⁺ = A P` and the Joseph
2857            // form `P⁺ = A P Aᵀ + R K Kᵀ` is algebraically identical. `R` is
2858            // `1/wₜ` — data, not a function of ρ — so differentiating the
2859            // Joseph form gives
2860            //
2861            //     dP⁺ = A dP Aᵀ + dK·(R Kᵀ − M⁺ᵀ) + (R K − M⁺)·dKᵀ
2862            //
2863            // and the first column of the UPDATED covariance is
2864            //
2865            //     M⁺ = P⁺e₀ = M − M·P₀₀/F = M·(F − P₀₀)/F = M·R/F = R·K
2866            //
2867            // so `R K − M⁺ = 0` EXACTLY and both `dK` terms vanish:
2868            //
2869            //     dP⁺ = A · dP · Aᵀ
2870            //
2871            // The first derivative is a pure congruence of the derivative, and
2872            // `dK` does not enter it at all. That matters here because the
2873            // subtractive form this replaces built each `s_k` from products of
2874            // derivative quantities divided by `F`, so an interval evaluation
2875            // widened multiplicatively at every one of the ~180 updates until
2876            // the accumulators saturated at `[−∞, +∞]` while their centres
2877            // stayed exact. The congruence enters the derivative LINEARLY with
2878            // value-side coefficients, and in the scalar case reads
2879            // `dP⁺/dρ = (R/F)²·dP⁻/dρ` with `0 < R/F < 1` — a contraction where
2880            // the old form accumulated a cancelling difference.
2881            //
2882            // Nothing here is a bound, a tolerance or a restored invariant: it
2883            // is the same quantity through an expression that does not cancel,
2884            // so it cannot make a non-stationary point certify.
2885            let d1_pred = p_star_d1;
2886            let d2_pred = p_star_d2;
2887            let d3_pred = p_star_d3;
2888            // Gain jets from the UPDATED covariance, not from a subtractive
2889            // recursion (#2614, second measurement).
2890            //
2891            // The identity that made `dP+ = A dP A^T` exact gives the gain
2892            // derivatives for free. `M+ = R*K` with `R = 1/w_t` constant in rho,
2893            // so `K = M+/R` and
2894            //
2895            //     d^k K / drho^k  =  (d^k M+ / drho^k) / R
2896            //
2897            // Every gain jet is a column of the corresponding UPDATED covariance
2898            // jet divided by a constant: no subtraction, no division by `F`, no
2899            // accumulation of derivative-times-derivative products.
2900            //
2901            // The measurement that forced this. After the covariance jets became
2902            // congruences, the refusal -- now carrying its own evidence -- named
2903            // the first accumulator to leave the finite range as
2904            // `sum_v2_over_f_d3`, at node 63 of 179, q = 1.641e7, value
2905            // 6.766e-5, enclosure [-inf, +inf]. That accumulator is fed by the
2906            // MEAN derivative chain `a_d3 -> v_d3 -> vv_d3`, which consumes
2907            // `gain_d3` -- the one recursion the congruence rewrite left alone,
2908            // still built as three subtractions of derivative products divided
2909            // by `F`, once per node.
2910            //
2911            // The staging is well founded, not circular: `d1` needs only `gain`;
2912            // `gain_d1` is then `p_new_d1`'s first column; `d2` needs `gain_d1`;
2913            // and so on. Each jet exists exactly when the next one needs it,
2914            // which is also why the mean update now follows this block.
2915            // `A[0][0] = 1 − K₀ = R/F` EXACTLY — never `1 − K₀` as a
2916            // subtraction. See `ball_update_operator`: that single entry is
2917            // where the `d3` jet was losing everything, and the measurement
2918            // that found it is on #2614 (`F'''` at `+/-4.1e247` by node 62,
2919            // ~`10^4` per node). The same operator carries the VALUE covariance
2920            // above, for the same reason.
2921            //
2922            // That measurement also reported `d1` and `d2` as clean, and this
2923            // comment used to say so without qualification. THAT IS FALSE as a
2924            // general statement, and it stood long enough to send a later lane
2925            // looking inside the `d3` recursion for a defect that was never
2926            // there. It was a reading at ONE rho. Measured at `rho = -13.8411`
2927            // on the same nodes at order 2, the per-node enclosure WIDTHS over
2928            // nodes 40..43 are
2929            //
2930            //     d1: 1.36e46  -> 2.34e49  -> 4.43e52   -> 9.18e55    (x1.7e3)
2931            //     d2: 1.06e93  -> 3.12e99  -> 1.11e106  -> 4.80e112   (x3e6)
2932            //     d3: 1.24e140 -> 6.23e149 -> 4.21e159  -> 3.76e169   (x6e9)
2933            //
2934            // — exponents exactly 1 : 2 : 3, so `d3` is `d1`'s growth CUBED and
2935            // not a defect of its own. At node 40 the congruence `A D3 Aᵀ` is
2936            // 5.17e133 against a predicted `d3` of 1.68e135, i.e. it CONTRACTS
2937            // by 32x, while the two terms carrying `D1` are the large ones.
2938            // Every value is stable to 15 digits throughout. The repair that
2939            // followed is the carried factor below, not anything in this block.
2940
2941            // dP⁺ = A · dP · Aᵀ  (the `dK` terms cancel exactly, since M⁺ = R·K)
2942            //
2943            // Applied to the ZONOTOPE, not to a matrix of intervals. This is
2944            // the same recursion the carried `−dP` Cholesky factor used to
2945            // guard, and the factor is gone with it: a factor can only be
2946            // re-seeded from an enclosure that still PROVES `−dP ⪰ 0`, so once
2947            // the componentwise widths grew past that it could not come back,
2948            // which is exactly the window the trace shows (`w(dP₁₁)` 1.4e−2 at
2949            // node 40, 7.5e2 at node 80, back to 4e−9 at node 160 when a
2950            // re-seed finally succeeded). A zonotope needs no re-seeding
2951            // because it never loses the structure in the first place.
2952            if !covariance_d1.apply(
2953                &zonotope_congruence_map(&a_operator, &a_operator, order),
2954                &[Ball::ZERO; COVARIANCE_D1_DIM],
2955            ) || !project_symmetric_zonotope(&mut covariance_d1, order)
2956            {
2957                return Err(SplineScoreProofError::InvalidArithmetic {
2958                    context: "covariance-derivative zonotope update",
2959                });
2960            }
2961            let mut p_new_d1 = zonotope_to_matrix(&covariance_d1, order);
2962            // `0 ⪯ −dP/dρ ⪯ P`, applied to the MATERIALIZED view the consumers
2963            // below read rather than to the zonotope that carries it: the bound
2964            // is a fact about the matrix, the zonotope is a representation of
2965            // it, and every consumer here — `gain_d1`, and through it every
2966            // higher jet — reads this matrix.
2967            //
2968            // Only once the diffuse rank is consumed: until then `P*` is the
2969            // proper PART of a two-matrix decomposition and not the filtered
2970            // covariance the concavity argument is about.
2971            if diffuse_rank == 0 {
2972                intersect_derivative_covariance_below_its_own_covariance(
2973                    &mut p_new_d1,
2974                    &p_new,
2975                    order,
2976                );
2977            }
2978            let mut gain_d1 = [Ball::ZERO; MAX_ORDER];
2979            for i in 0..order {
2980                gain_d1[i] = p_new_d1[i][0].div_positive(r);
2981            }
2982            let a_d1_operator = ball_update_operator_derivative(&gain_d1, order);
2983
2984            // d²P⁺ = A D₂ Aᵀ + A′D₁Aᵀ + A D₁A′ᵀ
2985            let mut p_new_d2 = ball_congruence(&a_operator, &d2_pred, &a_operator, order);
2986            let d2_cross = ball_congruence(&a_d1_operator, &d1_pred, &a_operator, order);
2987            for i in 0..order {
2988                for j in 0..order {
2989                    p_new_d2[i][j] = p_new_d2[i][j].add(d2_cross[i][j]).add(d2_cross[j][i]);
2990                }
2991            }
2992            let mut gain_d2 = [Ball::ZERO; MAX_ORDER];
2993            for i in 0..order {
2994                gain_d2[i] = p_new_d2[i][0].div_positive(r);
2995            }
2996            let a_d2_operator = ball_update_operator_derivative(&gain_d2, order);
2997
2998            // d³P⁺ = A D₃ Aᵀ + A″D₁Aᵀ + A D₁A″ᵀ + 2A′D₂Aᵀ + 2A D₂A′ᵀ + 2A′D₁A′ᵀ
2999            let d3_congruence = ball_congruence(&a_operator, &d3_pred, &a_operator, order);
3000            let mut p_new_d3 = d3_congruence;
3001            let d3_second = ball_congruence(&a_d2_operator, &d1_pred, &a_operator, order);
3002            let d3_first = ball_congruence(&a_d1_operator, &d2_pred, &a_operator, order);
3003            let d3_both = ball_congruence(&a_d1_operator, &d1_pred, &a_d1_operator, order);
3004            for i in 0..order {
3005                for j in 0..order {
3006                    p_new_d3[i][j] = p_new_d3[i][j]
3007                        .add(d3_second[i][j])
3008                        .add(d3_second[j][i])
3009                        .add(d3_first[i][j].scale(2.0))
3010                        .add(d3_first[j][i].scale(2.0))
3011                        .add(d3_both[i][j].scale(2.0));
3012                }
3013            }
3014            let mut gain_d3 = [Ball::ZERO; MAX_ORDER];
3015            for i in 0..order {
3016                gain_d3[i] = p_new_d3[i][0].div_positive(r);
3017            }
3018
3019            if let Some(sink) = trace.as_mut() {
3020                for record in [
3021                    ("f_star", f_star),
3022                    ("inv_f", inv_f),
3023                    ("a_operator_00", a_operator[0][0]),
3024                    ("gain_d1_0", gain_d1[0]),
3025                    ("gain_d2_0", gain_d2[0]),
3026                    ("gain_d3_0", gain_d3[0]),
3027                    ("d3_pred_00", d3_pred[0][0]),
3028                    ("d3_congruence_00", d3_congruence[0][0]),
3029                    ("d3_term_a2_d1_at", d3_second[0][0]),
3030                    ("d3_term_a1_d2_at", d3_first[0][0]),
3031                    ("d3_term_a1_d1_a1t", d3_both[0][0]),
3032                ] {
3033                    sink.push((t, record.0, record.1));
3034                }
3035                if order > 1 {
3036                    sink.push((t, "p_upd_01", p_new[0][1]));
3037                    sink.push((t, "d1_upd_01", p_new_d1[0][1]));
3038                    sink.push((t, "d2_upd_01", p_new_d2[0][1]));
3039                    sink.push((t, "d3_upd_01", p_new_d3[0][1]));
3040                }
3041                for i in 0..order {
3042                    sink.push((t, GAIN_NAMES[i], gain[i]));
3043                    sink.push((t, P_DIAGONAL_NAMES[i], p_new[i][i]));
3044                    sink.push((t, D1_DIAGONAL_NAMES[i], p_new_d1[i][i]));
3045                    sink.push((t, D2_DIAGONAL_NAMES[i], p_new_d2[i][i]));
3046                    sink.push((t, D3_DIAGONAL_NAMES[i], p_new_d3[i][i]));
3047                }
3048            }
3049
3050            // Mean update as ONE linear map on the stacked `(a, a′)`.
3051            //
3052            // The jets follow from `a⁺⁽ᵏ⁾ = Σ_j C(k,j)·A⁽ʲ⁾a⁽ᵏ⁻ʲ⁾ + K⁽ᵏ⁾y` with
3053            // `A⁽ʲ⁾ = −K⁽ʲ⁾e₀ᵀ` for `j ≥ 1` and `v⁽ᵐ⁾ = −a⁽ᵐ⁾₀`:
3054            //
3055            //     a⁺    = A a    + K y
3056            //     a⁺′   = A a′   + K′v  = A a′ − K′e₀ᵀa + K′y
3057            //     a⁺″   = A a″   + 2K′v′  + K″v
3058            //     a⁺‴   = A a‴   + 3K′v″  + 3K″v′ + K‴v
3059            //
3060            // The first two are BLOCK LOWER-TRIANGULAR in `(a, a′)` — `a` never
3061            // reads its own jet — which is what lets one zonotope carry both.
3062            //
3063            // `64778c4e0` wrote the first line for every row and was corrected
3064            // by a row split, because under BOX arithmetic `A a + K y` costs
3065            // `(|a₀| + |y|)·w(K_i)` on rows `i ≥ 1` where the innovation form
3066            // `a_i + K_i v` costs only `|v|·w(K_i)`. Under affine arithmetic
3067            // that distinction is gone: the two are the same linear map, and a
3068            // zonotope applies a linear map exactly, so writing it as a matrix
3069            // costs nothing and is what the generators need. This is NOT a
3070            // reinstatement of that commit's claim — its form is used because
3071            // the enclosure it was wrong about is no longer a box.
3072            let y_node = Ball::exact(nodes[t].y);
3073            let mut update_map = zonotope_identity_map::<MEAN_DIM>(MEAN_BLOCKS * order);
3074            mean_set_block(&mut update_map, 0, 0, &a_operator, order);
3075            mean_set_block(&mut update_map, 1, 1, &a_operator, order);
3076            mean_set_block(&mut update_map, 1, 0, &a_d1_operator, order);
3077            let mut update_constant = [Ball::ZERO; MEAN_DIM];
3078            for i in 0..order {
3079                update_constant[i] = gain[i].mul(y_node);
3080                update_constant[order + i] = gain_d1[i].mul(y_node);
3081            }
3082            if !mean.apply(&update_map, &update_constant) {
3083                return Err(SplineScoreProofError::InvalidArithmetic {
3084                    context: "proper mean zonotope",
3085                });
3086            }
3087            let a_d2_contracted = ball_mat_vec(&a_operator, &a_d2, order);
3088            let a_d3_contracted = ball_mat_vec(&a_operator, &a_d3, order);
3089            for i in 0..order {
3090                a_d2[i] = a_d2_contracted[i]
3091                    .add(gain_d1[i].mul(v_d1).scale(2.0))
3092                    .add(gain_d2[i].mul(v));
3093                a_d3[i] = a_d3_contracted[i]
3094                    .add(gain_d1[i].mul(v_d2).scale(3.0))
3095                    .add(gain_d2[i].mul(v_d1).scale(3.0))
3096                    .add(gain_d3[i].mul(v));
3097            }
3098
3099            p_star = p_new;
3100            p_star_d2 = p_new_d2;
3101            p_star_d3 = p_new_d3;
3102            ball_symmetrize(&mut p_star, order);
3103            ball_symmetrize(&mut p_star_d2, order);
3104            ball_symmetrize(&mut p_star_d3, order);
3105            intersect_proper_covariance_psd(&mut p_star, order)?;
3106
3107            let vv = v.square();
3108            let vv_d1 = v.mul(v_d1).scale(2.0);
3109            let vv_d2 = v_d1.square().add(v.mul(v_d2)).scale(2.0);
3110            let vv_d3 = v.mul(v_d3).add(v_d1.mul(v_d2).scale(3.0)).scale(2.0);
3111            let logf_d1 = f_star_d1.mul(inv_f);
3112            let logf_d2 = f_star_d2.mul(inv_f).sub(logf_d1.square());
3113            let logf_d3 = f_star_d3
3114                .mul(inv_f)
3115                .sub(f_star_d2.mul(inv_f).mul(logf_d1).scale(3.0))
3116                .add(logf_d1.square().mul(logf_d1).scale(2.0));
3117            sum_log_f = sum_log_f.add(f_star.ln_positive());
3118            sum_log_f_d1 = sum_log_f_d1.add(logf_d1);
3119            sum_log_f_d2 = sum_log_f_d2.add(logf_d2);
3120            sum_log_f_d3 = sum_log_f_d3.add(logf_d3);
3121            let t0 = vv.mul(inv_f);
3122            let t1 = vv_d1.sub(t0.mul(f_star_d1)).mul(inv_f);
3123            let t2 = vv_d2
3124                .sub(t1.mul(f_star_d1).scale(2.0))
3125                .sub(t0.mul(f_star_d2))
3126                .mul(inv_f);
3127            let t3 = vv_d3
3128                .sub(t2.mul(f_star_d1).scale(3.0))
3129                .sub(t1.mul(f_star_d2).scale(3.0))
3130                .sub(t0.mul(f_star_d3))
3131                .mul(inv_f);
3132            sum_v2_over_f = sum_v2_over_f.add(t0);
3133            sum_v2_over_f_d1 = sum_v2_over_f_d1.add(t1);
3134            sum_v2_over_f_d2 = sum_v2_over_f_d2.add(t2);
3135            sum_v2_over_f_d3 = sum_v2_over_f_d3.add(t3);
3136            n_proper += 1;
3137            if diffuse_rank == 0 {
3138                intersect_first_order_accumulator_exact_ranges(
3139                    &mut sum_v2_over_f,
3140                    &mut sum_v2_over_f_d1,
3141                    &mut sum_log_f_d1,
3142                    weighted_energy,
3143                    n_proper,
3144                );
3145            }
3146            if let Some(sink) = trace.as_mut() {
3147                for i in 0..order {
3148                    sink.push((t, GAIN_NAMES[i], gain[i]));
3149                }
3150                for record in [
3151                    ("mean_a0", mean.coordinate(0)),
3152                    ("mean_a0_d1", mean.coordinate(MAX_ORDER)),
3153                    ("innovation_v", v),
3154                    ("innovation_v_d1", v_d1),
3155                    ("logf_d1", logf_d1),
3156                    ("term_t0", t0),
3157                    ("term_t1", t1),
3158                    ("acc_sum_log_f", sum_log_f),
3159                    ("acc_sum_log_f_d1", sum_log_f_d1),
3160                    ("acc_sum_v2", sum_v2_over_f),
3161                    ("acc_sum_v2_d1", sum_v2_over_f_d1),
3162                ] {
3163                    sink.push((t, record.0, record.1));
3164                }
3165            }
3166            // Refuse AT the node that diverged, not at the end of the pass.
3167            //
3168            // The end-of-pass check below reports that some accumulator is
3169            // non-finite and nothing more, which is what made #2614 expensive:
3170            // two exact repairs were aimed at the wrong term because the
3171            // refusal could not say WHICH accumulator went, WHERE, or how wide
3172            // it was. Checking here costs eight `is_finite` calls per proper
3173            // node and turns the refusal into the measurement.
3174            if let Some((accumulator, ball, contribution)) = [
3175                ("sum_log_f", sum_log_f, f_star.ln_positive()),
3176                ("sum_log_f_d1", sum_log_f_d1, logf_d1),
3177                ("sum_v2_over_f", sum_v2_over_f, t0),
3178                ("sum_v2_over_f_d1", sum_v2_over_f_d1, t1),
3179                // Second and third order last, and deliberately outside the
3180                // scan below: each has a closed-form global bound the
3181                // certificate substitutes, so neither can justify discarding a
3182                // value and slope that are finite.
3183                ("sum_log_f_d2", sum_log_f_d2, logf_d2),
3184                ("sum_v2_over_f_d2", sum_v2_over_f_d2, t2),
3185                ("sum_log_f_d3", sum_log_f_d3, logf_d3),
3186                ("sum_v2_over_f_d3", sum_v2_over_f_d3, t3),
3187            ]
3188            .into_iter()
3189            .take(GLOBALLY_BOUNDED_FROM)
3190            .find(|(_, ball, _)| !ball.is_finite())
3191            {
3192                return Err(SplineScoreProofError::AccumulatorDiverged {
3193                    node: t,
3194                    n_proper,
3195                    accumulator,
3196                    value: ball.value,
3197                    lo: ball.lo,
3198                    hi: ball.hi,
3199                    q_value: q.value,
3200                    contribution_lo: contribution.lo,
3201                    contribution_hi: contribution.hi,
3202                    f_star_d3_lo: f_star_d3.lo,
3203                    f_star_d3_hi: f_star_d3.hi,
3204                    updated_d3_lo: p_star_d3[0][0].lo,
3205                    updated_d3_hi: p_star_d3[0][0].hi,
3206                });
3207            }
3208        }
3209
3210        if t + 1 < nodes.len() {
3211            let delta = Ball::exact(nodes[t + 1].x).sub(Ball::exact(nodes[t].x));
3212            let f_t = ball_transition(delta, order);
3213            // The transition is block diagonal on the stacked mean: every jet
3214            // is transported by the same `T`, since `T` does not depend on ρ.
3215            let mut transition_map = zonotope_identity_map::<MEAN_DIM>(MEAN_BLOCKS * order);
3216            mean_set_block(&mut transition_map, 0, 0, &f_t, order);
3217            mean_set_block(&mut transition_map, 1, 1, &f_t, order);
3218            if !mean.apply(&transition_map, &[Ball::ZERO; MEAN_DIM]) {
3219                return Err(SplineScoreProofError::InvalidArithmetic {
3220                    context: "mean zonotope transition",
3221                });
3222            }
3223            a_d2 = ball_mat_vec(&f_t, &a_d2, order);
3224            a_d3 = ball_mat_vec(&f_t, &a_d3, order);
3225            let f_t_t = ball_mat_t(&f_t, order);
3226            let ProcessNoiseTaylor {
3227                enclosure: q_noise,
3228                constant: q_noise_constant,
3229                shared_q: q_noise_shared_q,
3230            } = ball_process_noise_taylor(delta, q, order);
3231            let component_p_next = ball_mat_add(
3232                &ball_mat_mul(&ball_mat_mul(&f_t, &p_star, order), &f_t_t, order),
3233                &q_noise,
3234                order,
3235            );
3236            if !covariance.apply_with_shared_q(
3237                &zonotope_congruence_map(&f_t, &f_t, order),
3238                &q_noise_constant,
3239                &q_noise_shared_q,
3240            ) || !project_symmetric_zonotope(&mut covariance, order)
3241            {
3242                return Err(SplineScoreProofError::InvalidArithmetic {
3243                    context: "proper covariance zonotope transition",
3244                });
3245            }
3246            let mut p_next = zonotope_to_matrix(&covariance, order);
3247            for i in 0..order {
3248                for j in 0..order {
3249                    intersect_with_independent_enclosure(&mut p_next[i][j], component_p_next[i][j]);
3250                }
3251            }
3252            let mut p_next_d2 = ball_mat_add(
3253                &ball_mat_mul(&ball_mat_mul(&f_t, &p_star_d2, order), &f_t_t, order),
3254                &q_noise,
3255                order,
3256            );
3257            let mut p_next_d3 = ball_mat_sub(
3258                &ball_mat_mul(&ball_mat_mul(&f_t, &p_star_d3, order), &f_t_t, order),
3259                &q_noise,
3260                order,
3261            );
3262            // `dP⁻ = F·dP·Fᵀ − Q`, since `dQ/dρ = −Q`: a congruence plus an
3263            // EXACT constant, so the whole `dP` recursion is affine in `dP`
3264            // with coefficients built from the value covariance.
3265            let mut prediction_constant = [Ball::ZERO; COVARIANCE_D1_DIM];
3266            let mut prediction_shared_q = [0.0_f64; COVARIANCE_D1_DIM];
3267            for i in 0..order {
3268                for j in 0..order {
3269                    let index = i * order + j;
3270                    prediction_constant[index] = q_noise_constant[index].neg();
3271                    prediction_shared_q[index] = -q_noise_shared_q[index];
3272                }
3273            }
3274            if !covariance_d1.apply_with_shared_q(
3275                &zonotope_congruence_map(&f_t, &f_t, order),
3276                &prediction_constant,
3277                &prediction_shared_q,
3278            ) || !project_symmetric_zonotope(&mut covariance_d1, order)
3279            {
3280                return Err(SplineScoreProofError::InvalidArithmetic {
3281                    context: "covariance-derivative zonotope transition",
3282                });
3283            }
3284            ball_symmetrize(&mut p_next, order);
3285            ball_symmetrize(&mut p_next_d2, order);
3286            ball_symmetrize(&mut p_next_d3, order);
3287            // NOTE: `0 ⪯ −dP/dρ ⪯ P` is applied at the UPDATE, where the
3288            // derivative covariance is materialized from its zonotope, and not
3289            // here. Across the prediction the zonotope carries `dP/dρ` in
3290            // factored form and never forms the matrix, which is the whole
3291            // point of carrying it that way; the bound is a fact about the
3292            // matrix, so it belongs at the materialization its consumers read.
3293            p_star = p_next;
3294            p_star_d2 = p_next_d2;
3295            p_star_d3 = p_next_d3;
3296            if let Some(sink) = trace.as_mut() {
3297                for i in 0..order {
3298                    for j in 0..order {
3299                        sink.push((t, P_NEXT_ENTRY_NAMES[i][j], p_star[i][j]));
3300                    }
3301                }
3302                sink.push((t, "d1_next_00", covariance_d1.coordinate(0)));
3303                sink.push((t, "d2_next_00", p_star_d2[0][0]));
3304                sink.push((t, "d3_next_00", p_star_d3[0][0]));
3305            }
3306            if diffuse_rank > 0 {
3307                let mut pi_next = ball_mat_mul(&ball_mat_mul(&f_t, &p_inf, order), &f_t_t, order);
3308                ball_symmetrize(&mut pi_next, order);
3309                p_inf = pi_next;
3310            } else {
3311                intersect_proper_covariance_psd(&mut p_star, order)?;
3312            }
3313            // Carry the factor across the transition: `P⁻ = (F L)(F L)ᵀ + Q`,
3314            // so the prearray is `[F·L, L_Q]` and re-triangularizing it keeps
3315            // the factor `order`-wide. Nothing here subtracts.
3316            carried_factor = carried_factor.and_then(|factor| {
3317                let transported = ball_mat_mul(&f_t, &factor, order);
3318                let noise_factor = ball_cholesky(&q_noise, order)?;
3319                let mut prearray = [[Ball::ZERO; PREARRAY_COLUMNS]; MAX_ORDER];
3320                for i in 0..order {
3321                    for j in 0..order {
3322                        prearray[i][j] = transported[i][j];
3323                        prearray[i][order + j] = noise_factor[i][j];
3324                    }
3325                }
3326                let (next_factor, trailing, gram_scale) =
3327                    ball_retriangularize(&mut prearray, order, 2 * order);
3328                let gram = ball_factor_gram(&next_factor, order);
3329                // `P⁻ = (L Lᵀ + D)/gram_scale` with `0 ⪯ D` and every entry of
3330                // `D` bounded by `trace(D)`; `gram_scale` is `1 + O(eps)`.
3331                let slack = Ball {
3332                    value: 0.0,
3333                    lo: -trailing,
3334                    hi: trailing,
3335                };
3336                let scale = Ball {
3337                    value: 1.0,
3338                    lo: next_down_ball(1.0 / gram_scale),
3339                    hi: next_up_ball(gram_scale),
3340                };
3341                for i in 0..order {
3342                    for j in 0..order {
3343                        let evidence = gram[i][j].add(slack).mul(scale);
3344                        intersect_with_independent_enclosure(&mut p_star[i][j], evidence);
3345                    }
3346                }
3347                Some(next_factor)
3348            });
3349        }
3350    }
3351
3352    let pass = BallFilterPass {
3353        sum_log_f,
3354        sum_log_f_d1,
3355        sum_log_f_d2,
3356        sum_log_f_d3,
3357        sum_v2_over_f,
3358        sum_v2_over_f_d1,
3359        sum_v2_over_f_d2,
3360        sum_v2_over_f_d3,
3361        n_proper,
3362    };
3363    if [
3364        pass.sum_log_f,
3365        pass.sum_log_f_d1,
3366        pass.sum_v2_over_f,
3367        pass.sum_v2_over_f_d1,
3368    ]
3369    .into_iter()
3370    .any(|ball| !ball.is_finite())
3371    {
3372        return Err(SplineScoreProofError::InvalidArithmetic {
3373            context: "diffuse filter accumulator",
3374        });
3375    }
3376    Ok(pass)
3377}
3378
3379/// Fitted exact smoothing-spline posterior on the pooled knots.
3380#[derive(Clone, Debug)]
3381pub struct SplineScanFit {
3382    /// Smoothing-spline order `m` (penalize `∫(f^{(m)})²`); state dimension.
3383    /// `m = 1` is the random-walk/linear smoother, `m = 2` the cubic smoother,
3384    /// `m = 3` the quintic smoother.
3385    pub order: usize,
3386    /// Distinct sorted abscissae (pooled knots).
3387    pub knots: Vec<f64>,
3388    /// Smoothed posterior mean of `f` at each knot.
3389    pub mean: Vec<f64>,
3390    /// Smoothed posterior mean of `f′` at each knot, present only for order
3391    /// `m ≥ 2`. At `m = 1` the latent process is Brownian motion, which has NO
3392    /// pointwise derivative state (it is a.s. nondifferentiable), so this is
3393    /// `None` rather than a fabricated zero.
3394    pub deriv: Option<Vec<f64>>,
3395    /// Posterior variance of `f` at each knot (scaled by `sigma2`).
3396    pub var: Vec<f64>,
3397    /// Selected (or supplied) log smoothing parameter `log λ`.
3398    log_lambda: f64,
3399    /// Profiled (or supplied) observation variance σ².
3400    pub sigma2: f64,
3401    /// Concentrated diffuse restricted log-likelihood at the optimum, up to a
3402    /// λ- and data-independent additive constant. Differences across λ are
3403    /// exact REML criterion differences.
3404    pub restricted_loglik: f64,
3405    /// Ordinary fully normalized Gaussian log-likelihood at the fitted mean
3406    /// and profiled observation variance. Unlike `restricted_loglik`, this is
3407    /// on the common data-likelihood scale consumed by conditional AIC.
3408    pub log_likelihood: f64,
3409    /// Original training row count (pre-pooling; ties collapse to fewer
3410    /// knots), retained for every sample-size-based post-fit calculation.
3411    training_sample_size: std::num::NonZeroUsize,
3412    /// Weighted DATA residual sum of squares `Σ wᵢ (yᵢ − f̂(xᵢ))²` at the
3413    /// smoothed posterior mean. Stored explicitly because the profiled
3414    /// innovations quadratic `σ̂²·(n − order)` is the REML objective's
3415    /// quadratic — data residual energy PLUS process/roughness energy at the
3416    /// posterior mode — and is therefore NOT the Gaussian deviance.
3417    pub data_sse: f64,
3418    /// Smoothed full states `(f, f′)` per knot.
3419    smoothed_state: Vec<Vec2>,
3420    /// Smoothed full state covariances per knot (unit-σ² scale).
3421    smoothed_cov: Vec<Mat2>,
3422    /// RTS backward gains `G_t` (lag-one cross-covariance is `G_t · P^s_{t+1}`).
3423    rts_gain: Vec<Mat2>,
3424    /// q = 1/λ used by the pass (unit-σ² scale).
3425    q: f64,
3426    /// Pooled observation weight per knot (sum of tied raw weights).
3427    node_weight: Vec<f64>,
3428}
3429
3430/// Move the response into its constant-null-space chart, pool tied abscissae,
3431/// and validate inputs. Returns nodes plus the within-tie weighted residual
3432/// sum, raw observation count, and chart origin.
3433///
3434/// Centering must precede pooling. A weighted tied-row mean formed from the
3435/// absolute response level leaks that level through floating-point products
3436/// and sums before the innovation recurrence ever sees the data. Computing
3437/// both the pooled mean and within-tie residual energy from the same centered
3438/// rows makes the translation-free chart the sole arithmetic authority.
3439fn pool_nodes(
3440    x: &[f64],
3441    y: &[f64],
3442    w: &[f64],
3443    order: usize,
3444) -> Result<(Vec<PooledNode>, f64, usize, f64), String> {
3445    let n = x.len();
3446    if y.len() != n || w.len() != n {
3447        return Err(format!(
3448            "spline scan: length mismatch x={n}, y={}, w={}",
3449            y.len(),
3450            w.len()
3451        ));
3452    }
3453    for i in 0..n {
3454        if !(x[i].is_finite() && y[i].is_finite() && w[i].is_finite() && w[i] > 0.0) {
3455            return Err(format!(
3456                "spline scan: non-finite or non-positive input at row {i} (x={}, y={}, w={})",
3457                x[i], y[i], w[i]
3458            ));
3459        }
3460    }
3461    let mut perm: Vec<usize> = (0..n).collect();
3462    perm.sort_by(|&i, &j| x[i].total_cmp(&x[j]));
3463    let response_origin = perm
3464        .first()
3465        .map(|&index| y[index])
3466        .ok_or_else(|| "spline scan: cannot pool an empty response".to_string())?;
3467    let centered_y = y
3468        .iter()
3469        .enumerate()
3470        .map(|(index, &value)| {
3471            let centered = value - response_origin;
3472            centered.is_finite().then_some(centered).ok_or_else(|| {
3473                format!("spline scan: centered response is non-finite at row {index}")
3474            })
3475        })
3476        .collect::<Result<Vec<_>, _>>()?;
3477    let mut nodes: Vec<PooledNode> = Vec::new();
3478    for &i in &perm {
3479        match nodes.last_mut() {
3480            Some(last) if last.x == x[i] => {
3481                let w_new = last.w + w[i];
3482                last.y = (last.y * last.w + centered_y[i] * w[i]) / w_new;
3483                last.w = w_new;
3484            }
3485            _ => nodes.push(PooledNode {
3486                x: x[i],
3487                y: centered_y[i],
3488                w: w[i],
3489            }),
3490        }
3491    }
3492    // Need the `order` diffuse dimensions plus at least one proper innovation.
3493    if nodes.len() < order + 1 {
3494        return Err(format!(
3495            "spline scan: order {order} needs at least {} distinct abscissae, got {}",
3496            order + 1,
3497            nodes.len()
3498        ));
3499    }
3500    // Within-tie residual sum Σ w_i (y_i − ȳ_group)², part of the profiled σ².
3501    let mut ssr_within = 0.0;
3502    let mut k = 0usize;
3503    for &i in &perm {
3504        while nodes[k].x != x[i] {
3505            k += 1;
3506        }
3507        let d = centered_y[i] - nodes[k].y;
3508        ssr_within += w[i] * d * d;
3509    }
3510    Ok((nodes, ssr_within, n, response_origin))
3511}
3512
3513/// Concentrated diffuse restricted log-likelihood and its exact first three
3514/// derivatives with respect to `log λ` (σ² profiled). The derivatives are
3515/// propagated through the same diffuse Kalman recursion as the value; no
3516/// finite differencing or surrogate objective is involved. The third order
3517/// exists solely to anchor the certified-search enclosure on endpoint pairs
3518/// (#2300/#2614 fourth-order tail).
3519fn concentrated_criterion_jet(
3520    nodes: &[PooledNode],
3521    ssr_within: f64,
3522    n_obs: usize,
3523    log_lambda: f64,
3524    order: usize,
3525) -> Result<(f64, f64, f64, f64), String> {
3526    let q = gam_problem::checked_exp_log_strength(-log_lambda)
3527        .map_err(|error| format!("spline scan inverse log strength: {error}"))?;
3528    let pass = run_filter::<false>(nodes, q, order)?;
3529    // Profiled σ̂² over the proper innovations plus within-tie residuals;
3530    // the restricted degrees of freedom subtract the diffuse dimension `order`.
3531    let dof = (n_obs - order) as f64;
3532    let rss = pass.sum_v2_over_f + ssr_within;
3533    if rss <= 0.0 {
3534        return Err("spline scan: degenerate zero residual sum".to_string());
3535    }
3536    let sigma2 = rss / dof;
3537    if pass.n_proper != nodes.len() - order {
3538        return Err(format!(
3539            "spline scan: expected {} proper innovations, got {} (diffuse rank not consumed)",
3540            nodes.len() - order,
3541            pass.n_proper
3542        ));
3543    }
3544    let rss_d1 = pass.sum_v2_over_f_d1;
3545    let rss_d2 = pass.sum_v2_over_f_d2;
3546    let rss_d3 = pass.sum_v2_over_f_d3;
3547    let rss_log_d1 = rss_d1 / rss;
3548    let rss_log_d2 = rss_d2 / rss - rss_log_d1 * rss_log_d1;
3549    let rss_log_d3 = rss_d3 / rss - 3.0 * (rss_d2 / rss) * rss_log_d1
3550        + 2.0 * rss_log_d1 * rss_log_d1 * rss_log_d1;
3551    Ok((
3552        -0.5 * (pass.sum_log_f + dof * sigma2.ln()),
3553        -0.5 * (pass.sum_log_f_d1 + dof * rss_log_d1),
3554        -0.5 * (pass.sum_log_f_d2 + dof * rss_log_d2),
3555        -0.5 * (pass.sum_log_f_d3 + dof * rss_log_d3),
3556    ))
3557}
3558
3559#[derive(Clone, Copy, Debug)]
3560struct CertifiedCriterionJet {
3561    jet: ScoreJet,
3562    value: Ball,
3563    derivative: Ball,
3564    curvature: Ball,
3565    third: Ball,
3566    /// Where the curvature and the third order came from. A search that quietly
3567    /// got weaker is the same defect class as a criterion that quietly drifted,
3568    /// so a certificate that fell back to a global constant names itself.
3569    curvature_source: BoundSource,
3570    third_source: BoundSource,
3571}
3572
3573impl CertifiedCriterionJet {
3574    /// Which bounds anchored this endpoint, when either is not the exact jet.
3575    ///
3576    /// `curvature_source` / `third_source` exist so a certificate that fell back
3577    /// to a closed-form global constant NAMES ITSELF instead of quietly getting
3578    /// wider. A field nobody reads cannot do that: written-and-never-read IS the
3579    /// silent degradation these fields were added to prevent, and it is also a
3580    /// hard `-D dead-code` failure in any build of this crate as a plain library
3581    /// rather than a test target -- which `-p gam-solve --lib` never exercises,
3582    /// so it broke every integration binary while the usual measurement stayed
3583    /// green.
3584    ///
3585    /// `None` on the common path, so a reader only hears about a weakened anchor.
3586    fn weakened_anchor(self) -> Option<(BoundSource, BoundSource)> {
3587        if matches!(
3588            (self.curvature_source, self.third_source),
3589            (BoundSource::EndpointJet, BoundSource::EndpointJet)
3590        ) {
3591            None
3592        } else {
3593            Some((self.curvature_source, self.third_source))
3594        }
3595    }
3596}
3597
3598/// Which bound anchored a derivative at this endpoint.
3599#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3600enum BoundSource {
3601    /// The exact endpoint `V‴` jet — the fourth-order tail of #2300/#2614.
3602    EndpointJet,
3603    /// The closed-form global bound `½(r/4 + 6ν)`, taken because the endpoint
3604    /// jet's enclosure left the finite range. The search stays CERTIFIED and
3605    /// its tail cells are merely wider: `(|V′|/L₃)^{1/2}` in place of
3606    /// the endpoint-pair `(|V′|/L₅)^{1/4}` rate. The third order exists SOLELY
3607    /// to anchor that radius, so losing it costs cells, never soundness.
3608    ///
3609    /// The measured boundary that makes this reachable, recorded where a reader
3610    /// meets it rather than left to be rediscovered: before #2614's centred
3611    /// Riccati/shared-`q` repair, smoothing order 3 refused throughout
3612    /// `-20 <= ρ <= -10` when the covariance-derivative zonotope overflowed.
3613    /// The repaired representation reaches the exact endpoint jets throughout
3614    /// that measured domain. This fallback remains the sound terminal bound for
3615    /// other inputs whose endpoint jet genuinely carries less information.
3616    AnalyticGlobalBound,
3617}
3618
3619/// `|V″| ≤ ½(r/4 + 2ν)` and `|V‴| ≤ ½(r/4 + 6ν)`, the closed-form derivative
3620/// bounds derived in [`concentrated_criterion_enclosure`]'s own documentation,
3621/// in the same family as the fourth-order bound the radius already uses.
3622fn curvature_global_bound(proper_modes: f64, residual_dof: f64) -> f64 {
3623    0.5 * (0.25 * proper_modes + 2.0 * residual_dof)
3624}
3625
3626fn third_derivative_global_bound(proper_modes: f64, residual_dof: f64) -> f64 {
3627    0.5 * (0.25 * proper_modes + 6.0 * residual_dof)
3628}
3629
3630/// Conservative closed-form bound on the concentrated criterion's fifth
3631/// derivative with respect to `rho = log(lambda)`.
3632///
3633/// For a determinant mode `u in [0,1]`, the fifth derivative is
3634///
3635/// `u(1-u)(1 - 14u + 36u² - 24u³)`,
3636///
3637/// up to sign. Absolute coefficient summation and `u(1-u) <= 1/4` bound it by
3638/// `(1+14+36+24)/4 = 18.75`. For one normalized residual kernel,
3639///
3640/// `t⁽⁵⁾/t = u(1 - 30u + 150u² - 240u³ + 120u⁴)`,
3641///
3642/// up to sign, hence `|t⁽⁵⁾/t| <= 541`; the ratios through order four are each
3643/// bounded by one as documented on [`concentrated_criterion_enclosure`].
3644/// Faa di Bruno for `(log R)⁽⁵⁾` adds absolute coefficients
3645/// `5+10+20+30+60+24 = 149` from those lower ratios, for `541+149 = 690`.
3646///
3647/// These deliberately elementary coefficient bounds are wider than the exact
3648/// polynomial ranges but need no spectral decomposition or data-dependent
3649/// tail assumption. Their consumer integrates the bound four times, so the
3650/// resulting remainder is still far below endpoint evaluator error.
3651fn fifth_derivative_global_bound(proper_modes: Ball, residual_dof: Ball) -> Ball {
3652    proper_modes
3653        .scale(18.75)
3654        .add(residual_dof.scale(690.0))
3655        .scale(0.5)
3656}
3657
3658/// Intersect a derivative enclosure with its closed-form global bound.
3659///
3660/// ONE rule at every order, not a branch taken only on failure: the minimum of
3661/// two valid upper bounds on the same quantity is a valid upper bound, so this
3662/// is sound wherever it applies and strictly tighter than the endpoint jet
3663/// whenever the jet is the wider of the two. The fallback is then the special
3664/// case where the jet carries no information at all.
3665///
3666/// The thing being replaced in that case is `[-inf, +inf]`, which is not a
3667/// stronger object than a finite global bound — the search cannot bracket on
3668/// it. A certificate that took the global bound says so through its
3669/// [`BoundSource`], because a search that silently got weaker is the defect
3670/// class this whole issue is about.
3671fn intersect_with_global_bound(ball: Ball, bound: f64) -> (Ball, BoundSource) {
3672    if ball.is_finite() {
3673        let lo = ball.lo.max(-bound);
3674        let hi = ball.hi.min(bound);
3675        if lo <= hi {
3676            return (
3677                Ball {
3678                    value: ball.value.clamp(lo, hi),
3679                    lo,
3680                    hi,
3681                },
3682                BoundSource::EndpointJet,
3683            );
3684        }
3685    }
3686    (
3687        Ball {
3688            value: ball.value.clamp(-bound, bound),
3689            lo: -bound,
3690            hi: bound,
3691        },
3692        BoundSource::AnalyticGlobalBound,
3693    )
3694}
3695
3696/// The concentrated criterion evaluated once with a simultaneous
3697/// directed-rounding proof of all four returned components.
3698fn certified_concentrated_criterion_jet(
3699    nodes: &[PooledNode],
3700    ssr_within: f64,
3701    n_obs: usize,
3702    log_lambda: f64,
3703    order: usize,
3704) -> Result<CertifiedCriterionJet, SplineScoreProofError> {
3705    let q_value = gam_problem::checked_exp_log_strength(-log_lambda).map_err(|error| {
3706        SplineScoreProofError::InvalidInput(format!("spline scan inverse log strength: {error}"))
3707    })?;
3708    let q_enclosure = gam_math::score_opt::certified_exp(-log_lambda).ok_or(
3709        SplineScoreProofError::InvalidArithmetic {
3710            context: "inverse log-strength exponential",
3711        },
3712    )?;
3713    let q = Ball::certified(q_value, q_enclosure);
3714    let pass = run_filter_ball(nodes, q, order)?;
3715    if pass.n_proper != nodes.len() - order {
3716        return Err(SplineScoreProofError::InvalidInput(format!(
3717            "spline scan: expected {} proper innovations, got {} (diffuse rank not consumed)",
3718            nodes.len() - order,
3719            pass.n_proper
3720        )));
3721    }
3722
3723    let dof = Ball::exact((n_obs - order) as f64);
3724    let rss = pass.sum_v2_over_f.add(Ball::exact(ssr_within));
3725    if !(rss.lo > 0.0) {
3726        return Err(SplineScoreProofError::NonPositiveProfileResidual {
3727            enclosure: rss.interval(),
3728        });
3729    }
3730    let sigma2 = rss.div_positive(dof);
3731    let rss_d1 = pass.sum_v2_over_f_d1;
3732    let rss_d2 = pass.sum_v2_over_f_d2;
3733    let rss_d3 = pass.sum_v2_over_f_d3;
3734    let mut rss_log_d1 = rss_d1.div_positive(rss);
3735    // `0 ≤ (Σ v²/F̃)′ ≤ Σ v²/F̃ ≤ rss` bounds this RATIO by one, and the division
3736    // above cannot see that: it ranges numerator and denominator independently,
3737    // so the same dependency loss the accumulator ranges just removed reappears
3738    // one line later. Measured at order 3, ρ = −16.6135: the accumulator pair is
3739    // `(21.2 ± 34.8, 0.494 ± 38.6)` — both inside their ranges — and the
3740    // quotient still reaches `2.8e2`, carrying the certified derivative to
3741    // `[−2.51e4, 88.5]` when the model fixes it at `[−ν/2, r/2]`. The upper end
3742    // is already exactly `r/2 = 88.5`; only the quotient's lower end was loose.
3743    intersect_with_exact_range(&mut rss_log_d1, 0.0, 1.0);
3744    let rss_log_d2 = rss_d2.div_positive(rss).sub(rss_log_d1.square());
3745    let rss_log_d3 = rss_d3
3746        .div_positive(rss)
3747        .sub(rss_d2.div_positive(rss).mul(rss_log_d1).scale(3.0))
3748        .add(rss_log_d1.square().mul(rss_log_d1).scale(2.0));
3749    let value = pass
3750        .sum_log_f
3751        .add(dof.mul(sigma2.ln_positive()))
3752        .scale(-0.5);
3753    let derivative = pass.sum_log_f_d1.add(dof.mul(rss_log_d1)).scale(-0.5);
3754    let curvature = pass.sum_log_f_d2.add(dof.mul(rss_log_d2)).scale(-0.5);
3755    let third = pass.sum_log_f_d3.add(dof.mul(rss_log_d3)).scale(-0.5);
3756    if [value, derivative]
3757        .into_iter()
3758        .any(|ball| !ball.is_finite())
3759    {
3760        return Err(SplineScoreProofError::InvalidArithmetic {
3761            context: "concentrated criterion",
3762        });
3763    }
3764    // The third order is an OPTIMISATION, not a requirement: endpoint pairs
3765    // linearly interpolate it so the #2300/#2614 tail remainder is fourth
3766    // order. When its enclosure leaves the finite range, the closed-form global
3767    // bound keeps the certificate valid and costs only a wider tail cell.
3768    // Refusing the whole jet instead discards a value, slope and curvature that
3769    // are all finite, which is what the divergence refusal used to do at every
3770    // order-3 rho below -6.
3771    let proper_modes = (nodes.len() - order) as f64;
3772    let residual_dof = (n_obs - order) as f64;
3773    let (curvature, curvature_source) = intersect_with_global_bound(
3774        curvature,
3775        curvature_global_bound(proper_modes, residual_dof),
3776    );
3777    let (third, third_source) = intersect_with_global_bound(
3778        third,
3779        third_derivative_global_bound(proper_modes, residual_dof),
3780    );
3781    Ok(CertifiedCriterionJet {
3782        jet: ScoreJet {
3783            value: value.value,
3784            derivative: derivative.value,
3785            curvature: curvature.value,
3786            third: third.value,
3787        },
3788        value,
3789        derivative,
3790        curvature,
3791        third,
3792        curvature_source,
3793        third_source,
3794    })
3795}
3796
3797/// Rigorous interval enclosure of the score's first two derivatives.
3798///
3799/// After eliminating the diffuse polynomial null space, the Gaussian profile
3800/// is an affine covariance pencil. Every determinant mode has response
3801/// `u in [0,1]`; every normalized profiled-residual derivative is a convex
3802/// average of the same kernels. Consequently
3803///
3804/// `|L'| <= 1/2 (r/4 + nu)`, `|L''| <= 1/2 (r/4 + 2 nu)`,
3805/// `|L'''| <= 1/2 (r/4 + 6 nu)`, `|L''''| <= 1/2 (r/4 + 26 nu)`, and
3806/// `|L'''''| <= 1/2 (18.75 r + 690 nu)`,
3807///
3808/// where `r` is the number of proper innovation modes and `nu=n-order` is the
3809/// residual d.f. For one normalized determinant contribution, the fourth
3810/// derivative is `u(1-u)(1-6u+6u^2)`, whose magnitude is at most `1/4` on
3811/// `u in [0,1]`; so is the FIRST derivative `u(1-u)`, which is why every order
3812/// carries the same `r/4` term. For each residual kernel `t = z^2 (1-u)`,
3813/// every ratio `|t^{(k)}/t| <= 1` for `k <= 4`, so Faa di Bruno on `log R`
3814/// gives `1 = 1` at first order, `1+1 = 2` at second, `1+3+2 = 6` at third and
3815/// `1+4+3+12+6 = 26` at fourth. Within-tie residual energy is
3816/// lambda-independent and only tightens these bounds.
3817/// Endpoint jets plus these analytic Lipschitz bounds therefore enclose the
3818/// entire interval without a sampling lattice.
3819///
3820/// The cell is the union of its two half-cells. Every point is within
3821/// `h=(hi-lo)/2` of its nearest endpoint, so the left endpoint anchors signed
3822/// displacements `[0,h]` and the right endpoint anchors `[-h,0]`. The two
3823/// certified endpoint `V'''` balls define a linear interpolant. The standard
3824/// interpolation error
3825///
3826/// `|V'''(x) - linear(V'''(lo), V'''(hi))| <= L5 (x-lo)(hi-x)/2`
3827///
3828/// integrates from either endpoint to give maximum half-cell remainders
3829/// `L5*w^5/960`, `L5*w^4/128`, and `L5*w^3/24` for `V`, `V'`, and `V''`.
3830/// Evaluating the resulting quartic polynomial over each signed half-cell and
3831/// hulling the two results gives one theorem uniformly for all three channels.
3832/// Independently, integrating over the full cell from EACH endpoint gives
3833/// value remainders `L5*w^5/80`. Both full-cell Taylor ranges contain every
3834/// score in the cell, so their intersection with the half-cell hull removes
3835/// endpoint roundoff asymmetry without weakening the theorem.
3836///
3837/// Nearest-endpoint geometry first removes factors 16, 8, and 4 of false
3838/// fourth-derivative uncertainty. Even then, a data-independent global `L4`
3839/// dominates an exponentially saturated endpoint jet. Interpolating the
3840/// endpoint third derivatives removes that constant floor without a fourth
3841/// filter jet: only the globally bounded interpolation error remains, one
3842/// asymptotic order smaller.
3843///
3844/// A second independent curvature theorem protects stationary isolation from
3845/// a loose covariance second-derivative recurrence. The derivative endpoint
3846/// balls give a secant `s=(V'(hi)-V'(lo))/w`; the mean-value theorem supplies
3847/// `xi` in the cell with `V''(xi)=s`, and the global `|V'''|` bound then gives
3848/// `V''(x) in s ± L3*w` everywhere in the cell. Intersecting this range with
3849/// the endpoint-third range can only tighten a valid outer enclosure. It is
3850/// especially decisive near a root, where endpoint `V'` remains sharp even
3851/// when direct interval propagation has lost the sign of `V''`.
3852///
3853/// Once that whole-cell curvature range `C` is known, integrating it from both
3854/// endpoints gives two further derivative theorems:
3855/// `V'(x) in V'(lo)+C*[0,w]` and `V'(x) in V'(hi)+C*[-w,0]`. Their intersection
3856/// with the endpoint-third derivative range preserves the same exact-real
3857/// derivative while recovering local root information from tight endpoint
3858/// balls. A disjoint intersection is an internal certificate contradiction,
3859/// never a reason to widen or fall back.
3860///
3861/// All polynomial operations below use endpoint BALLS and outward interval
3862/// arithmetic. The search caches those balls alongside each endpoint jet, so
3863/// this function performs no filter pass of its own and includes the endpoint
3864/// evaluator's directed-rounding error in every returned channel.
3865fn concentrated_criterion_enclosure(
3866    n_nodes: usize,
3867    n_obs: usize,
3868    left: ScoreSample,
3869    right: ScoreSample,
3870    left_certificate: CertifiedCriterionJet,
3871    right_certificate: CertifiedCriterionJet,
3872    order: usize,
3873) -> Result<DerivativeEnclosure, SplineScoreProofError> {
3874    let (lo, hi) = (left.x, right.x);
3875    if !(lo.is_finite() && hi.is_finite() && lo <= hi) {
3876        return Err(SplineScoreProofError::InvalidInput(format!(
3877            "spline scan: invalid score-enclosure interval [{lo}, {hi}]"
3878        )));
3879    }
3880    if lo == hi {
3881        return Ok(DerivativeEnclosure {
3882            score: ScoreValueEnclosure {
3883                value: ClosedInterval::new(
3884                    left_certificate.value.lo.min(right_certificate.value.lo),
3885                    left_certificate.value.hi.max(right_certificate.value.hi),
3886                ),
3887                evaluation_error: left_certificate
3888                    .value
3889                    .forward_error()
3890                    .max(right_certificate.value.forward_error()),
3891            },
3892            derivative: ClosedInterval::new(
3893                left_certificate
3894                    .derivative
3895                    .lo
3896                    .min(right_certificate.derivative.lo),
3897                left_certificate
3898                    .derivative
3899                    .hi
3900                    .max(right_certificate.derivative.hi),
3901            ),
3902            curvature: ClosedInterval::new(
3903                left_certificate
3904                    .curvature
3905                    .lo
3906                    .min(right_certificate.curvature.lo),
3907                left_certificate
3908                    .curvature
3909                    .hi
3910                    .max(right_certificate.curvature.hi),
3911            ),
3912        });
3913    }
3914    let width = Ball::exact(hi).sub(Ball::exact(lo));
3915    if !(width.lo > 0.0) {
3916        return Err(SplineScoreProofError::InvalidArithmetic {
3917            context: "positive score-enclosure width",
3918        });
3919    }
3920    let proper_modes = Ball::exact((n_nodes - order) as f64);
3921    let residual_dof = Ball::exact((n_obs - order) as f64);
3922    let fifth_abs_bound = fifth_derivative_global_bound(proper_modes, residual_dof);
3923    let third_abs_bound = proper_modes
3924        .scale(0.25)
3925        .add(residual_dof.scale(6.0))
3926        .scale(0.5);
3927    // Announce a weakened anchor at the point it anchors.
3928    //
3929    // Both endpoints feed their nearest half-cell, so either one falling back
3930    // to a global constant widens that half. Reported here rather than at
3931    // construction so the message names the consequence, not just the fact.
3932    for (side, weakened) in [
3933        ("left", left_certificate.weakened_anchor()),
3934        ("right", right_certificate.weakened_anchor()),
3935    ] {
3936        if let Some((curvature_source, third_source)) = weakened {
3937            log::debug!(
3938                "spline scan enclosure: {side} endpoint curvature anchored by \
3939                 {curvature_source:?}, third order by {third_source:?}. A global-bound \
3940                 anchor keeps the search CERTIFIED and widens its tail cells -- half rate \
3941                 in place of fourth-order rate -- so it costs cells, never soundness."
3942            );
3943        }
3944    }
3945    let half_width = width.scale(0.5);
3946    let width2 = width.square();
3947    let width3 = width2.mul(width);
3948    let width4 = width2.square();
3949    let width5 = width4.mul(width);
3950    let value_remainder = fifth_abs_bound
3951        .mul(width5)
3952        .div_positive(Ball::exact(960.0))
3953        .hi;
3954    let derivative_remainder = fifth_abs_bound
3955        .mul(width4)
3956        .div_positive(Ball::exact(128.0))
3957        .hi;
3958    let curvature_remainder = fifth_abs_bound
3959        .mul(width3)
3960        .div_positive(Ball::exact(24.0))
3961        .hi;
3962    let third_slope = right_certificate
3963        .third
3964        .sub(left_certificate.third)
3965        .div_positive(width);
3966
3967    // Integrate the endpoint-pair linear interpolant of V''' from either
3968    // endpoint over a signed displacement. Keeping the sign of `d` is materially
3969    // tighter than replacing every term by an absolute-value radius, while
3970    // ordinary interval arithmetic still gives an outer range despite
3971    // dependencies among powers of `d`.
3972    let endpoint_enclosure = |certificate: CertifiedCriterionJet,
3973                              displacement: ClosedInterval,
3974                              value_remainder: f64,
3975                              derivative_remainder: f64,
3976                              curvature_remainder: f64| {
3977        let d = Ball::certified(0.0, displacement);
3978        let d2 = d.square();
3979        let d3 = d2.mul(d);
3980        let d4 = d2.square();
3981        let value = certificate
3982            .value
3983            .add(certificate.derivative.mul(d))
3984            .add(certificate.curvature.mul(d2).scale(0.5))
3985            .add(certificate.third.mul(d3).div_positive(Ball::exact(6.0)))
3986            .add(third_slope.mul(d4).div_positive(Ball::exact(24.0)))
3987            .interval()
3988            .add(ClosedInterval::new(-value_remainder, value_remainder));
3989        let derivative = certificate
3990            .derivative
3991            .add(certificate.curvature.mul(d))
3992            .add(certificate.third.mul(d2).scale(0.5))
3993            .add(third_slope.mul(d3).div_positive(Ball::exact(6.0)))
3994            .interval()
3995            .add(ClosedInterval::new(
3996                -derivative_remainder,
3997                derivative_remainder,
3998            ));
3999        let curvature = certificate
4000            .curvature
4001            .add(certificate.third.mul(d))
4002            .add(third_slope.mul(d2).scale(0.5))
4003            .interval()
4004            .add(ClosedInterval::new(
4005                -curvature_remainder,
4006                curvature_remainder,
4007            ));
4008        (value, derivative, curvature)
4009    };
4010
4011    let (left_value, left_derivative, left_curvature) = endpoint_enclosure(
4012        left_certificate,
4013        ClosedInterval::new(0.0, half_width.hi),
4014        value_remainder,
4015        derivative_remainder,
4016        curvature_remainder,
4017    );
4018    let (right_value, right_derivative, right_curvature) = endpoint_enclosure(
4019        right_certificate,
4020        ClosedInterval::new(-half_width.hi, 0.0),
4021        value_remainder,
4022        derivative_remainder,
4023        curvature_remainder,
4024    );
4025    let half_cell_score = ClosedInterval::new(
4026        left_value.lo.min(right_value.lo),
4027        left_value.hi.max(right_value.hi),
4028    );
4029    let full_value_remainder = fifth_abs_bound
4030        .mul(width5)
4031        .div_positive(Ball::exact(80.0))
4032        .hi;
4033    let full_derivative_remainder = fifth_abs_bound
4034        .mul(width4)
4035        .div_positive(Ball::exact(24.0))
4036        .hi;
4037    let full_curvature_remainder = fifth_abs_bound
4038        .mul(width3)
4039        .div_positive(Ball::exact(12.0))
4040        .hi;
4041    let (full_left_value, _, _) = endpoint_enclosure(
4042        left_certificate,
4043        ClosedInterval::new(0.0, width.hi),
4044        full_value_remainder,
4045        full_derivative_remainder,
4046        full_curvature_remainder,
4047    );
4048    let (full_right_value, _, _) = endpoint_enclosure(
4049        right_certificate,
4050        ClosedInterval::new(-width.hi, 0.0),
4051        full_value_remainder,
4052        full_derivative_remainder,
4053        full_curvature_remainder,
4054    );
4055    let score_value = ClosedInterval::new(
4056        half_cell_score
4057            .lo
4058            .max(full_left_value.lo)
4059            .max(full_right_value.lo),
4060        half_cell_score
4061            .hi
4062            .min(full_left_value.hi)
4063            .min(full_right_value.hi),
4064    );
4065    if !(score_value.lo <= score_value.hi) {
4066        return Err(SplineScoreProofError::InvalidArithmetic {
4067            context: "endpoint score-enclosure intersection",
4068        });
4069    }
4070    let endpoint_third_derivative = ClosedInterval::new(
4071        left_derivative.lo.min(right_derivative.lo),
4072        left_derivative.hi.max(right_derivative.hi),
4073    );
4074    let endpoint_third_curvature = ClosedInterval::new(
4075        left_curvature.lo.min(right_curvature.lo),
4076        left_curvature.hi.max(right_curvature.hi),
4077    );
4078    let derivative_secant = right_certificate
4079        .derivative
4080        .sub(left_certificate.derivative)
4081        .div_positive(width);
4082    let secant_radius = third_abs_bound.mul(width).hi;
4083    let secant_curvature = derivative_secant
4084        .interval()
4085        .add(ClosedInterval::new(-secant_radius, secant_radius));
4086    let curvature = ClosedInterval::new(
4087        endpoint_third_curvature.lo.max(secant_curvature.lo),
4088        endpoint_third_curvature.hi.min(secant_curvature.hi),
4089    );
4090    if !(curvature.lo <= curvature.hi) {
4091        return Err(SplineScoreProofError::InvalidArithmetic {
4092            context: "curvature secant intersection",
4093        });
4094    }
4095    let curvature_ball = Ball::certified(0.0, curvature);
4096    let derivative_from_left = left_certificate
4097        .derivative
4098        .add(curvature_ball.mul(Ball::certified(0.0, ClosedInterval::new(0.0, width.hi))))
4099        .interval();
4100    let derivative_from_right = right_certificate
4101        .derivative
4102        .add(curvature_ball.mul(Ball::certified(0.0, ClosedInterval::new(-width.hi, 0.0))))
4103        .interval();
4104    let derivative_from_curvature = ClosedInterval::new(
4105        derivative_from_left.lo.max(derivative_from_right.lo),
4106        derivative_from_left.hi.min(derivative_from_right.hi),
4107    );
4108    let derivative = ClosedInterval::new(
4109        endpoint_third_derivative
4110            .lo
4111            .max(derivative_from_curvature.lo),
4112        endpoint_third_derivative
4113            .hi
4114            .min(derivative_from_curvature.hi),
4115    );
4116    if !(derivative.lo <= derivative.hi) {
4117        return Err(SplineScoreProofError::InvalidArithmetic {
4118            context: "derivative curvature-integral intersection",
4119        });
4120    }
4121    let evaluation_error = left_certificate
4122        .value
4123        .forward_error()
4124        .max(right_certificate.value.forward_error());
4125    Ok(DerivativeEnclosure {
4126        score: ScoreValueEnclosure {
4127            value: score_value,
4128            evaluation_error,
4129        },
4130        derivative,
4131        curvature,
4132    })
4133}
4134
4135/// Exact diffuse smoother for the `order−1` partially-diffuse leading nodes
4136/// (#1044 — the multi-node generalization of the `m = 2` reverse-Markov
4137/// closure).
4138///
4139/// Ordinary RTS recovers every node `t ≥ order−1` (where the filtered
4140/// distribution is proper). The first `order−1` nodes are partially diffuse:
4141/// their filtered covariance still carries unresolved diffuse mass, so RTS —
4142/// which needs the predicted covariance `P_{t+1|t}` to be invertible — cannot
4143/// reach them. By the Markov property the leading block depends on all future
4144/// data ONLY through the first proper smoothed node `α_{order−1}`:
4145///
4146///   p(α_{0..order−2} | y) = ∫ p(α_{0..order−2} | α_{order−1}, y_{0..order−2})
4147///                             · p(α_{order−1} | y) dα_{order−1}.
4148///
4149/// The inner conditional is a proper Gaussian: it is the flat (improper)
4150/// leading prior tightened by the Markov increments `(α_{t+1} − Fα_t)ᵀ(qQ)⁻¹(·)`
4151/// and the leading observations `w_t (y_t − f_t)²`, with `α_{order−1}` entering
4152/// linearly through the last increment. Writing `u = (α_0, …, α_{order−2})`,
4153///
4154///   u | α_{order−1} ~ N(C·α_{order−1} + d,  Σ),   Σ = Λ⁻¹,
4155///   Λ  = increments(F'(qQ)⁻¹F …) + leading obs,
4156///   d  = Σ·b_const,   C = Σ·B   (B = the pinned-node coupling F'(qQ)⁻¹),
4157///
4158/// and pushing the smoothed `α_{order−1} ~ N(α̂_p, V_p)` through the affine map
4159/// gives the EXACT smoothed leading block, its covariances, and the lag-one
4160/// cross-covariances `Cov(α_j, α_{j+1} | y)` the bridge `predict` needs:
4161///
4162///   mean(u) = C·α̂_p + d,   Cov(u) = C V_p Cᵀ + Σ,   Cov(u, α_p) = C V_p.
4163///
4164/// This is exact Gaussian conditioning — no diffuse RTS recursion, no
4165/// sign-convention-laden `r/N` adjoint. At `order = 2` (one leading node) it is
4166/// algebraically the existing single-node closure.
4167fn leading_block_smooth(
4168    sm_state: &mut [Vec2],
4169    sm_cov: &mut [Mat2],
4170    gains: &mut [Mat2],
4171    nodes: &[PooledNode],
4172    q: f64,
4173    order: usize,
4174) -> Result<(), String> {
4175    let nb = order - 1; // leading nodes 0..nb-1 (the partially-diffuse ones)
4176    let pin = order - 1; // first proper smoothed node (conditioning anchor)
4177    let d = nb * order; // joint dimension of the leading block
4178    let mut lambda = vec![vec![0.0_f64; d]; d];
4179    let mut b_const = vec![0.0_f64; d];
4180    let mut bmat = vec![vec![0.0_f64; order]; d]; // coupling to the pinned node
4181
4182    // Markov increments t = 0..order-2, each connecting node t and node t+1.
4183    for t in 0..order - 1 {
4184        let delta = nodes[t + 1].x - nodes[t].x;
4185        let f = transition(delta, order);
4186        let qn = process_noise(delta, q, order);
4187        let a = mat_inv(&qn, order, "leading-block increment noise")?; // (qQ)⁻¹ (symmetric)
4188        let ft = mat_t(&f, order);
4189        let fta = mat_mul(&ft, &a, order); // F'A
4190        let ftaf = mat_mul(&fta, &f, order); // F'A F
4191        let af = mat_mul(&a, &f, order); // A F = (F'A)'
4192        // Node t diagonal block (node t is always in the block): += F'A F.
4193        for i in 0..order {
4194            for j in 0..order {
4195                lambda[t * order + i][t * order + j] += ftaf[i][j];
4196            }
4197        }
4198        if t + 1 <= nb - 1 {
4199            // Both nodes are in the block: fill node t+1's diagonal and the
4200            // symmetric cross blocks.
4201            for i in 0..order {
4202                for j in 0..order {
4203                    lambda[(t + 1) * order + i][(t + 1) * order + j] += a[i][j];
4204                    lambda[t * order + i][(t + 1) * order + j] -= fta[i][j];
4205                    lambda[(t + 1) * order + i][t * order + j] -= af[i][j];
4206                }
4207            }
4208        } else {
4209            // t+1 is the pinned node: it enters the conditional only linearly,
4210            // through B (its coupling into node t's score is F'A·α_pin).
4211            for i in 0..order {
4212                for j in 0..order {
4213                    bmat[t * order + i][j] += fta[i][j];
4214                }
4215            }
4216        }
4217    }
4218    // Leading observations: y_t informs the f-component (local index 0) of node t.
4219    for t in 0..nb {
4220        let w = nodes[t].w;
4221        lambda[t * order][t * order] += w;
4222        b_const[t * order] += w * nodes[t].y;
4223    }
4224
4225    // Conditional covariance Σ = Λ⁻¹, intercept d = Σ·b_const, coupling C = Σ·B.
4226    let sigma = dense_spd_inverse(&lambda, "leading-block precision")?;
4227    let dvec: Vec<f64> = (0..d)
4228        .map(|i| (0..d).map(|k| sigma[i][k] * b_const[k]).sum())
4229        .collect();
4230    let cmat: Vec<Vec<f64>> = (0..d)
4231        .map(|i| {
4232            (0..order)
4233                .map(|j| (0..d).map(|k| sigma[i][k] * bmat[k][j]).sum())
4234                .collect()
4235        })
4236        .collect();
4237
4238    // Pinned smoothed moments (from the ordinary RTS pass).
4239    let ahat_p = sm_state[pin];
4240    let vp = sm_cov[pin];
4241    // cvp = C·V_p  (= Cov(u, α_pin)), D×order.
4242    let cvp: Vec<Vec<f64>> = (0..d)
4243        .map(|i| {
4244            (0..order)
4245                .map(|j| (0..order).map(|k| cmat[i][k] * vp[k][j]).sum())
4246                .collect()
4247        })
4248        .collect();
4249    // mean(u) = C·α̂_p + d.
4250    let mean_u: Vec<f64> = (0..d)
4251        .map(|i| (0..order).map(|j| cmat[i][j] * ahat_p[j]).sum::<f64>() + dvec[i])
4252        .collect();
4253    // Cov(u) = cvp·Cᵀ + Σ.
4254    let cov_u: Vec<Vec<f64>> = (0..d)
4255        .map(|i| {
4256            (0..d)
4257                .map(|k| (0..order).map(|j| cvp[i][j] * cmat[k][j]).sum::<f64>() + sigma[i][k])
4258                .collect()
4259        })
4260        .collect();
4261
4262    // Scatter the smoothed leading states and covariances.
4263    for j in 0..nb {
4264        for i in 0..order {
4265            sm_state[j][i] = mean_u[j * order + i];
4266        }
4267        let mut cov = [[0.0_f64; MAX_ORDER]; MAX_ORDER];
4268        for i in 0..order {
4269            for k in 0..order {
4270                cov[i][k] = cov_u[j * order + i][j * order + k];
4271            }
4272        }
4273        symmetrize(&mut cov, order);
4274        sm_cov[j] = cov;
4275    }
4276    // Lag-one bridge gains for the leading intervals [j, j+1], j = 0..order-2.
4277    // gain_j = Cov(α_j, α_{j+1} | y) · Cov(α_{j+1} | y)⁻¹, so that the bridge's
4278    // `gain_j · P^s_{j+1}` reproduces the exact lag-one smoothed cross-cov.
4279    for j in 0..nb {
4280        let mut cross = [[0.0_f64; MAX_ORDER]; MAX_ORDER];
4281        if j + 1 <= nb - 1 {
4282            // Both in the block: read the (j, j+1) sub-block of Cov(u).
4283            for i in 0..order {
4284                for k in 0..order {
4285                    cross[i][k] = cov_u[j * order + i][(j + 1) * order + k];
4286                }
4287            }
4288        } else {
4289            // j+1 is the pinned node: read node j's rows of Cov(u, α_pin) = cvp.
4290            for i in 0..order {
4291                for k in 0..order {
4292                    cross[i][k] = cvp[j * order + i][k];
4293                }
4294            }
4295        }
4296        let denom_inv = mat_inv(&sm_cov[j + 1], order, "leading-block gain denominator")?;
4297        gains[j] = mat_mul(&cross, &denom_inv, order);
4298    }
4299    Ok(())
4300}
4301
4302/// Fit at a FIXED `log λ` and order `m ∈ {1, 2, 3}`, σ² either supplied or
4303/// profiled.
4304pub fn fit_spline_scan_at(
4305    x: &[f64],
4306    y: &[f64],
4307    w: &[f64],
4308    log_lambda: f64,
4309    sigma2: Option<f64>,
4310    order: usize,
4311) -> Result<SplineScanFit, String> {
4312    if order == 0 || order > MAX_ORDER {
4313        return Err(format!(
4314            "spline scan: order must be in 1..={MAX_ORDER}, got {order}"
4315        ));
4316    }
4317    let (nodes, ssr_within, n_obs, response_origin) = pool_nodes(x, y, w, order)?;
4318    let q = gam_problem::checked_exp_log_strength(-log_lambda)
4319        .map_err(|error| format!("spline scan inverse log strength: {error}"))?;
4320    let pass = run_filter::<true>(&nodes, q, order)?;
4321    let n = nodes.len();
4322    let dof = (n_obs - order) as f64;
4323    let sigma2 = match sigma2 {
4324        Some(s) => {
4325            if !(s.is_finite() && s > 0.0) {
4326                return Err(format!("spline scan: invalid sigma2 {s}"));
4327            }
4328            s
4329        }
4330        None => (pass.sum_v2_over_f + ssr_within) / dof,
4331    };
4332    // Full diffuse restricted log-likelihood at this (λ, σ²), up to λ- and
4333    // σ-free additive constants: −½[Σ log F̃ + dof·ln σ² + RSS/σ²]. At the
4334    // profiled σ̂² the quadratic term collapses to the λ-free constant `dof`,
4335    // matching `concentrated_criterion` up to that constant.
4336    let rss = pass.sum_v2_over_f + ssr_within;
4337    let restricted_loglik = -0.5 * (pass.sum_log_f + dof * sigma2.ln() + rss / sigma2);
4338
4339    // ── Smoother: ordinary RTS for the proper nodes (t ≥ order−1) plus an
4340    // exact diffuse conditioning of the `order−1` leading nodes. ──
4341    // The filtered distribution is fully proper from node order−1 onward (the
4342    // diffuse rank, = order, is consumed by node order−1), so ordinary RTS is
4343    // valid for t ≥ order−1. The first order−1 nodes are partially diffuse —
4344    // their filtered covariance still carries unresolved diffuse mass and the
4345    // RTS predicted-covariance inverse is singular there — and are recovered
4346    // exactly, jointly, by `leading_block_smooth` (conditioning the whole
4347    // leading block on the first proper smoothed node). For order = 1 there is
4348    // no leading node and RTS covers every node down to t = 0.
4349    let mut sm_state = vec![[0.0_f64; MAX_ORDER]; n];
4350    let mut sm_cov = vec![[[0.0_f64; MAX_ORDER]; MAX_ORDER]; n];
4351    let mut gains = vec![[[0.0_f64; MAX_ORDER]; MAX_ORDER]; n];
4352    sm_state[n - 1] = pass.steps[n - 1].a_filt;
4353    sm_cov[n - 1] = pass.steps[n - 1].p_filt;
4354    for t in (order - 1..n - 1).rev() {
4355        let p_next_pred = &pass.steps[t + 1].p_pred;
4356        let delta = nodes[t + 1].x - nodes[t].x;
4357        let f_t = transition(delta, order);
4358        let p_inv = mat_inv(p_next_pred, order, "RTS predicted covariance")?;
4359        let g = mat_mul(
4360            &mat_mul(&pass.steps[t].p_filt, &mat_t(&f_t, order), order),
4361            &p_inv,
4362            order,
4363        );
4364        let mut dm: Vec2 = [0.0; MAX_ORDER];
4365        for i in 0..order {
4366            dm[i] = sm_state[t + 1][i] - pass.steps[t + 1].a_pred[i];
4367        }
4368        let corr = mat_vec(&g, &dm, order);
4369        for i in 0..order {
4370            sm_state[t][i] = pass.steps[t].a_filt[i] + corr[i];
4371        }
4372        let dp = mat_sub(&sm_cov[t + 1], p_next_pred, order);
4373        let mut cov = mat_add(
4374            &pass.steps[t].p_filt,
4375            &mat_mul(&mat_mul(&g, &dp, order), &mat_t(&g, order), order),
4376            order,
4377        );
4378        symmetrize(&mut cov, order);
4379        sm_cov[t] = cov;
4380        gains[t] = g;
4381    }
4382    // The order−1 partially-diffuse leading nodes by exact joint conditioning
4383    // (the multi-node generalization of the m=2 reverse-Markov closure).
4384    if order >= 2 {
4385        leading_block_smooth(&mut sm_state, &mut sm_cov, &mut gains, &nodes, q, order)?;
4386    }
4387
4388    let knots: Vec<f64> = nodes.iter().map(|n| n.x).collect();
4389    let centered_mean: Vec<f64> = sm_state.iter().map(|s| s[0]).collect();
4390    // Weighted DATA residual sum of squares at the smoothed mean. Tied rows
4391    // pool exactly: Σᵢ wᵢ(yᵢ − f̂ₖ)² = Σᵢ wᵢ(yᵢ − ȳₖ)² + Σₖ Wₖ(ȳₖ − f̂ₖ)²
4392    // (within-tie scatter plus pooled-node misfit), so the raw rows the scan
4393    // does not retain are not needed.
4394    let data_sse = ssr_within
4395        + nodes
4396            .iter()
4397            .zip(centered_mean.iter())
4398            .map(|(node, &fhat)| {
4399                let r = node.y - fhat;
4400                node.w * r * r
4401            })
4402            .sum::<f64>();
4403    // Evaluate the ordinary weighted Gaussian likelihood while the original
4404    // row weights still exist. Pooled node weights preserve Σw but not Σln(w):
4405    // for tied rows, ln(w₁ + w₂) != ln(w₁) + ln(w₂), so this normalizer cannot
4406    // be reconstructed from the persisted state after pooling.
4407    let sum_log_weights = w.iter().map(|weight| weight.ln()).sum::<f64>();
4408    let log_likelihood = -0.5
4409        * (data_sse / sigma2
4410            + n_obs as f64 * (std::f64::consts::TAU.ln() + sigma2.ln())
4411            - sum_log_weights);
4412    if !log_likelihood.is_finite() {
4413        return Err(format!(
4414            "spline scan: weighted Gaussian log-likelihood is non-finite \
4415             (data_sse={data_sse}, sigma2={sigma2}, n={n_obs}, \
4416             sum_log_weights={sum_log_weights})"
4417        ));
4418    }
4419    // Cross back from the centered numerical chart only after every
4420    // translation-invariant quantity has been formed. Derivative states and
4421    // covariances are unchanged by a constant shift.
4422    for (index, state) in sm_state.iter_mut().enumerate() {
4423        state[0] += response_origin;
4424        if !state[0].is_finite() {
4425            return Err(format!(
4426                "spline scan: restored fitted response is non-finite at node {index}"
4427            ));
4428        }
4429    }
4430    let mean: Vec<f64> = sm_state.iter().map(|state| state[0]).collect();
4431    // f′ lives at state index 1 — present for order ≥ 2 only; the m = 1 latent
4432    // process (Brownian motion) has no derivative state to expose.
4433    let deriv: Option<Vec<f64>> =
4434        (order >= 2).then(|| sm_state.iter().map(|state| state[1]).collect());
4435    let var: Vec<f64> = sm_cov.iter().map(|p| p[0][0] * sigma2).collect();
4436    Ok(SplineScanFit {
4437        order,
4438        knots,
4439        mean,
4440        deriv,
4441        var,
4442        log_lambda,
4443        sigma2,
4444        restricted_loglik,
4445        log_likelihood,
4446        training_sample_size: std::num::NonZeroUsize::new(n_obs)
4447            .expect("pool_nodes requires at least one training row"),
4448        data_sse,
4449        smoothed_state: sm_state,
4450        smoothed_cov: sm_cov,
4451        rts_gain: gains,
4452        q,
4453        node_weight: nodes.iter().map(|n| n.w).collect(),
4454    })
4455}
4456
4457#[derive(Clone, Copy, Debug, PartialEq)]
4458enum SplineKktKind {
4459    LowerBoundary,
4460    UpperBoundary,
4461    Stationary { curvature: ClosedInterval },
4462}
4463
4464#[derive(Clone, Copy, Debug, PartialEq)]
4465enum SplineOptimumProof {
4466    Kkt {
4467        bracket: ClosedInterval,
4468        kind: SplineKktKind,
4469    },
4470    /// The producer proved the region's exact maximum indistinguishable from
4471    /// the representative at the point evaluator's certified comparison
4472    /// resolution. This is a successful typed optimum, not a failed
4473    /// stationary-point certificate.
4474    ResolutionFlat {
4475        bracket: ClosedInterval,
4476        max_score_gap: f64,
4477        score_resolution: f64,
4478    },
4479}
4480
4481/// Preserve the certified optimizer's proof category at the spline consumer
4482/// seam.
4483///
4484/// Boundary and stationary selections require their exact-real KKT proof
4485/// below. A [`ScoreOptimumLocation::ResolutionFlat`] selection instead carries
4486/// the producer's successful value-resolution theorem. Requiring a stationary
4487/// KKT certificate from that category contradicts its contract: its whole
4488/// purpose is that unresolved stationary structure is immaterial because the
4489/// maximum's excess over the representative does not exceed comparison
4490/// resolution.
4491fn spline_optimum_proof(
4492    search: &ScoreSearchResult,
4493) -> Result<SplineOptimumProof, SplineScoreProofError> {
4494    match search.location {
4495        ScoreOptimumLocation::LowerBoundary => Ok(SplineOptimumProof::Kkt {
4496            bracket: ClosedInterval::point(search.lower_boundary.x),
4497            kind: SplineKktKind::LowerBoundary,
4498        }),
4499        ScoreOptimumLocation::UpperBoundary => Ok(SplineOptimumProof::Kkt {
4500            bracket: ClosedInterval::point(search.upper_boundary.x),
4501            kind: SplineKktKind::UpperBoundary,
4502        }),
4503        ScoreOptimumLocation::Stationary(index) => {
4504            let stationary = search.stationary_points.get(index).ok_or_else(|| {
4505                SplineScoreProofError::Search(
4506                    "optimizer returned an invalid stationary-point index".to_string(),
4507                )
4508            })?;
4509            Ok(SplineOptimumProof::Kkt {
4510                bracket: stationary.bracket,
4511                kind: SplineKktKind::Stationary {
4512                    curvature: stationary.curvature,
4513                },
4514            })
4515        }
4516        ScoreOptimumLocation::ResolutionFlat(index) => {
4517            let flat = search.resolution_flat_regions.get(index).ok_or_else(|| {
4518                SplineScoreProofError::Search(
4519                    "optimizer returned an invalid resolution-flat index".to_string(),
4520                )
4521            })?;
4522            if !(flat.max_score_gap.is_finite()
4523                && flat.max_score_gap >= 0.0
4524                && flat.score_resolution.is_finite()
4525                && flat.score_resolution >= 0.0
4526                && flat.max_score_gap <= flat.score_resolution
4527                && flat.bracket.contains(search.optimum.x)
4528                && flat.sample.x.to_bits() == search.optimum.x.to_bits())
4529            {
4530                return Err(SplineScoreProofError::Search(format!(
4531                    "optimizer returned an invalid resolution-flat certificate: selected {}, \
4532                     representative {}, bracket {:?}, maximum score gap {}, score resolution {}",
4533                    search.optimum.x,
4534                    flat.sample.x,
4535                    flat.bracket,
4536                    flat.max_score_gap,
4537                    flat.score_resolution
4538                )));
4539            }
4540            Ok(SplineOptimumProof::ResolutionFlat {
4541                bracket: flat.bracket,
4542                max_score_gap: flat.max_score_gap,
4543                score_resolution: flat.score_resolution,
4544            })
4545        }
4546    }
4547}
4548
4549fn spline_kkt_holds(
4550    kind: SplineKktKind,
4551    final_enclosure: DerivativeEnclosure,
4552) -> (bool, ClosedInterval) {
4553    match kind {
4554        SplineKktKind::LowerBoundary => (
4555            final_enclosure.derivative.hi <= 0.0,
4556            final_enclosure.curvature,
4557        ),
4558        SplineKktKind::UpperBoundary => (
4559            final_enclosure.derivative.lo >= 0.0,
4560            final_enclosure.curvature,
4561        ),
4562        SplineKktKind::Stationary { curvature } => (
4563            // Recompute the final bracket's derivative containment, which
4564            // depends on its endpoint certificates. Preserve the producer's
4565            // strict curvature enclosure: it proved this root unique on a
4566            // parent cell and therefore remains valid on every contracted
4567            // subset, even if a fresh tiny-cell formula loses the sign to
4568            // cancellation.
4569            final_enclosure.derivative.contains_zero() && curvature.hi < 0.0,
4570            curvature,
4571        ),
4572    }
4573}
4574
4575/// Fit with `log λ` selected by the concentrated diffuse REML criterion.
4576/// Every stationary interval in the bounded, scale-equivariant log-λ domain
4577/// is isolated using analytic derivatives and rigorous interval bounds; the
4578/// two boundary/null-recovery candidates are evaluated exactly.
4579pub fn fit_spline_scan(
4580    x: &[f64],
4581    y: &[f64],
4582    w: &[f64],
4583    order: usize,
4584) -> Result<SplineScanFit, SplineScoreProofError> {
4585    if order == 0 || order > MAX_ORDER {
4586        return Err(SplineScoreProofError::InvalidInput(format!(
4587            "spline scan: order must be in 1..={MAX_ORDER}, got {order}"
4588        )));
4589    }
4590    let (nodes, ssr_within, n_obs, _response_origin) = pool_nodes(x, y, w, order)?;
4591    // Covariate-rescaling equivariance (#1214). The order-`m` IWP process noise
4592    // is `Q(δ) ∝ q · δ^{2m−1}`, so under an affine covariate rescale `x → a·x`
4593    // (all abscissa gaps `δ → a·δ`) the posterior `f(x)` is *exactly* invariant
4594    // iff the smoothing parameter co-transforms as `q → q / a^{2m−1}`, i.e.
4595    // `log λ → log λ + (2m−1)·log a` (λ = 1/q). The whole smoother — criterion,
4596    // fit, and the Gaussian-bridge `predict` — runs self-consistently in the raw
4597    // covariate units, so the *only* place covariate scale leaks in is this
4598    // outer `log λ` search: a fixed absolute bracket `[LOG_LAMBDA_LO,
4599    // LOG_LAMBDA_HI]` does not track the data span, so at small/large covariate
4600    // scale the equivariant optimum rails out of the bracket and the fit drifts.
4601    // Anchor the bracket to the data's own length scale: search `log λ` around
4602    // `(2m−1)·log L` where `L` is the abscissa span (which scales linearly with
4603    // the covariate), so the search is performed in scale-free units and the
4604    // selected `q · L^{2m−1}` — hence the posterior `f(x)` — is invariant.
4605    let first_x = nodes
4606        .first()
4607        .ok_or_else(|| {
4608            SplineScoreProofError::InvalidInput(
4609                "spline scan: pooled data unexpectedly contain no nodes".to_string(),
4610            )
4611        })?
4612        .x;
4613    let last_x = nodes
4614        .last()
4615        .ok_or_else(|| {
4616            SplineScoreProofError::InvalidInput(
4617                "spline scan: pooled data unexpectedly contain no nodes".to_string(),
4618            )
4619        })?
4620        .x;
4621    let span = last_x - first_x;
4622    if !(span.is_finite() && span > 0.0) {
4623        return Err(SplineScoreProofError::InvalidInput(format!(
4624            "spline scan: pooled covariate span must be finite and positive, got {span}"
4625        )));
4626    }
4627    let log_span = gam_math::score_opt::certified_ln_positive(span).ok_or(
4628        SplineScoreProofError::InvalidArithmetic {
4629            context: "covariate-span logarithm",
4630        },
4631    )?;
4632    let log_span_representative = log_span.lo + 0.5 * (log_span.hi - log_span.lo);
4633    let scale_shift = (2 * order - 1) as f64 * log_span_representative;
4634    let lo_anchor = LOG_LAMBDA_LO + scale_shift;
4635    let hi_anchor = LOG_LAMBDA_HI + scale_shift;
4636    let n_nodes = nodes.len();
4637    let endpoint_certificates = RefCell::new(HashMap::<u64, CertifiedCriterionJet>::new());
4638    let search = maximize_score_1d(
4639        lo_anchor,
4640        hi_anchor,
4641        f64::EPSILON.sqrt(),
4642        |ll| {
4643            let certificate =
4644                certified_concentrated_criterion_jet(&nodes, ssr_within, n_obs, ll, order)?;
4645            endpoint_certificates
4646                .borrow_mut()
4647                .insert(ll.to_bits(), certificate);
4648            Ok(certificate.jet)
4649        },
4650        |left, right| {
4651            let certificates = endpoint_certificates.borrow();
4652            let left_certificate = certificates
4653                .get(&left.x.to_bits())
4654                .copied()
4655                .ok_or(SplineScoreProofError::MissingEndpointCertificate { log_lambda: left.x })?;
4656            let right_certificate = certificates.get(&right.x.to_bits()).copied().ok_or(
4657                SplineScoreProofError::MissingEndpointCertificate {
4658                    log_lambda: right.x,
4659                },
4660            )?;
4661            concentrated_criterion_enclosure(
4662                n_nodes,
4663                n_obs,
4664                left,
4665                right,
4666                left_certificate,
4667                right_certificate,
4668                order,
4669            )
4670        },
4671    )
4672    .map_err(|error| match error {
4673        gam_math::score_opt::ScoreSearchError::PointEvaluation { source, .. }
4674        | gam_math::score_opt::ScoreSearchError::EnclosureEvaluation { source, .. } => source,
4675        other => SplineScoreProofError::Search(other.to_string()),
4676    })?;
4677    if search.value_certificate.maximum_excess > search.value_certificate.comparison_resolution {
4678        return Err(SplineScoreProofError::GlobalValueOrderingUnresolved {
4679            maximum_excess: search.value_certificate.maximum_excess,
4680            comparison_resolution: search.value_certificate.comparison_resolution,
4681        });
4682    }
4683    match spline_optimum_proof(&search)? {
4684        SplineOptimumProof::Kkt {
4685            bracket: kkt_bracket,
4686            kind: kkt_kind,
4687        } => {
4688            let kkt_enclosure = {
4689                let certificates = endpoint_certificates.borrow();
4690                let left_certificate = certificates.get(&kkt_bracket.lo.to_bits()).copied().ok_or(
4691                    SplineScoreProofError::MissingEndpointCertificate {
4692                        log_lambda: kkt_bracket.lo,
4693                    },
4694                )?;
4695                let right_certificate = certificates
4696                    .get(&kkt_bracket.hi.to_bits())
4697                    .copied()
4698                    .ok_or(SplineScoreProofError::MissingEndpointCertificate {
4699                        log_lambda: kkt_bracket.hi,
4700                    })?;
4701                let sample = |log_lambda: f64, certificate: CertifiedCriterionJet| ScoreSample {
4702                    x: log_lambda,
4703                    value: certificate.jet.value,
4704                    derivative: certificate.jet.derivative,
4705                    curvature: certificate.jet.curvature,
4706                    third: certificate.jet.third,
4707                };
4708                concentrated_criterion_enclosure(
4709                    n_nodes,
4710                    n_obs,
4711                    sample(kkt_bracket.lo, left_certificate),
4712                    sample(kkt_bracket.hi, right_certificate),
4713                    left_certificate,
4714                    right_certificate,
4715                    order,
4716                )?
4717            };
4718            let (kkt_holds, kkt_curvature) = spline_kkt_holds(kkt_kind, kkt_enclosure);
4719            if !kkt_holds {
4720                return Err(SplineScoreProofError::OptimumKktUncertified {
4721                    location: search.location,
4722                    bracket: kkt_bracket,
4723                    derivative: kkt_enclosure.derivative,
4724                    curvature: kkt_curvature,
4725                });
4726            }
4727        }
4728        SplineOptimumProof::ResolutionFlat {
4729            bracket,
4730            max_score_gap,
4731            score_resolution,
4732        } => {
4733            log::debug!(
4734                "spline scan: accepting certified resolution-flat REML optimum on \
4735                 {bracket:?}; maximum score excess {max_score_gap:e} <= comparison \
4736                 resolution {score_resolution:e}"
4737            );
4738        }
4739    }
4740    // The fixed-λ fitter below consumes the historical scalar recurrence.
4741    // Before crossing that seam, independently re-evaluate the selected point
4742    // and require every scalar component to lie in the directed ball that won
4743    // the search. This is one O(n) pass per completed fit, not per search cell.
4744    let selected_certificate = endpoint_certificates
4745        .borrow()
4746        .get(&search.optimum.x.to_bits())
4747        .copied()
4748        .ok_or_else(|| {
4749            SplineScoreProofError::Search(format!(
4750                "spline scan: selected log lambda {} has no cached score certificate",
4751                search.optimum.x
4752            ))
4753        })?;
4754    let independent =
4755        concentrated_criterion_jet(&nodes, ssr_within, n_obs, search.optimum.x, order)
4756            .map_err(SplineScoreProofError::Computation)?;
4757    for (name, ball, scalar) in [
4758        ("value", selected_certificate.value, independent.0),
4759        ("derivative", selected_certificate.derivative, independent.1),
4760        ("curvature", selected_certificate.curvature, independent.2),
4761        ("third", selected_certificate.third, independent.3),
4762    ] {
4763        if !ball.interval().contains(scalar) {
4764            return Err(SplineScoreProofError::Computation(format!(
4765                "spline scan: selected {name} scalar {scalar} escapes its directed score ball {:?}",
4766                ball.interval()
4767            )));
4768        }
4769    }
4770    fit_spline_scan_at(x, y, w, search.optimum.x, None, order)
4771        .map_err(SplineScoreProofError::Computation)
4772}
4773
4774/// Lossless serializable snapshot of a [`SplineScanFit`] (#1034).
4775///
4776/// Carries exactly the smoother state the Gaussian-bridge `predict` replays:
4777/// pooled knots, smoothed `(f, f′, …, f^{(m−1)})` states (`m` per knot),
4778/// smoothed state covariances (unit-σ² scale, symmetric — stored as the
4779/// upper triangle row-major, `m(m+1)/2` per knot), RTS backward gains (full
4780/// `m×m` row-major — gains are NOT symmetric), pooled node weights, and the
4781/// required fit scalars. `q = e^{−log λ}` and the public `mean`/`deriv`/`var`
4782/// views are derived on restore rather than stored, so a snapshot cannot go
4783/// internally inconsistent. Every field is required: a snapshot that predates
4784/// the current statistical contract must be regenerated rather than guessed.
4785#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
4786pub struct SplineScanState {
4787    /// Smoothing-spline order `m ∈ {1, 2, 3}`.
4788    pub order: usize,
4789    pub knots: Vec<f64>,
4790    /// Smoothed `(f, f′, …, f^{(m−1)})` per knot, row-major (`m` per knot).
4791    pub state: Vec<f64>,
4792    /// Smoothed covariance per knot at unit-σ² scale, upper triangle row-major
4793    /// (`m(m+1)/2` per knot): `[c00, c01, …, c0,m−1, c11, …, c_{m−1,m−1}]`.
4794    pub cov: Vec<f64>,
4795    /// RTS backward gain per knot, full `m×m` row-major (`m²` per knot); the
4796    /// last knot's gain is structurally unused and stored as written.
4797    pub gain: Vec<f64>,
4798    /// Pooled (tied-abscissa summed) observation weight per knot.
4799    pub node_weight: Vec<f64>,
4800    pub log_lambda: f64,
4801    pub sigma2: f64,
4802    pub restricted_loglik: f64,
4803    /// Ordinary fully normalized weighted Gaussian log-likelihood. Required on
4804    /// the wire because the raw per-row weights needed for its normalizer are
4805    /// deliberately not retained after tied abscissae are pooled.
4806    pub log_likelihood: f64,
4807    /// Original training row count. Required on the wire.
4808    pub training_sample_size: std::num::NonZeroU64,
4809    /// Weighted data residual sum of squares `Σ wᵢ (yᵢ − f̂(xᵢ))²` at the
4810    /// smoothed mean — the Gaussian deviance. Stored because it cannot be
4811    /// recovered from the profiled σ² (whose quadratic also carries
4812    /// process/roughness energy) and the raw rows are not retained.
4813    pub data_sse: f64,
4814}
4815
4816impl SplineScanFit {
4817    /// Snapshot the full smoother state for persistence (#1034).
4818    pub fn to_state(&self) -> SplineScanState {
4819        let order = self.order;
4820        let tri = order * (order + 1) / 2;
4821        let nk = self.knots.len();
4822        let mut state = Vec::with_capacity(order * nk);
4823        for s in &self.smoothed_state {
4824            state.extend_from_slice(&s[..order]);
4825        }
4826        let mut cov = Vec::with_capacity(tri * nk);
4827        for c in &self.smoothed_cov {
4828            for i in 0..order {
4829                for j in i..order {
4830                    cov.push(c[i][j]);
4831                }
4832            }
4833        }
4834        let mut gain = Vec::with_capacity(order * order * nk);
4835        for g in &self.rts_gain {
4836            for i in 0..order {
4837                for j in 0..order {
4838                    gain.push(g[i][j]);
4839                }
4840            }
4841        }
4842        SplineScanState {
4843            order: self.order,
4844            knots: self.knots.clone(),
4845            state,
4846            cov,
4847            gain,
4848            node_weight: self.node_weight.clone(),
4849            log_lambda: self.log_lambda,
4850            sigma2: self.sigma2,
4851            restricted_loglik: self.restricted_loglik,
4852            log_likelihood: self.log_likelihood,
4853            training_sample_size: std::num::NonZeroU64::new(
4854                u64::try_from(self.training_sample_size.get())
4855                    .expect("SplineScanFit row count exceeds the persistence format"),
4856            )
4857            .expect("SplineScanFit construction requires training rows"),
4858            data_sse: self.data_sse,
4859        }
4860    }
4861
4862    /// Rebuild the exact in-memory fit from a persisted snapshot (#1034).
4863    ///
4864    /// Validates shape, finiteness, strict knot ordering, positive weights and
4865    /// σ², so a corrupt payload fails loudly here instead of inside a later
4866    /// `predict`. The restored fit replays the Gaussian bridge bit-for-bit:
4867    /// every field `predict`/`edf`/`deriv_at_knot` reads is either stored
4868    /// verbatim or derived by the same expressions the fitter uses.
4869    pub fn from_state(state: &SplineScanState) -> Result<Self, String> {
4870        let order = state.order;
4871        if order == 0 || order > MAX_ORDER {
4872            return Err(format!(
4873                "spline scan state: order must be in 1..={MAX_ORDER}, got {order}"
4874            ));
4875        }
4876        let m = state.knots.len();
4877        if m < order + 1 {
4878            return Err(format!(
4879                "spline scan state: order {order} needs at least {} knots, got {m}",
4880                order + 1
4881            ));
4882        }
4883        let tri = order * (order + 1) / 2;
4884        if state.state.len() != order * m
4885            || state.cov.len() != tri * m
4886            || state.gain.len() != order * order * m
4887            || state.node_weight.len() != m
4888        {
4889            return Err(format!(
4890                "spline scan state: inconsistent lengths (order={order}, m={m}, state={}, cov={}, gain={}, weights={})",
4891                state.state.len(),
4892                state.cov.len(),
4893                state.gain.len(),
4894                state.node_weight.len()
4895            ));
4896        }
4897        let all = state
4898            .state
4899            .iter()
4900            .chain(&state.cov)
4901            .chain(&state.gain)
4902            .chain(&state.knots)
4903            .chain(&state.node_weight);
4904        for (i, v) in all.enumerate() {
4905            if !v.is_finite() {
4906                return Err(format!("spline scan state: non-finite entry at {i}"));
4907            }
4908        }
4909        gam_problem::validate_log_strength(state.log_lambda)
4910            .map_err(|error| format!("spline scan state: {error}"))?;
4911        if !(state.restricted_loglik.is_finite()
4912            && state.log_likelihood.is_finite()
4913            && state.sigma2.is_finite()
4914            && state.sigma2 > 0.0)
4915        {
4916            return Err(format!(
4917                "spline scan state: invalid scalars (log_lambda={}, sigma2={}, \
4918                 restricted_loglik={}, log_likelihood={})",
4919                state.log_lambda, state.sigma2, state.restricted_loglik, state.log_likelihood
4920            ));
4921        }
4922        if !(state.data_sse.is_finite() && state.data_sse >= 0.0) {
4923            return Err(format!(
4924                "spline scan state: invalid data_sse {}",
4925                state.data_sse
4926            ));
4927        }
4928        if state.knots.windows(2).any(|kk| !(kk[0] < kk[1])) {
4929            return Err("spline scan state: knots must be strictly increasing".to_string());
4930        }
4931        if state.node_weight.iter().any(|&w| w <= 0.0) {
4932            return Err("spline scan state: node weights must be positive".to_string());
4933        }
4934        let smoothed_state: Vec<Vec2> = state
4935            .state
4936            .chunks_exact(order)
4937            .map(|s| {
4938                let mut v = [0.0_f64; MAX_ORDER];
4939                v[..order].copy_from_slice(s);
4940                v
4941            })
4942            .collect();
4943        let smoothed_cov: Vec<Mat2> = state
4944            .cov
4945            .chunks_exact(tri)
4946            .map(|c| {
4947                let mut mm = [[0.0_f64; MAX_ORDER]; MAX_ORDER];
4948                let mut idx = 0;
4949                for i in 0..order {
4950                    for j in i..order {
4951                        mm[i][j] = c[idx];
4952                        mm[j][i] = c[idx];
4953                        idx += 1;
4954                    }
4955                }
4956                mm
4957            })
4958            .collect();
4959        let rts_gain: Vec<Mat2> = state
4960            .gain
4961            .chunks_exact(order * order)
4962            .map(|g| {
4963                let mut mm = [[0.0_f64; MAX_ORDER]; MAX_ORDER];
4964                for i in 0..order {
4965                    for j in 0..order {
4966                        mm[i][j] = g[i * order + j];
4967                    }
4968                }
4969                mm
4970            })
4971            .collect();
4972        let sigma2 = state.sigma2;
4973        let training_sample_size =
4974            usize::try_from(state.training_sample_size.get()).map_err(|_| {
4975                format!(
4976                    "spline scan state: training_sample_size {} exceeds this platform's usize",
4977                    state.training_sample_size
4978                )
4979            })?;
4980        Ok(Self {
4981            order,
4982            knots: state.knots.clone(),
4983            mean: smoothed_state.iter().map(|s| s[0]).collect(),
4984            deriv: (order >= 2).then(|| smoothed_state.iter().map(|s| s[1]).collect()),
4985            var: smoothed_cov.iter().map(|c| c[0][0] * sigma2).collect(),
4986            log_lambda: state.log_lambda,
4987            sigma2,
4988            restricted_loglik: state.restricted_loglik,
4989            log_likelihood: state.log_likelihood,
4990            training_sample_size: std::num::NonZeroUsize::new(training_sample_size)
4991                .expect("nonzero wire count remains nonzero after conversion"),
4992            data_sse: state.data_sse,
4993            smoothed_state,
4994            smoothed_cov,
4995            rts_gain,
4996            q: gam_problem::checked_exp_log_strength(-state.log_lambda)
4997                .map_err(|error| format!("spline scan inverse log strength: {error}"))?,
4998            node_weight: state.node_weight.clone(),
4999        })
5000    }
5001
5002    /// Exact posterior `(mean, variance)` of `f` at an arbitrary abscissa.
5003    ///
5004    /// Interior points use the Gaussian bridge conditional on the two flanking
5005    /// smoothed states with the exact lag-one smoothed cross-covariance
5006    /// `Cov(α_t, α_{t+1} | y) = G_t · P^s_{t+1}`; exterior points extrapolate
5007    /// from the boundary state (linear mean, cubically growing variance).
5008    pub fn predict(&self, x_new: f64) -> Result<(f64, f64), String> {
5009        if !x_new.is_finite() {
5010            return Err("spline scan: non-finite prediction abscissa".to_string());
5011        }
5012        let n = self.knots.len();
5013        let order = self.order;
5014        let first = self.knots[0];
5015        let last = self.knots[n - 1];
5016        if x_new <= first {
5017            let delta = first - x_new;
5018            // Backward extrapolation through the reverse map α(x) = F⁻¹(α₁ − η).
5019            let f_t = transition(delta, order);
5020            let f_inv = mat_inv(&f_t, order, "backward extrapolation transition")?;
5021            let mean_s = mat_vec(&f_inv, &self.smoothed_state[0], order);
5022            let qm = process_noise(delta, self.q, order);
5023            let cov = mat_add(
5024                &mat_mul(
5025                    &mat_mul(&f_inv, &self.smoothed_cov[0], order),
5026                    &mat_t(&f_inv, order),
5027                    order,
5028                ),
5029                &mat_mul(&mat_mul(&f_inv, &qm, order), &mat_t(&f_inv, order), order),
5030                order,
5031            );
5032            return Ok((mean_s[0], cov[0][0] * self.sigma2));
5033        }
5034        if x_new >= last {
5035            let delta = x_new - last;
5036            let f_t = transition(delta, order);
5037            let mean_s = mat_vec(&f_t, &self.smoothed_state[n - 1], order);
5038            let cov = mat_add(
5039                &mat_mul(
5040                    &mat_mul(&f_t, &self.smoothed_cov[n - 1], order),
5041                    &mat_t(&f_t, order),
5042                    order,
5043                ),
5044                &process_noise(delta, self.q, order),
5045                order,
5046            );
5047            return Ok((mean_s[0], cov[0][0] * self.sigma2));
5048        }
5049        // Flanking knot interval via binary search.
5050        let t = match self.knots.binary_search_by(|k| k.total_cmp(&x_new)) {
5051            Ok(idx) => return Ok((self.mean[idx], self.var[idx])),
5052            Err(idx) => idx - 1,
5053        };
5054        let (xa, xb) = (self.knots[t], self.knots[t + 1]);
5055        let (d1, d2) = (x_new - xa, xb - x_new);
5056        let (f1m, f2m) = (transition(d1, order), transition(d2, order));
5057        let (q1, q2) = (
5058            process_noise(d1, self.q, order),
5059            process_noise(d2, self.q, order),
5060        );
5061        let q1_inv = mat_inv(&q1, order, "bridge left noise")?;
5062        let q2_inv = mat_inv(&q2, order, "bridge right noise")?;
5063        // p(α* | α_t, α_{t+1}) ∝ N(α*; F₁α_t, Q₁)·N(α_{t+1}; F₂α*, Q₂):
5064        //   Λ = Q₁⁻¹ + F₂ᵀQ₂⁻¹F₂,  mean = Λ⁻¹(Q₁⁻¹F₁ α_t + F₂ᵀQ₂⁻¹ α_{t+1}).
5065        let lambda = mat_add(
5066            &q1_inv,
5067            &mat_mul(&mat_mul(&mat_t(&f2m, order), &q2_inv, order), &f2m, order),
5068            order,
5069        );
5070        let lam_inv = mat_inv(&lambda, order, "bridge precision")?;
5071        let ca = mat_mul(&lam_inv, &mat_mul(&q1_inv, &f1m, order), order);
5072        let cb = mat_mul(
5073            &lam_inv,
5074            &mat_mul(&mat_t(&f2m, order), &q2_inv, order),
5075            order,
5076        );
5077        let ma = mat_vec(&ca, &self.smoothed_state[t], order);
5078        let mb = mat_vec(&cb, &self.smoothed_state[t + 1], order);
5079        let mut mean_s = [0.0_f64; MAX_ORDER];
5080        for i in 0..order {
5081            mean_s[i] = ma[i] + mb[i];
5082        }
5083        // Push the joint smoothed covariance of (α_t, α_{t+1}) through the
5084        // affine map: cross term uses Cov(α_t, α_{t+1}|y) = G_t · P^s_{t+1}.
5085        let cross = mat_mul(&self.rts_gain[t], &self.smoothed_cov[t + 1], order);
5086        let mut cov = mat_add(
5087            &mat_add(
5088                &mat_mul(
5089                    &mat_mul(&ca, &self.smoothed_cov[t], order),
5090                    &mat_t(&ca, order),
5091                    order,
5092                ),
5093                &mat_mul(
5094                    &mat_mul(&cb, &self.smoothed_cov[t + 1], order),
5095                    &mat_t(&cb, order),
5096                    order,
5097                ),
5098                order,
5099            ),
5100            &lam_inv,
5101            order,
5102        );
5103        let cab = mat_mul(&mat_mul(&ca, &cross, order), &mat_t(&cb, order), order);
5104        cov = mat_add(&cov, &mat_add(&cab, &mat_t(&cab, order), order), order);
5105        symmetrize(&mut cov, order);
5106        Ok((mean_s[0], cov[0][0] * self.sigma2))
5107    }
5108
5109    /// Exact effective degrees of freedom of the fitted smoother.
5110    ///
5111    /// For a Gaussian smoother the influence (hat) matrix is
5112    /// `S = Cov_post · W / σ²` (posterior mean is linear in `y` with that
5113    /// exact coefficient matrix), so
5114    /// `EDF = tr(S) = tr(W · Cov_post) / σ² = Σ_t w_t · Var_smoothed(f_t) / σ²`.
5115    /// This is the standard Gaussian-process identity — no second smoother
5116    /// pass and no approximation. Tied abscissae pool exactly: each raw row
5117    /// `i` in tie-group `k` contributes `∂f̂(x_k)/∂y_i = C̃_kk · w_i` (the
5118    /// pooled mean `ȳ_k` is precision-weighted), so the raw-row trace
5119    /// `Σ_i w_i · C̃_{k(i),k(i)}` collapses to `Σ_k W_k · C̃_kk` with the
5120    /// pooled weights `W_k`. `smoothed_cov` is stored at unit-σ² scale
5121    /// (`C̃ = Cov_post / σ²`), so the σ² factors cancel exactly.
5122    pub fn edf(&self) -> f64 {
5123        self.node_weight
5124            .iter()
5125            .zip(self.smoothed_cov.iter())
5126            .map(|(w, c)| w * c[0][0])
5127            .sum()
5128    }
5129
5130    /// Selected smoothing parameter `λ = e^{log λ}` (#1046).
5131    pub fn lambda(&self) -> f64 {
5132        gam_problem::checked_exp_log_strength(self.log_lambda)
5133            .expect("SplineScanFit construction validates its private log strength")
5134    }
5135
5136    pub fn log_lambda(&self) -> f64 {
5137        self.log_lambda
5138    }
5139
5140    /// Number of original training rows / experimental units.
5141    pub fn training_sample_size(&self) -> usize {
5142        self.training_sample_size.get()
5143    }
5144
5145    /// Gaussian deviance — the weighted DATA residual sum of squares
5146    /// `Σ wᵢ(yᵢ − f̂ᵢ)²` at the smoothed mean (#1046). This is the stored
5147    /// `data_sse`, computed against the fitted values at fit time. It is NOT
5148    /// `σ̂²·(n − order)`: the profiled σ² divides the REML innovations
5149    /// quadratic, which is data residual energy PLUS process/roughness energy
5150    /// at the posterior mode (for order 1 on `x = (0,1)`, `y = (0,1)`, unit
5151    /// weights and λ = 1 the posterior mean is `(1/3, 2/3)`; the data SSE is
5152    /// 2/9 while `σ̂²·(n − order) = 1/3`, the extra 1/9 being penalty energy).
5153    pub fn deviance(&self) -> f64 {
5154        self.data_sse
5155    }
5156}
5157
5158#[cfg(test)]
5159mod tests {
5160    /// Seed a covariance zonotope without throwing away exact symmetry.
5161    ///
5162    /// The two off-diagonal storage locations denote one real covariance entry.
5163    /// A shared generator therefore encloses their common error while preserving
5164    /// that identity; two independent axis generators would immediately forget it
5165    /// and recreate the componentwise wrapping effect on the first congruence.
5166    fn covariance_zonotope_from_symmetric_matrix(
5167        matrix: &BallMat,
5168        order: usize,
5169    ) -> Zonotope<COVARIANCE_D1_DIM> {
5170        let mut state = Zonotope::<COVARIANCE_D1_DIM>::zeroed(order * order);
5171        for i in 0..order {
5172            for j in i..order {
5173                let value = matrix[i][j].value;
5174                state.center[i * order + j] = value;
5175                state.center[j * order + i] = value;
5176                let radius = [
5177                    (value - matrix[i][j].lo).abs(),
5178                    (matrix[i][j].hi - value).abs(),
5179                    (value - matrix[j][i].lo).abs(),
5180                    (matrix[j][i].hi - value).abs(),
5181                ]
5182                .into_iter()
5183                .fold(0.0_f64, f64::max);
5184                if radius > 0.0 {
5185                    let mut generator = [0.0_f64; COVARIANCE_D1_DIM];
5186                    let radius = next_up_ball(radius);
5187                    generator[i * order + j] = radius;
5188                    generator[j * order + i] = radius;
5189                    state.generators.push(generator);
5190                }
5191            }
5192        }
5193        state
5194    }
5195
5196    /// Compaction must preserve the signed directions that make a contracting
5197    /// recursion contract.  Fresh roundoff enters as axis generators, so age
5198    /// based reduction used to fold this old `[1, -1]` direction first and
5199    /// replace it by the expanding box `[±1] × [±1]`.
5200    #[test]
5201    fn zonotope_compaction_retains_correlation_before_axis_roundoff() {
5202        let mut state = Zonotope::<2>::zeroed(2);
5203        state.generators.push([1.0, -1.0]);
5204        for i in 0..ZONOTOPE_GENERATOR_CAP {
5205            state
5206                .generators
5207                .push(if i % 2 == 0 { [0.25, 0.0] } else { [0.0, 0.25] });
5208        }
5209
5210        state.compact();
5211
5212        assert!(state.generators.len() <= ZONOTOPE_GENERATOR_CAP);
5213        assert!(
5214            state
5215                .generators
5216                .iter()
5217                .any(|generator| *generator == [1.0, -1.0]),
5218            "compaction discarded the only signed correlation direction"
5219        );
5220    }
5221
5222    /// Two occurrences of `qQ` contain ONE uncertain `q`, not two independent
5223    /// interval choices. The distinguished coefficient must therefore add
5224    /// under the identity and cancel under an opposing signed map. Turning
5225    /// each occurrence into a fresh axis generator leaves radius `2|g|` in the
5226    /// cancellation arm and cannot prove the exact identity.
5227    #[test]
5228    fn shared_q_process_noise_injections_accumulate_and_cancel_as_one_generator() {
5229        let q = Ball {
5230            value: 10.0,
5231            lo: 9.0,
5232            hi: 11.0,
5233        };
5234        let noise = ball_process_noise_taylor(Ball::exact(2.0), q, 1);
5235        let g = noise.shared_q[0];
5236        assert!(g > 0.0);
5237
5238        let identity = zonotope_identity_map::<COVARIANCE_D1_DIM>(1);
5239        let mut accumulated = Zonotope::<COVARIANCE_D1_DIM>::zeroed(1);
5240        assert!(accumulated.apply_with_shared_q(&identity, &noise.constant, &noise.shared_q,));
5241        assert!(accumulated.apply_with_shared_q(&identity, &noise.constant, &noise.shared_q,));
5242        assert_eq!(accumulated.shared_q[0], 2.0 * g);
5243
5244        let mut negative_identity = [[Ball::ZERO; COVARIANCE_D1_DIM]; COVARIANCE_D1_DIM];
5245        negative_identity[0][0] = Ball::exact(-1.0);
5246        let mut cancelled = Zonotope::<COVARIANCE_D1_DIM>::zeroed(1);
5247        assert!(cancelled.apply_with_shared_q(&identity, &noise.constant, &noise.shared_q,));
5248        assert!(cancelled.apply_with_shared_q(
5249            &negative_identity,
5250            &noise.constant,
5251            &noise.shared_q,
5252        ));
5253        assert_eq!(cancelled.shared_q[0], 0.0);
5254
5255        let old_independent_radius = 2.0 * g.abs();
5256        assert!(
5257            ball_radius_about_value(cancelled.coordinate(0)) < old_independent_radius * 1.0e-10,
5258            "independent qQ axes would retain radius {old_independent_radius:e}, \
5259             but the shared-q cancellation left {:?}",
5260            cancelled.coordinate(0),
5261        );
5262    }
5263
5264    #[test]
5265    fn centred_riccati_zonotope_contains_an_off_centre_covariance_and_noise() {
5266        let centres = [[4.0, 1.0, 0.3], [1.0, 3.0, 0.2], [0.3, 0.2, 2.0]];
5267        let radii = [[0.2, 0.1, 0.08], [0.1, 0.2, 0.07], [0.08, 0.07, 0.2]];
5268        let mut enclosure = [[Ball::ZERO; MAX_ORDER]; MAX_ORDER];
5269        for i in 0..MAX_ORDER {
5270            for j in 0..MAX_ORDER {
5271                enclosure[i][j] = Ball {
5272                    value: centres[i][j],
5273                    lo: centres[i][j] - radii[i][j],
5274                    hi: centres[i][j] + radii[i][j],
5275                };
5276            }
5277        }
5278        let mut state = covariance_zonotope_from_symmetric_matrix(&enclosure, MAX_ORDER);
5279        let observation_variance = Ball {
5280            value: 1.2,
5281            lo: 1.1,
5282            hi: 1.3,
5283        };
5284        assert!(covariance_zonotope_measurement_update(
5285            &mut state,
5286            observation_variance,
5287            MAX_ORDER,
5288        ));
5289
5290        let actual = [[4.1, 0.95, 0.35], [0.95, 3.1, 0.15], [0.35, 0.15, 1.9]];
5291        let actual_r = 1.25;
5292        let innovation = actual[0][0] + actual_r;
5293        for i in 0..MAX_ORDER {
5294            for j in 0..MAX_ORDER {
5295                let updated = actual[i][j] - actual[i][0] * actual[0][j] / innovation;
5296                assert!(
5297                    state
5298                        .coordinate(i * MAX_ORDER + j)
5299                        .interval()
5300                        .contains(updated),
5301                    "updated covariance ({i},{j})={updated} escaped {:?}",
5302                    state.coordinate(i * MAX_ORDER + j).interval()
5303                );
5304            }
5305        }
5306    }
5307
5308    /// Diagnostic reproduction of the #2300 weighted-scan non-termination:
5309    /// the exact acceptance DGP (n=180, step weights 1/9), with the SAME
5310    /// certified search `fit_spline_scan` runs — but through a counting
5311    /// wrapper that bails out with the evaluation count and the stuck
5312    /// abscissa once the search exceeds a budget no terminating search on a
5313    /// 36-wide bracket can legitimately need. A pass proves termination in
5314    /// bounded work; the panic message is the diagnosis.
5315    #[test]
5316    fn weighted_scan_dgp_2300_search_terminates_in_bounded_evaluations() {
5317        // Deterministic stand-in for the acceptance DGP (xorshift Box-Muller;
5318        // the hang class is structural, not noise-realization-specific). Shared
5319        // with the `d3` enclosure diagnostic so the two cannot drift apart.
5320        let (x, y, w) = dgp_2300();
5321        // Every smoothing order, not just the cubic: the order-3 (quintic)
5322        // search has a deeper λ→∞ tail walk (scale shift (2m−1)·log L) and a
5323        // larger residual-d.f. Lipschitz constant, and was the remaining
5324        // effective hang after the order-2 fix (#2300 — the degree-5
5325        // observation-interval node timed out at 1500s). Endpoint-pair V‴
5326        // interpolation certifies its tail at fourth-order rate, so a uniform
5327        // budget far below the pre-fix eval counts must hold at all orders.
5328        //
5329        // The three orders are mathematically independent. Run them as three
5330        // scoped, single-core lanes so this regression's wall time is the
5331        // maximum order cost instead of their sum; three workers are negligible
5332        // on the remote validation nodes and avoid turning a performance test
5333        // into its own serial bottleneck.
5334        std::thread::scope(|scope| {
5335            for order in 1..=MAX_ORDER {
5336                let (x, y, w) = (&x, &y, &w);
5337                scope.spawn(move || {
5338                    let (nodes, ssr_within, n_obs, _response_origin) =
5339                        pool_nodes(x, y, w, order).expect("pool");
5340                    let span = nodes.last().unwrap().x - nodes.first().unwrap().x;
5341                    let scale_shift = (2 * order - 1) as f64 * span.ln();
5342                    let lo = LOG_LAMBDA_LO + scale_shift;
5343                    let hi = LOG_LAMBDA_HI + scale_shift;
5344
5345                    let n_nodes = nodes.len();
5346                    let evals = std::cell::Cell::new(0u64);
5347                    let last_x = std::cell::Cell::new(f64::NAN);
5348                    let endpoint_certificates =
5349                        RefCell::new(HashMap::<u64, CertifiedCriterionJet>::new());
5350                    let budget = 4_096u64;
5351                    let result = gam_math::score_opt::maximize_score_1d(
5352                        lo,
5353                        hi,
5354                        f64::EPSILON.sqrt(),
5355                        |ll| {
5356                            let count = evals.get() + 1;
5357                            evals.set(count);
5358                            last_x.set(ll);
5359                            assert!(
5360                                count <= budget,
5361                                "order-{order} certified scan search exceeded {budget} criterion \
5362                                 evaluations (last log-lambda sample {ll:.9}; bracket \
5363                                 [{lo:.3}, {hi:.3}]) — non-terminating subdivision reproduced"
5364                            );
5365                            let certificate = certified_concentrated_criterion_jet(
5366                                &nodes, ssr_within, n_obs, ll, order,
5367                            )?;
5368                            endpoint_certificates
5369                                .borrow_mut()
5370                                .insert(ll.to_bits(), certificate);
5371                            Ok(certificate.jet)
5372                        },
5373                        |a, b| {
5374                            let certificates = endpoint_certificates.borrow();
5375                            let left = certificates.get(&a.x.to_bits()).copied().ok_or(
5376                                SplineScoreProofError::MissingEndpointCertificate {
5377                                    log_lambda: a.x,
5378                                },
5379                            )?;
5380                            let right = certificates.get(&b.x.to_bits()).copied().ok_or(
5381                                SplineScoreProofError::MissingEndpointCertificate {
5382                                    log_lambda: b.x,
5383                                },
5384                            )?;
5385                            concentrated_criterion_enclosure(
5386                                n_nodes, n_obs, a, b, left, right, order,
5387                            )
5388                        },
5389                    );
5390                    match result {
5391                        Ok(search) => assert!(
5392                            search.optimum.x.is_finite(),
5393                            "order-{order} search must return a finite optimum"
5394                        ),
5395                        Err(error) => panic!(
5396                            "order-{order} weighted scan search failed after {} evaluations \
5397                             (last x {:.9}): {error:?}",
5398                            evals.get(),
5399                            last_x.get()
5400                        ),
5401                    }
5402                });
5403            }
5404        });
5405    }
5406
5407    /// The #2300 weighted-scan DGP, as its own function so the certified-search
5408    /// test and the `d3` enclosure diagnostics below read the SAME data rather
5409    /// than two copies that can drift apart.
5410    fn dgp_2300() -> (Vec<f64>, Vec<f64>, Vec<f64>) {
5411        let n = 180usize;
5412        let mut state: u64 = 0x2300_2300_2300_2300;
5413        let mut next_unit = move || {
5414            state ^= state << 13;
5415            state ^= state >> 7;
5416            state ^= state << 17;
5417            (state >> 11) as f64 / (1u64 << 53) as f64
5418        };
5419        let mut x = Vec::with_capacity(n);
5420        let mut y = Vec::with_capacity(n);
5421        let mut w = Vec::with_capacity(n);
5422        for i in 0..n {
5423            let xi = -2.0 + 4.0 * (i as f64) / ((n - 1) as f64);
5424            let wi: f64 = if xi < 0.0 { 1.0 } else { 9.0 };
5425            let u1 = next_unit().max(1e-12);
5426            let u2 = next_unit();
5427            let z = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
5428            x.push(xi);
5429            w.push(wi);
5430            y.push(0.4 + (1.3 * xi).sin() + (0.45 / wi.sqrt()) * z);
5431        }
5432        (x, y, w)
5433    }
5434
5435    /// The certified derivative ladder reaches exact endpoint jets throughout
5436    /// the search domain that exposed #2614.
5437    ///
5438    /// This fixture used to refuse at smoothing order 3 throughout
5439    /// `-20 <= rho <= -10` when its covariance-derivative zonotope overflowed.
5440    /// Merely accepting a global analytic fallback there would be sound but
5441    /// would reintroduce the loose cells that exhausted the subdivision budget.
5442    /// The repaired centred Riccati/shared-`q` representation must instead
5443    /// preserve enough dependence for BOTH curvature and third derivative to
5444    /// come from their endpoint jets at every measured point.
5445    #[test]
5446    fn certified_ladder_reaches_endpoint_jets_across_the_search_domain() {
5447        let (x, y, w) = dgp_2300();
5448        let visited = [
5449            -24.0_f64,
5450            -20.0,
5451            -18.0,
5452            -16.6135,
5453            // The log-lambda the #2300 certified search refuses at, order 2.
5454            -13.841116916640328,
5455            -10.0,
5456            -6.0,
5457            0.0,
5458            6.0,
5459        ];
5460        for order in 1..=MAX_ORDER {
5461            let (nodes, within, n_obs, _response_origin) =
5462                pool_nodes(&x, &y, &w, order).expect("pool");
5463            for &log_lambda in &visited {
5464                let certificate = certified_concentrated_criterion_jet(
5465                    &nodes, within, n_obs, log_lambda, order,
5466                )
5467                .unwrap_or_else(|error| {
5468                    panic!(
5469                        "order {order}, rho {log_lambda}: repaired certified ladder refused: \
5470                         {error:?}"
5471                    )
5472                });
5473                assert_eq!(
5474                    certificate.curvature_source,
5475                    BoundSource::EndpointJet,
5476                    "order {order}, rho {log_lambda}: curvature lost its exact endpoint anchor"
5477                );
5478                assert_eq!(
5479                    certificate.third_source,
5480                    BoundSource::EndpointJet,
5481                    "order {order}, rho {log_lambda}: third derivative lost its exact endpoint anchor"
5482                );
5483            }
5484        }
5485    }
5486
5487    /// The certified criterion jet stays inside the range the Gaussian model
5488    /// gives it, on the fixture where it did not (#2614).
5489    ///
5490    /// `V′ = −½(Σ log F̃)′ − ½·ν·(Σ v²/F̃)′/rss`, and the exact accumulator ranges
5491    /// (see [`intersect_first_order_accumulator_exact_ranges`]) are
5492    /// `−r ≤ (Σ log F̃)′ ≤ 0` and `0 ≤ (Σ v²/F̃)′ ≤ Σ v²/F̃ ≤ rss`, so
5493    /// `−ν/2 ≤ V′ ≤ r/2` — a width of at most `(r + ν)/2`. Measured before those
5494    /// ranges were applied: `±1.95e91` at order 2, `ρ = −18`, i.e. 89 orders of
5495    /// magnitude outside a range the model fixes at `178`. That is what made the
5496    /// search report `Unresolved` rather than bracket a stationary point.
5497    ///
5498    /// Containment is asserted FIRST and against an independent recurrence: the
5499    /// ball jet must enclose the scalar `f64` jet, which shares no arithmetic
5500    /// with it. A narrower enclosure that stops containing the value it encloses
5501    /// is a worse defect than the width this test exists to bound.
5502    #[test]
5503    fn the_certified_jet_contains_the_scalar_jet_and_stays_in_its_closed_form_range() {
5504        let (x, y, w) = dgp_2300();
5505        for order in 1..=MAX_ORDER {
5506            let (nodes, within, n_obs, _response_origin) =
5507                pool_nodes(&x, &y, &w, order).expect("pool");
5508            let proper_modes = (nodes.len() - order) as f64;
5509            let residual_dof = (n_obs - order) as f64;
5510            for &rho in &[
5511                -18.0_f64,
5512                -16.6135,
5513                -13.841116916640328,
5514                -10.0,
5515                -6.0,
5516                0.0,
5517                6.0,
5518            ] {
5519                let Ok(certificate) =
5520                    certified_concentrated_criterion_jet(&nodes, within, n_obs, rho, order)
5521                else {
5522                    continue;
5523                };
5524                let scalar = concentrated_criterion_jet(&nodes, within, n_obs, rho, order)
5525                    .expect("independent scalar recurrence");
5526                assert!(
5527                    certificate.value.interval().contains(scalar.0),
5528                    "order={order} rho={rho}: scalar value {} escaped {:?}",
5529                    scalar.0,
5530                    certificate.value
5531                );
5532                assert!(
5533                    certificate.derivative.interval().contains(scalar.1),
5534                    "order={order} rho={rho}: scalar derivative {} escaped {:?}",
5535                    scalar.1,
5536                    certificate.derivative
5537                );
5538                let width = certificate.derivative.hi - certificate.derivative.lo;
5539                assert!(
5540                    width < proper_modes + residual_dof,
5541                    "order={order} rho={rho}: the certified derivative ball is {width:e} \
5542                     wide, outside the closed-form range the accumulators are bounded to \
5543                     ({:e}); the search cannot sign an interval that wide",
5544                    0.5 * (proper_modes + residual_dof)
5545                );
5546            }
5547        }
5548    }
5549
5550    /// The amplifier behind every width in this file, measured on the map
5551    /// itself rather than inferred from what it produces — and the one bound on
5552    /// it that needs no interval product.
5553    ///
5554    /// The filtered covariance sits at its Riccati fixed point on this fixture
5555    /// (`P₁₁ = 1225.652` and `P₂₂ = 950572.8` at nodes 40, 60 and 80 alike), so
5556    /// the recursion's true width map is the closed-loop congruence
5557    /// `Ψ = F A`, `A = I − K e₀ᵀ`, and it CONTRACTS. The componentwise interval
5558    /// evaluation of that same recursion propagates widths through `|Ψ|`
5559    /// instead, and that EXPLODES. Both products are formed here, over the whole
5560    /// proper range, from the traced per-node gains.
5561    ///
5562    /// This is not dependency loss that a corner or exact-range evaluation can
5563    /// reach. `Ψ` has `−K_i` below the diagonal of its first column and `+δ`
5564    /// above it, so the sign of the `1↔2` cycle is NEGATIVE — and a cycle's sign
5565    /// is invariant under diagonal similarity, so no rescaling of the state
5566    /// makes `|Ψ| = Ψ`. The cancellation is between coordinates of one step, and
5567    /// no componentwise interval arithmetic in any diagonal basis can see it.
5568    /// Every enclosure this file builds by recursion over nodes — the
5569    /// covariance, its jets, the mean, and equally the BACKWARD smoother
5570    /// recursions a closed-form `V′` would need, which propagate through `Ψᵀ`
5571    /// and inherit the same factor — is bounded below by it.
5572    ///
5573    /// THE THIRD COLUMN is the reason this test is worth its cost. The Riccati
5574    /// recursion is `P⁻_{t+1} = Ψ_t P⁻_t Ψ_tᵀ + G_t` with
5575    /// `G_t = F R K Kᵀ Fᵀ + Q ⪰ 0`, which is an IDENTITY at every node and not
5576    /// only at a fixed point. So `Ψ_t P⁻_t Ψ_tᵀ ⪯ P⁻_{t+1}`, and with
5577    /// `S_t = (P⁻_{t+1})^{-1/2} Ψ_t (P⁻_t)^{1/2}` that says `‖S_t‖₂ ≤ 1` —
5578    /// the closed loop is a contraction in the metric its own covariance
5579    /// defines. The product telescopes,
5580    /// `Π Ψ = (P⁻_b)^{1/2}(S_b ⋯ S_a)(P⁻_a)^{-1/2}`, so `Π‖S_t‖₂` bounds the
5581    /// signed product with NO interval product formed anywhere: a per-node
5582    /// scalar, each one certifiable on its own. That is what a windowed repair
5583    /// would need in place of the exploding column, and this test measures
5584    /// whether the sub-multiplicative bound is strong enough to be that
5585    /// replacement — `Π‖S_t‖` against the `‖Π Ψ‖` it must stand in for.
5586    ///
5587    /// As measured (order 3, ρ = −16.6135, 175 closed-loop steps):
5588    ///
5589    /// ```text
5590    ///   steps    ‖Π Ψ‖        ‖Π |Ψ|‖       Π‖S_t‖
5591    ///      20    1.73e−1      9.01e+6       8.08e−1
5592    ///      40    1.13e−3      2.81e+11      6.48e−1
5593    ///      80    1.02e−9      2.74e+20      4.17e−1
5594    ///     120    1.83e−17     1.78e+33      1.52e−1
5595    ///     175    9.54e−30     4.40e+51      3.19e−2
5596    ///   per step 0.6826       1.9729        0.98050   (worst step 0.993226)
5597    /// ```
5598    ///
5599    /// Read all three. The filter contracts by 30 orders of magnitude over its
5600    /// own data while the componentwise interval evaluation of the same
5601    /// recursion inflates by 51 — 81 orders between what the filter does and
5602    /// what that arithmetic can prove about it, and `4.4e51 × 2.2e−16` is why
5603    /// quantities whose values are bit-stable carry enclosures of no
5604    /// information at all.
5605    ///
5606    /// And the Lyapunov column HOLDS but does not RESCUE. Every `‖S_t‖₂` is at
5607    /// most one exactly as the Riccati identity says (largest 0.993226), so the
5608    /// bound is real and needs no interval product — but `Π‖S_t‖` decays at
5609    /// 0.98050 per step against the true 0.6826, so over the same 175 steps it
5610    /// certifies `3.19e−2` where the truth is `9.54e−30`. Twenty-seven orders
5611    /// too weak, because `‖S_t‖₂` is the WORST direction — the barely-observed
5612    /// curvature coordinate, contracting at 0.9932 — while the product contracts
5613    /// fast only because its dominant directions ROTATE, which submultiplicativity
5614    /// cannot see.
5615    ///
5616    /// What that leaves is not "the Lyapunov structure is useless" but a
5617    /// sharper statement of the repair: the `S_t` are contractions in the metric
5618    /// the covariance defines, so an interval product OF THE `S_t` grows its
5619    /// widths additively rather than geometrically. Carrying the enclosure in
5620    /// `P^{1/2}` coordinates — not bounding the product by a product of bounds —
5621    /// is the move, and this test certifies per node the one property that makes
5622    /// that preconditioner the right one.
5623    #[test]
5624    fn the_closed_loop_map_contracts_while_its_absolute_value_explodes() {
5625        let (x, y, w) = dgp_2300();
5626        let order = 3;
5627        // This is the middle of the formerly refusing order-3 tail.
5628        let log_lambda = -16.6135_f64;
5629        let (nodes, within, n_obs, _response_origin) =
5630            pool_nodes(&x, &y, &w, order).expect("pool");
5631        let q_value =
5632            gam_problem::checked_exp_log_strength(-log_lambda).expect("inverse log strength");
5633        let q = Ball::certified(
5634            q_value,
5635            gam_math::score_opt::certified_exp(-log_lambda).expect("certified exponential"),
5636        );
5637        let mut trace: Vec<BallTraceRecord> = Vec::new();
5638        certified_concentrated_criterion_jet(&nodes, within, n_obs, log_lambda, order)
5639            .expect("the certified jet must exist at the rho this map is measured at");
5640        run_filter_ball_traced(&nodes, q, order, Some(&mut trace)).expect("traced pass");
5641        let mut gains: HashMap<usize, [f64; MAX_ORDER]> = HashMap::new();
5642        let mut predicted: HashMap<usize, Mat2> = HashMap::new();
5643        for (node, name, ball) in &trace {
5644            if let Some(coordinate) = GAIN_NAMES.iter().position(|candidate| candidate == name) {
5645                gains.entry(*node).or_insert([0.0; MAX_ORDER])[coordinate] = ball.value;
5646            }
5647            for (i, row) in P_NEXT_ENTRY_NAMES.iter().enumerate().take(order) {
5648                for (j, entry) in row.iter().enumerate().take(order) {
5649                    if entry == name {
5650                        predicted
5651                            .entry(*node)
5652                            .or_insert([[0.0; MAX_ORDER]; MAX_ORDER])[i][j] = ball.value;
5653                    }
5654                }
5655            }
5656        }
5657        let max_norm = |matrix: &Mat2| -> f64 {
5658            let mut norm = 0.0_f64;
5659            for row in matrix.iter().take(order) {
5660                for entry in row.iter().take(order) {
5661                    norm = norm.max(entry.abs());
5662                }
5663            }
5664            norm
5665        };
5666        // Largest eigenvalue of a matrix similar to a symmetric PSD one, by
5667        // power iteration. `None` when the iterate collapses, which is a
5668        // statement about this fixture and not about the matrix.
5669        let spectral_radius = |matrix: &Mat2| -> Option<f64> {
5670            let mut vector = [1.0_f64; MAX_ORDER];
5671            let mut radius = 0.0_f64;
5672            let mut iterations = 0usize;
5673            while iterations < 500 {
5674                let mut next = [0.0_f64; MAX_ORDER];
5675                for i in 0..order {
5676                    for k in 0..order {
5677                        next[i] += matrix[i][k] * vector[k];
5678                    }
5679                }
5680                let scale = next
5681                    .iter()
5682                    .take(order)
5683                    .fold(0.0_f64, |widest, entry| widest.max(entry.abs()));
5684                if !(scale > 0.0 && scale.is_finite()) {
5685                    return None;
5686                }
5687                for i in 0..order {
5688                    vector[i] = next[i] / scale;
5689                }
5690                radius = scale;
5691                iterations += 1;
5692            }
5693            Some(radius)
5694        };
5695        let mut signed: Mat2 = [[0.0; MAX_ORDER]; MAX_ORDER];
5696        let mut absolute: Mat2 = [[0.0; MAX_ORDER]; MAX_ORDER];
5697        for i in 0..order {
5698            signed[i][i] = 1.0;
5699            absolute[i][i] = 1.0;
5700        }
5701        let mut log_lyapunov = 0.0_f64;
5702        let mut worst_step = 0.0_f64;
5703        let mut steps = 0usize;
5704        for t in (order + 1)..(nodes.len() - 1) {
5705            let (Some(gain), Some(before), Some(after)) =
5706                (gains.get(&t), predicted.get(&(t - 1)), predicted.get(&t))
5707            else {
5708                continue;
5709            };
5710            let delta = nodes[t + 1].x - nodes[t].x;
5711            let ball_f = ball_transition(Ball::exact(delta), order);
5712            let mut transition: Mat2 = [[0.0; MAX_ORDER]; MAX_ORDER];
5713            let mut update: Mat2 = [[0.0; MAX_ORDER]; MAX_ORDER];
5714            for i in 0..order {
5715                update[i][i] = 1.0;
5716                for j in 0..order {
5717                    transition[i][j] = ball_f[i][j].value;
5718                }
5719            }
5720            for i in 0..order {
5721                update[i][0] -= gain[i];
5722            }
5723            // Update THEN predict, which is the order the filter runs in.
5724            let closed = mat_mul(&transition, &update, order);
5725            let mut next_signed: Mat2 = [[0.0; MAX_ORDER]; MAX_ORDER];
5726            let mut next_absolute: Mat2 = [[0.0; MAX_ORDER]; MAX_ORDER];
5727            for i in 0..order {
5728                for j in 0..order {
5729                    for k in 0..order {
5730                        next_signed[i][j] += closed[i][k] * signed[k][j];
5731                        next_absolute[i][j] += closed[i][k].abs() * absolute[k][j];
5732                    }
5733                }
5734            }
5735            signed = next_signed;
5736            absolute = next_absolute;
5737            // `‖S_t‖₂² = λ_max((P⁻_{t+1})⁻¹ Ψ P⁻_t Ψᵀ)`.
5738            let Ok(inverse_after) = mat_inv(after, order, "lyapunov weight") else {
5739                continue;
5740            };
5741            let congruence = mat_mul(
5742                &mat_mul(&closed, before, order),
5743                &mat_t(&closed, order),
5744                order,
5745            );
5746            let Some(squared) = spectral_radius(&mat_mul(&inverse_after, &congruence, order))
5747            else {
5748                continue;
5749            };
5750            let factor = squared.max(0.0).sqrt();
5751            worst_step = worst_step.max(factor);
5752            log_lyapunov += factor.ln();
5753            steps += 1;
5754            if steps % 20 == 0 {
5755                eprintln!(
5756                    "after {steps} steps: ||prod Psi|| = {:.6e}, ||prod |Psi||| = {:.6e}, \
5757                     prod ||S_t|| = {:.6e}",
5758                    max_norm(&signed),
5759                    max_norm(&absolute),
5760                    log_lyapunov.exp()
5761                );
5762            }
5763        }
5764        let contracted = max_norm(&signed);
5765        let inflated = max_norm(&absolute);
5766        let lyapunov = log_lyapunov.exp();
5767        eprintln!(
5768            "closed loop over {steps} steps: signed {contracted:.6e}, absolute {inflated:.6e}, \
5769             lyapunov {lyapunov:.6e}; per step signed {:.4}, absolute {:.4}, lyapunov {:.6}, \
5770             worst single step {worst_step:.6}",
5771            contracted.powf(1.0 / steps as f64),
5772            inflated.powf(1.0 / steps as f64),
5773            lyapunov.powf(1.0 / steps as f64)
5774        );
5775        assert!(
5776            contracted < 1.0,
5777            "the closed-loop product does not contract ({contracted:e} over {steps} steps); \
5778             the filter's own stability is the premise of every width argument here"
5779        );
5780        assert!(
5781            inflated > 1.0e10,
5782            "the absolute closed-loop product no longer explodes ({inflated:e} over {steps} \
5783             steps). If that is a repair, the recursion-level enclosures can be tightened \
5784             directly and this test is where the new factor is recorded"
5785        );
5786        assert!(
5787            worst_step <= 1.0 + 1.0e-9,
5788            "the Riccati identity `Psi P Psi^T + G = P_next` with `G >= 0` makes every \
5789             `||S_t||_2` at most one; the largest measured is {worst_step}, so either the \
5790             traced covariance is not the one the recursion produced or the identity is \
5791             being read wrong"
5792        );
5793        assert!(
5794            lyapunov >= contracted,
5795            "the Lyapunov product {lyapunov:e} must bound the signed product {contracted:e} \
5796             it stands in for"
5797        );
5798    }
5799
5800    /// The centred Riccati representation keeps the filtered-mean enclosure
5801    /// below the search resolution throughout the former order-3 failure band.
5802    ///
5803    /// Before #2614, `mean_a0` stayed O(1) while its enclosure width grew from
5804    /// `3.6e-7` at node 8 to `2.3e254` at node 120. That was pure dependency
5805    /// loss: the scalar filter remained stable. The repaired path must preserve
5806    /// both facts directly — bounded values and a finite enclosure narrower
5807    /// than the resolution the certified search asks it to support — and the
5808    /// criterion consuming that pass must certify rather than refuse.
5809    #[test]
5810    fn centred_riccati_mean_enclosure_stays_below_search_resolution() {
5811        let (x, y, w) = dgp_2300();
5812        let order = 3;
5813        let log_lambda = -16.6135_f64;
5814        let (nodes, within, n_obs, _response_origin) =
5815            pool_nodes(&x, &y, &w, order).expect("pool");
5816        let q_value =
5817            gam_problem::checked_exp_log_strength(-log_lambda).expect("inverse log strength");
5818        let q = Ball::certified(
5819            q_value,
5820            gam_math::score_opt::certified_exp(-log_lambda).expect("certified exponential"),
5821        );
5822        let mut trace: Vec<BallTraceRecord> = Vec::new();
5823        run_filter_ball_traced(&nodes, q, order, Some(&mut trace))
5824            .expect("the repaired filter must certify the former failure point");
5825        let mean: Vec<(usize, Ball)> = trace
5826            .iter()
5827            .filter(|(_, name, _)| *name == "mean_a0")
5828            .map(|(node, _, ball)| (*node, *ball))
5829            .collect();
5830        assert_eq!(
5831            mean.len(),
5832            nodes.len() - order,
5833            "every proper filter node must expose a mean certificate"
5834        );
5835        let resolution = f64::EPSILON.sqrt();
5836        let widest_value = mean
5837            .iter()
5838            .fold(0.0_f64, |widest, (_, ball)| widest.max(ball.value.abs()));
5839        assert!(
5840            widest_value < 1.0e2,
5841            "the filtered mean's VALUE left O(1) at order {order}, rho {log_lambda}: \
5842             {widest_value:e}"
5843        );
5844        for (node, ball) in mean {
5845            assert!(
5846                ball.is_finite(),
5847                "mean enclosure is non-finite at node {node}"
5848            );
5849            let width = ball.hi - ball.lo;
5850            let scaled_resolution = resolution * (1.0 + ball.value.abs());
5851            assert!(
5852                width <= scaled_resolution,
5853                "mean enclosure at node {node} is {width:e} wide, exceeding the \
5854                 scale-aware search resolution {scaled_resolution:e}"
5855            );
5856        }
5857        certified_concentrated_criterion_jet(&nodes, within, n_obs, log_lambda, order)
5858            .expect("the criterion consuming the repaired pass must certify");
5859    }
5860
5861    /// Value-only diagnostic surface retained for the derivative oracle tests.
5862    fn concentrated_criterion(
5863        nodes: &[PooledNode],
5864        ssr_within: f64,
5865        n_obs: usize,
5866        log_lambda: f64,
5867        order: usize,
5868    ) -> Result<f64, String> {
5869        Ok(concentrated_criterion_jet(nodes, ssr_within, n_obs, log_lambda, order)?.0)
5870    }
5871    use super::*;
5872
5873    #[test]
5874    fn concentrated_score_jet_matches_test_only_differences() {
5875        let x = [0.0, 0.07, 0.19, 0.41, 0.41, 0.68, 1.0, 1.37];
5876        let y = [0.2, -0.4, 0.8, 0.1, 0.35, -0.2, 0.7, 0.15];
5877        let w = [1.0, 2.0, 0.7, 1.4, 0.9, 3.0, 1.2, 0.8];
5878        for order in 1..=MAX_ORDER {
5879            let (nodes, within, n_obs, _response_origin) =
5880                pool_nodes(&x, &y, &w, order).expect("pooled data");
5881            for &rho in &[-4.0, -0.3, 2.5] {
5882                let (value, d1, d2, d3) =
5883                    concentrated_criterion_jet(&nodes, within, n_obs, rho, order)
5884                        .expect("analytic score jet");
5885                // Finite differences are deliberately confined to this oracle
5886                // test; production selection uses the analytic sensitivities.
5887                let h = 2.0e-4;
5888                let fm = concentrated_criterion(&nodes, within, n_obs, rho - h, order)
5889                    .expect("left score");
5890                let fp = concentrated_criterion(&nodes, within, n_obs, rho + h, order)
5891                    .expect("right score");
5892                let fm2 = concentrated_criterion(&nodes, within, n_obs, rho - 2.0 * h, order)
5893                    .expect("far left score");
5894                let fp2 = concentrated_criterion(&nodes, within, n_obs, rho + 2.0 * h, order)
5895                    .expect("far right score");
5896                let d1_fd = (fp - fm) / (2.0 * h);
5897                let d2_fd = (fp - 2.0 * value + fm) / (h * h);
5898                let d3_fd = (fp2 - 2.0 * fp + 2.0 * fm - fm2) / (2.0 * h * h * h);
5899                // Independent finite-difference certificate: endpoint VALUE
5900                // balls enclose the central quotient, and the global third-
5901                // derivative theorem bounds its O(h²) truncation remainder.
5902                let left_ball =
5903                    certified_concentrated_criterion_jet(&nodes, within, n_obs, rho - h, order)
5904                        .expect("left value ball");
5905                let right_ball =
5906                    certified_concentrated_criterion_jet(&nodes, within, n_obs, rho + h, order)
5907                        .expect("right value ball");
5908                let finite_difference = right_ball
5909                    .value
5910                    .sub(left_ball.value)
5911                    .div_positive(Ball::exact(2.0 * h));
5912                let proper_modes = (nodes.len() - order) as f64;
5913                let residual_dof = (n_obs - order) as f64;
5914                let third_bound = 0.5 * (0.25 * proper_modes + 6.0 * residual_dof);
5915                let truncation = third_bound * h * h / 6.0;
5916                let certified_center =
5917                    certified_concentrated_criterion_jet(&nodes, within, n_obs, rho, order)
5918                        .expect("center derivative ball");
5919                assert!(
5920                    certified_center.derivative.hi >= finite_difference.lo - truncation
5921                        && certified_center.derivative.lo <= finite_difference.hi + truncation,
5922                    "order={order} rho={rho}: analytic derivative ball {:?} is disjoint \
5923                     from independently value-differenced {:?} ± {truncation:e}",
5924                    certified_center.derivative,
5925                    finite_difference
5926                );
5927                let d1_scale = 1.0 + d1.abs().max(d1_fd.abs());
5928                let d2_scale = 1.0 + d2.abs().max(d2_fd.abs());
5929                let d3_scale = 1.0 + d3.abs().max(d3_fd.abs());
5930                assert!(
5931                    (d1 - d1_fd).abs() <= 2.0e-6 * d1_scale,
5932                    "order={order} rho={rho}: analytic d1={d1}, FD={d1_fd}"
5933                );
5934                assert!(
5935                    (d2 - d2_fd).abs() <= 2.0e-4 * d2_scale,
5936                    "order={order} rho={rho}: analytic d2={d2}, FD={d2_fd}"
5937                );
5938                assert!(
5939                    (d3 - d3_fd).abs() <= 5.0e-3 * d3_scale,
5940                    "order={order} rho={rho}: analytic d3={d3}, FD={d3_fd}"
5941                );
5942            }
5943        }
5944    }
5945
5946    #[test]
5947    fn directed_score_balls_contain_independent_scalar_jets_across_scales() {
5948        let base_x = [0.0, 0.03, 0.11, 0.27, 0.52, 0.81, 1.17, 1.6];
5949        let y = [2.0e3, -4.0e2, 8.0e2, 1.0e2, 3.5e2, -2.0e2, 7.0e2, 1.5e2];
5950        let w = [1.0e-4, 2.0e4, 0.7, 1.4e3, 9.0e-3, 3.0e2, 1.2, 8.0e-2];
5951        for order in 1..=MAX_ORDER {
5952            for scale in [1.0e-1_f64, 1.0, 1.0e2] {
5953                let x: Vec<f64> = base_x.iter().map(|value| scale * value).collect();
5954                let (nodes, within, n_obs, _response_origin) =
5955                    pool_nodes(&x, &y, &w, order).expect("adversarial pooled data");
5956                let rho = (2 * order - 1) as f64 * scale.ln() + 0.35;
5957                let certified =
5958                    certified_concentrated_criterion_jet(&nodes, within, n_obs, rho, order)
5959                        .expect("directed score recurrence");
5960                let scalar = concentrated_criterion_jet(&nodes, within, n_obs, rho, order)
5961                    .expect("independent scalar recurrence");
5962                for (name, ball, reference) in [
5963                    ("value", certified.value, scalar.0),
5964                    ("derivative", certified.derivative, scalar.1),
5965                    ("curvature", certified.curvature, scalar.2),
5966                    ("third", certified.third, scalar.3),
5967                ] {
5968                    assert!(
5969                        ball.interval().contains(reference),
5970                        "order={order} scale={scale:e}: scalar {name} {reference} escaped {ball:?}"
5971                    );
5972                }
5973
5974                let point_sample = ScoreSample {
5975                    x: rho,
5976                    value: certified.jet.value,
5977                    derivative: certified.jet.derivative,
5978                    curvature: certified.jet.curvature,
5979                    third: certified.jet.third,
5980                };
5981                let point_enclosure = concentrated_criterion_enclosure(
5982                    nodes.len(),
5983                    n_obs,
5984                    point_sample,
5985                    point_sample,
5986                    certified,
5987                    certified,
5988                    order,
5989                )
5990                .expect("degenerate point enclosure");
5991                assert_eq!(
5992                    point_enclosure.derivative,
5993                    certified.derivative.interval(),
5994                    "a zero-width cell must preserve the certified point derivative exactly"
5995                );
5996                assert_eq!(
5997                    point_enclosure.curvature,
5998                    certified.curvature.interval(),
5999                    "a zero-width cell must preserve the certified point curvature exactly"
6000                );
6001                assert_eq!(
6002                    point_enclosure.score.value,
6003                    certified.value.interval(),
6004                    "a zero-width cell must preserve the certified point score exactly"
6005                );
6006
6007                let rho_right = rho + 0.125;
6008                let right =
6009                    certified_concentrated_criterion_jet(&nodes, within, n_obs, rho_right, order)
6010                        .expect("right endpoint ball");
6011                let enclosure = concentrated_criterion_enclosure(
6012                    nodes.len(),
6013                    n_obs,
6014                    ScoreSample {
6015                        x: rho,
6016                        value: certified.jet.value,
6017                        derivative: certified.jet.derivative,
6018                        curvature: certified.jet.curvature,
6019                        third: certified.jet.third,
6020                    },
6021                    ScoreSample {
6022                        x: rho_right,
6023                        value: right.jet.value,
6024                        derivative: right.jet.derivative,
6025                        curvature: right.jet.curvature,
6026                        third: right.jet.third,
6027                    },
6028                    certified,
6029                    right,
6030                    order,
6031                )
6032                .expect("endpoint-anchored enclosure");
6033                for certificate in [certified, right] {
6034                    assert!(
6035                        enclosure.derivative.lo <= certificate.derivative.lo
6036                            && enclosure.derivative.hi >= certificate.derivative.hi,
6037                        "exact endpoint derivative escaped the cell enclosure"
6038                    );
6039                    assert!(
6040                        enclosure.curvature.lo <= certificate.curvature.lo
6041                            && enclosure.curvature.hi >= certificate.curvature.hi,
6042                        "exact endpoint curvature escaped the cell enclosure"
6043                    );
6044                    assert!(
6045                        enclosure.score.value.lo <= certificate.value.lo
6046                            && enclosure.score.value.hi >= certificate.value.hi,
6047                        "exact endpoint score escaped the cell enclosure"
6048                    );
6049                }
6050            }
6051        }
6052    }
6053
6054    /// Regression oracle for both #2614 saturated order-3 tail cells. The
6055    /// production enclosure is a theorem, not a sampling scheme; these dense
6056    /// scalar evaluations independently guard its implementation, while the
6057    /// comparison with the old full-width L4 theorem proves that
6058    /// nearest-endpoint endpoint-third interpolation actually removes (rather
6059    /// than merely moves) the false Taylor uncertainty.
6060    #[test]
6061    fn nearest_endpoint_taylor_hull_contains_dense_cell_and_tightens_every_channel() {
6062        let n = 60usize;
6063        let mut x: Vec<f64> = (0..n).map(|i| i as f64 / (n as f64 - 1.0)).collect();
6064        x[7] = x[6];
6065        let y: Vec<f64> = x
6066            .iter()
6067            .enumerate()
6068            .map(|(i, &xi)| {
6069                (6.0 * xi).sin() + 0.3 * (17.0 * xi).cos() + 0.05 * ((i * 37 % 11) as f64 - 5.0)
6070            })
6071            .collect();
6072        let w: Vec<f64> = (0..n).map(|i| 1.0 + 0.5 * (i % 3) as f64).collect();
6073        let order = 3usize;
6074        let (nodes, within, n_obs, _response_origin) =
6075            pool_nodes(&x, &y, &w, order).expect("pooled data");
6076        let lo = 13.759_277_343_75;
6077        let hi = 13.760_375_976_562_5;
6078        let left = certified_concentrated_criterion_jet(&nodes, within, n_obs, lo, order)
6079            .expect("left endpoint certificate");
6080        let right = certified_concentrated_criterion_jet(&nodes, within, n_obs, hi, order)
6081            .expect("right endpoint certificate");
6082        let sample = |rho: f64, certificate: CertifiedCriterionJet| ScoreSample {
6083            x: rho,
6084            value: certificate.jet.value,
6085            derivative: certificate.jet.derivative,
6086            curvature: certificate.jet.curvature,
6087            third: certificate.jet.third,
6088        };
6089        let nearest = concentrated_criterion_enclosure(
6090            nodes.len(),
6091            n_obs,
6092            sample(lo, left),
6093            sample(hi, right),
6094            left,
6095            right,
6096            order,
6097        )
6098        .expect("nearest-endpoint enclosure");
6099
6100        for step in 0..=256 {
6101            let rho = lo + (hi - lo) * step as f64 / 256.0;
6102            let (value, derivative, curvature, _) =
6103                concentrated_criterion_jet(&nodes, within, n_obs, rho, order)
6104                    .expect("independent scalar jet");
6105            assert!(
6106                nearest.score.value.contains(value),
6107                "dense score sample at rho={rho:.17} escaped {:?}",
6108                nearest.score.value
6109            );
6110            assert!(
6111                nearest.derivative.contains(derivative),
6112                "dense derivative sample at rho={rho:.17} escaped {:?}",
6113                nearest.derivative
6114            );
6115            assert!(
6116                nearest.curvature.contains(curvature),
6117                "dense curvature sample at rho={rho:.17} escaped {:?}",
6118                nearest.curvature
6119            );
6120        }
6121
6122        // Re-evaluate the identical certified Taylor theorem with each endpoint
6123        // spanning the FULL cell. This is the pre-fix geometry, expressed
6124        // directionally rather than weakened further into absolute-value
6125        // radii, so beating it is the stronger comparison.
6126        let width = Ball::exact(hi).sub(Ball::exact(lo));
6127        let width2 = width.square();
6128        let width3 = width2.mul(width);
6129        let width4 = width2.square();
6130        let fourth_abs_bound = Ball::exact((nodes.len() - order) as f64)
6131            .scale(0.25)
6132            .add(Ball::exact((n_obs - order) as f64).scale(26.0))
6133            .scale(0.5);
6134        let value_remainder = fourth_abs_bound
6135            .mul(width4)
6136            .div_positive(Ball::exact(24.0))
6137            .hi;
6138        let derivative_remainder = fourth_abs_bound
6139            .mul(width3)
6140            .div_positive(Ball::exact(6.0))
6141            .hi;
6142        let curvature_remainder = fourth_abs_bound.mul(width2).scale(0.5).hi;
6143        let full_cell_from_endpoint =
6144            |certificate: CertifiedCriterionJet, displacement: ClosedInterval| {
6145                let d = Ball::certified(0.0, displacement);
6146                let d2 = d.square();
6147                let d3 = d2.mul(d);
6148                let value = certificate
6149                    .value
6150                    .add(certificate.derivative.mul(d))
6151                    .add(certificate.curvature.mul(d2).scale(0.5))
6152                    .add(certificate.third.mul(d3).div_positive(Ball::exact(6.0)))
6153                    .interval()
6154                    .add(ClosedInterval::new(-value_remainder, value_remainder));
6155                let derivative = certificate
6156                    .derivative
6157                    .add(certificate.curvature.mul(d))
6158                    .add(certificate.third.mul(d2).scale(0.5))
6159                    .interval()
6160                    .add(ClosedInterval::new(
6161                        -derivative_remainder,
6162                        derivative_remainder,
6163                    ));
6164                let curvature = certificate
6165                    .curvature
6166                    .add(certificate.third.mul(d))
6167                    .interval()
6168                    .add(ClosedInterval::new(
6169                        -curvature_remainder,
6170                        curvature_remainder,
6171                    ));
6172                (value, derivative, curvature)
6173            };
6174        let old_left = full_cell_from_endpoint(left, ClosedInterval::new(0.0, width.hi));
6175        let old_right = full_cell_from_endpoint(right, ClosedInterval::new(-width.hi, 0.0));
6176        let old_value = ClosedInterval::new(
6177            old_left.0.lo.min(old_right.0.lo),
6178            old_left.0.hi.max(old_right.0.hi),
6179        );
6180        let old_derivative = ClosedInterval::new(
6181            old_left.1.lo.min(old_right.1.lo),
6182            old_left.1.hi.max(old_right.1.hi),
6183        );
6184        let old_curvature = ClosedInterval::new(
6185            old_left.2.lo.min(old_right.2.lo),
6186            old_left.2.hi.max(old_right.2.hi),
6187        );
6188        for (name, tightened, full_width) in [
6189            ("score", nearest.score.value, old_value),
6190            ("derivative", nearest.derivative, old_derivative),
6191            ("curvature", nearest.curvature, old_curvature),
6192        ] {
6193            assert!(
6194                tightened.hi - tightened.lo < full_width.hi - full_width.lo,
6195                "nearest-endpoint {name} enclosure {tightened:?} was not strictly \
6196                 narrower than full-width theorem {full_width:?}"
6197            );
6198        }
6199        assert!(
6200            nearest.derivative.hi < 0.0,
6201            "the corrected theorem must certify the live #2614 cell's negative slope: {:?}",
6202            nearest.derivative
6203        );
6204
6205        // The half-cell L4 theorem above exposed the next saturated cell at
6206        // rho≈16.127. It has the same dyadic width, but its endpoint slope is
6207        // only 2.76e-9, so the old global L4 remainder is eight times larger
6208        // than the signal even with correct nearest-endpoint geometry. The
6209        // endpoint-third/L5 theorem must contain the whole cell AND recover its
6210        // sign; otherwise it merely moves the same budget exhaustion again.
6211        let shifted_lo = 16.126_831_054_687_5;
6212        let shifted_hi = 16.127_929_687_5;
6213        assert_eq!(
6214            shifted_hi - shifted_lo,
6215            hi - lo,
6216            "the old-theorem comparison below shares the measured dyadic width"
6217        );
6218        let shifted_left =
6219            certified_concentrated_criterion_jet(&nodes, within, n_obs, shifted_lo, order)
6220                .expect("shifted left endpoint certificate");
6221        let shifted_right =
6222            certified_concentrated_criterion_jet(&nodes, within, n_obs, shifted_hi, order)
6223                .expect("shifted right endpoint certificate");
6224        let shifted = concentrated_criterion_enclosure(
6225            nodes.len(),
6226            n_obs,
6227            sample(shifted_lo, shifted_left),
6228            sample(shifted_hi, shifted_right),
6229            shifted_left,
6230            shifted_right,
6231            order,
6232        )
6233        .expect("shifted endpoint-third enclosure");
6234        for step in 0..=256 {
6235            let rho = shifted_lo + (shifted_hi - shifted_lo) * step as f64 / 256.0;
6236            let (value, derivative, curvature, _) =
6237                concentrated_criterion_jet(&nodes, within, n_obs, rho, order)
6238                    .expect("shifted independent scalar jet");
6239            assert!(
6240                shifted.score.value.contains(value),
6241                "shifted dense score at rho={rho:.17} escaped {:?}",
6242                shifted.score.value
6243            );
6244            assert!(
6245                shifted.derivative.contains(derivative),
6246                "shifted dense derivative at rho={rho:.17} escaped {:?}",
6247                shifted.derivative
6248            );
6249            assert!(
6250                shifted.curvature.contains(curvature),
6251                "shifted dense curvature at rho={rho:.17} escaped {:?}",
6252                shifted.curvature
6253            );
6254        }
6255        let shifted_old_left =
6256            full_cell_from_endpoint(shifted_left, ClosedInterval::new(0.0, width.hi));
6257        let shifted_old_right =
6258            full_cell_from_endpoint(shifted_right, ClosedInterval::new(-width.hi, 0.0));
6259        for (name, tightened, old_left, old_right) in [
6260            (
6261                "score",
6262                shifted.score.value,
6263                shifted_old_left.0,
6264                shifted_old_right.0,
6265            ),
6266            (
6267                "derivative",
6268                shifted.derivative,
6269                shifted_old_left.1,
6270                shifted_old_right.1,
6271            ),
6272            (
6273                "curvature",
6274                shifted.curvature,
6275                shifted_old_left.2,
6276                shifted_old_right.2,
6277            ),
6278        ] {
6279            let full_width =
6280                ClosedInterval::new(old_left.lo.min(old_right.lo), old_left.hi.max(old_right.hi));
6281            assert!(
6282                tightened.hi - tightened.lo < full_width.hi - full_width.lo,
6283                "endpoint-third {name} enclosure {tightened:?} was not strictly \
6284                 narrower than the full-width L4 theorem {full_width:?}"
6285            );
6286        }
6287        assert!(
6288            shifted.derivative.hi < 0.0,
6289            "the endpoint-third theorem must certify the shifted #2614 cell's \
6290             negative slope: {:?}",
6291            shifted.derivative
6292        );
6293    }
6294
6295    #[test]
6296    fn spline_consumer_preserves_a_valid_resolution_flat_optimum_category() {
6297        let optimum = ScoreSample {
6298            x: -0.25,
6299            value: 3.0,
6300            derivative: 0.0,
6301            curvature: 0.0,
6302            third: 0.0,
6303        };
6304        let bracket = ClosedInterval::new(-0.5, 0.0);
6305        let max_score_gap = 0.125;
6306        let score_resolution = 0.25;
6307        let search = ScoreSearchResult {
6308            optimum,
6309            location: ScoreOptimumLocation::ResolutionFlat(0),
6310            lower_boundary: ScoreSample { x: -1.0, ..optimum },
6311            upper_boundary: ScoreSample { x: 1.0, ..optimum },
6312            stationary_points: Vec::new(),
6313            resolution_flat_regions: vec![gam_math::score_opt::ResolutionFlatRegion {
6314                sample: optimum,
6315                bracket,
6316                score: ClosedInterval::new(2.875, 3.0),
6317                max_score_gap,
6318                score_resolution,
6319            }],
6320            dominated_regions: Vec::new(),
6321            value_certificate: gam_math::score_opt::GlobalScoreCertificate {
6322                selected: ClosedInterval::point(3.0),
6323                maximum: ClosedInterval::new(3.0, 3.125),
6324                maximum_excess: max_score_gap,
6325                comparison_resolution: score_resolution,
6326            },
6327        };
6328        assert_eq!(
6329            spline_optimum_proof(&search).expect("valid resolution-flat proof"),
6330            SplineOptimumProof::ResolutionFlat {
6331                bracket,
6332                max_score_gap,
6333                score_resolution,
6334            },
6335            "the spline consumer must preserve the producer's successful typed category"
6336        );
6337
6338        let mut invalid = search;
6339        invalid.resolution_flat_regions[0].max_score_gap =
6340            invalid.resolution_flat_regions[0].score_resolution + f64::EPSILON;
6341        assert!(
6342            matches!(
6343                spline_optimum_proof(&invalid),
6344                Err(SplineScoreProofError::Search(_))
6345            ),
6346            "a malformed producer certificate must still fail instead of being accepted"
6347        );
6348    }
6349
6350    #[test]
6351    fn spline_consumer_retains_the_producers_stationary_curvature_proof() {
6352        let optimum = ScoreSample {
6353            x: -9.084_292_923_99,
6354            value: 3.0,
6355            derivative: 0.0,
6356            curvature: -1.0,
6357            third: 0.0,
6358        };
6359        let bracket = ClosedInterval::new(-9.084_292_924_175_005, -9.084_292_923_812_374);
6360        let producer_curvature = ClosedInterval::new(-6.4, -0.2);
6361        let point_score = ScoreValueEnclosure {
6362            value: ClosedInterval::new(2.999, 3.001),
6363            evaluation_error: 0.001,
6364        };
6365        let search = ScoreSearchResult {
6366            optimum,
6367            location: ScoreOptimumLocation::Stationary(0),
6368            lower_boundary: ScoreSample {
6369                x: -10.0,
6370                ..optimum
6371            },
6372            upper_boundary: ScoreSample { x: -8.0, ..optimum },
6373            stationary_points: vec![gam_math::score_opt::StationaryPoint {
6374                sample: optimum,
6375                bracket,
6376                score: point_score,
6377                curvature: producer_curvature,
6378            }],
6379            resolution_flat_regions: Vec::new(),
6380            dominated_regions: Vec::new(),
6381            value_certificate: gam_math::score_opt::GlobalScoreCertificate {
6382                selected: point_score.value,
6383                maximum: point_score.value,
6384                maximum_excess: 0.0,
6385                comparison_resolution: 0.002,
6386            },
6387        };
6388        let SplineOptimumProof::Kkt { bracket: got, kind } =
6389            spline_optimum_proof(&search).expect("valid stationary proof")
6390        else {
6391            panic!("stationary producer category was not preserved");
6392        };
6393        assert_eq!(got, bracket);
6394        assert_eq!(
6395            kind,
6396            SplineKktKind::Stationary {
6397                curvature: producer_curvature,
6398            }
6399        );
6400
6401        let local_enclosure = DerivativeEnclosure {
6402            score: point_score,
6403            derivative: ClosedInterval::new(-1.2e-9, 1.2e-9),
6404            // Mirrors the persistence failure: a fresh tiny-cell secant loses
6405            // curvature sign even though the parent proof remains strict.
6406            curvature: ClosedInterval::new(-6.39, 0.0064),
6407        };
6408        let (holds, consumed_curvature) = spline_kkt_holds(kind, local_enclosure);
6409        assert!(holds, "the final derivative still contains the unique root");
6410        assert_eq!(consumed_curvature, producer_curvature);
6411    }
6412
6413    #[test]
6414    fn derivative_secant_recovers_weighted_order3_root_curvature_sign() {
6415        let (x, y, w) = dgp_2300();
6416        let order = 3usize;
6417        let (nodes, within, n_obs, _response_origin) =
6418            pool_nodes(&x, &y, &w, order).expect("weighted pool");
6419        // Live cell at evaluation 1024 of the pre-secant #2300 traversal.
6420        let lo = -2.337_075_252_506_015;
6421        let hi = -2.337_040_920_230_624;
6422        let left = certified_concentrated_criterion_jet(&nodes, within, n_obs, lo, order)
6423            .expect("weighted left endpoint");
6424        let right = certified_concentrated_criterion_jet(&nodes, within, n_obs, hi, order)
6425            .expect("weighted right endpoint");
6426        assert!(
6427            left.curvature.interval().contains_zero() && right.curvature.interval().contains_zero(),
6428            "the oracle must exercise the loose direct covariance-d2 path"
6429        );
6430        let sample = |rho: f64, certificate: CertifiedCriterionJet| ScoreSample {
6431            x: rho,
6432            value: certificate.jet.value,
6433            derivative: certificate.jet.derivative,
6434            curvature: certificate.jet.curvature,
6435            third: certificate.jet.third,
6436        };
6437        let enclosure = concentrated_criterion_enclosure(
6438            nodes.len(),
6439            n_obs,
6440            sample(lo, left),
6441            sample(hi, right),
6442            left,
6443            right,
6444            order,
6445        )
6446        .expect("secant curvature enclosure");
6447        assert!(
6448            enclosure.curvature.hi < 0.0,
6449            "the derivative secant must recover strict concavity: {:?}",
6450            enclosure.curvature
6451        );
6452        assert!(
6453            enclosure.derivative.lo > 0.0,
6454            "integrating the secant curvature from both endpoints must preserve \
6455             the live cell's positive slope: {:?}",
6456            enclosure.derivative
6457        );
6458        for step in 0..=256 {
6459            let rho = lo + (hi - lo) * step as f64 / 256.0;
6460            let (_, derivative, curvature, _) =
6461                concentrated_criterion_jet(&nodes, within, n_obs, rho, order)
6462                    .expect("independent weighted scalar jet");
6463            assert!(
6464                enclosure.derivative.contains(derivative),
6465                "weighted scalar derivative {derivative} at rho={rho:.17} escaped {:?}",
6466                enclosure.derivative
6467            );
6468            assert!(
6469                enclosure.curvature.contains(curvature),
6470                "weighted scalar curvature {curvature} at rho={rho:.17} escaped {:?}",
6471                enclosure.curvature
6472            );
6473        }
6474    }
6475
6476    #[test]
6477    fn score_proof_refuses_exactly_when_diffuse_innovation_ball_contains_zero() {
6478        assert_eq!(
6479            Ball::ZERO.square(),
6480            Ball::ZERO,
6481            "structural zero must survive squaring exactly"
6482        );
6483        assert_eq!(
6484            Ball::ONE.square(),
6485            Ball::ONE,
6486            "the exact unit covariance must not acquire artificial width"
6487        );
6488        let tiny = f64::from_bits(1);
6489        let nodes = [
6490            PooledNode {
6491                x: 0.0,
6492                y: 0.0,
6493                w: 1.0,
6494            },
6495            PooledNode {
6496                x: tiny,
6497                y: 1.0,
6498                w: 1.0,
6499            },
6500            PooledNode {
6501                x: 1.0,
6502                y: -1.0,
6503                w: 1.0,
6504            },
6505        ];
6506        let error = run_filter_ball(&nodes, Ball::ONE, 2)
6507            .expect_err("an underflow-wide diffuse innovation cannot be divided soundly");
6508        assert!(matches!(
6509            error,
6510            SplineScoreProofError::InnovationContainsZero {
6511                node: 1,
6512                kind: SplineInnovationKind::Diffuse,
6513                ..
6514            }
6515        ));
6516    }
6517
6518    /// A hand-built persisted state, valid by `from_state`'s own structural
6519    /// rules: `state` is `order` per knot, `cov` the `order(order+1)/2` upper
6520    /// triangle per knot, `gain` the full `order²` per knot, one weight per
6521    /// knot, strictly increasing knots, `sigma2 > 0`, and an in-range
6522    /// `log_lambda`. No fitting is involved.
6523    fn hand_built_state(order: usize) -> SplineScanState {
6524        let knots = vec![0.0, 0.25, 0.6, 1.0, 1.4];
6525        let knot_count = knots.len();
6526        let tri = order * (order + 1) / 2;
6527        SplineScanState {
6528            order,
6529            state: (0..order * knot_count)
6530                .map(|i| 0.1 + 0.07 * i as f64)
6531                .collect(),
6532            // Diagonal-leading per knot so restored variances stay positive.
6533            cov: (0..tri * knot_count)
6534                .map(|i| {
6535                    if i % tri == 0 {
6536                        0.5 + 0.01 * i as f64
6537                    } else {
6538                        0.02
6539                    }
6540                })
6541                .collect(),
6542            gain: (0..order * order * knot_count)
6543                .map(|i| 0.03 * ((i % 5) as f64))
6544                .collect(),
6545            node_weight: (0..knot_count).map(|i| 1.0 + 0.25 * i as f64).collect(),
6546            knots,
6547            log_lambda: 0.35,
6548            sigma2: 1.75,
6549            restricted_loglik: -12.5,
6550            log_likelihood: -8.25,
6551            training_sample_size: std::num::NonZeroU64::new(64).expect("64 is nonzero"),
6552            data_sse: 3.25,
6553        }
6554    }
6555
6556    /// #2614 decoupling: the #1034/#1044 persistence seam, verified WITHOUT the
6557    /// optimizer.
6558    ///
6559    /// `round_trip_predict_bit_for_bit` opens with
6560    /// `fit_spline_scan(...).expect("scan fit")`, so while the certified scan
6561    /// refuses (#2614, measured: two of those three tests die there) every
6562    /// assertion behind it is WITHDRAWN rather than failing — the bit-for-bit
6563    /// posteriors, the off-knot bridge, both extrapolation sides, and the three
6564    /// corrupt-payload rejections are all unprotected, and the red count reads
6565    /// "some tests fail" when the truth is "a guarantee is untested".
6566    ///
6567    /// A serialization guarantee must not depend on an optimizer guarantee.
6568    /// `SplineScanFit::from_state` reconstructs a fit from a plain
6569    /// `SplineScanState`, so the whole seam can be driven from a hand-built
6570    /// state and holds regardless of whether any fit converges. This does NOT
6571    /// replace the fitted round-trip, which additionally proves the fitter's own
6572    /// fields survive; it makes the seam itself independently covered.
6573    #[test]
6574    fn persistence_seam_round_trips_without_the_optimizer_2614() {
6575        for order in 1..=MAX_ORDER {
6576            let built = hand_built_state(order);
6577            let fit = SplineScanFit::from_state(&built).expect("hand-built state must restore");
6578            let json = serde_json::to_string(&fit.to_state()).expect("serialize state");
6579            let parsed: SplineScanState = serde_json::from_str(&json).expect("deserialize state");
6580            let restored = SplineScanFit::from_state(&parsed).expect("restore fit");
6581
6582            assert_eq!(fit.order, restored.order, "order drifted (m={order})");
6583            assert_eq!(fit.knots, restored.knots, "knots drifted (m={order})");
6584            assert_eq!(fit.log_lambda.to_bits(), restored.log_lambda.to_bits());
6585            assert_eq!(fit.sigma2.to_bits(), restored.sigma2.to_bits());
6586            assert_eq!(
6587                fit.log_likelihood.to_bits(),
6588                restored.log_likelihood.to_bits()
6589            );
6590            assert_eq!(fit.edf().to_bits(), restored.edf().to_bits());
6591            assert_eq!(fit.deviance().to_bits(), restored.deviance().to_bits());
6592            assert_eq!(fit.training_sample_size(), restored.training_sample_size());
6593
6594            // Off-knot bridge, exact knot hits, and both extrapolation sides.
6595            for &xq in &[-0.3, 0.0, 0.13, 0.6, 1.0, 1.4, 1.9] {
6596                let (m0, v0) = fit.predict(xq).expect("predict original");
6597                let (m1, v1) = restored.predict(xq).expect("predict restored");
6598                assert_eq!(
6599                    m0.to_bits(),
6600                    m1.to_bits(),
6601                    "mean drift at x={xq} (m={order})"
6602                );
6603                assert_eq!(
6604                    v0.to_bits(),
6605                    v1.to_bits(),
6606                    "variance drift at x={xq} (m={order})"
6607                );
6608            }
6609
6610            // Corrupt payloads fail loudly, not inside a later predict.
6611            let mut bad = fit.to_state();
6612            bad.cov.truncate(bad.cov.len() - 1);
6613            SplineScanFit::from_state(&bad).expect_err("length mismatch must error");
6614            let mut bad = fit.to_state();
6615            bad.sigma2 = -1.0;
6616            SplineScanFit::from_state(&bad).expect_err("non-positive sigma2 must error");
6617            let mut bad = fit.to_state();
6618            bad.knots[2] = bad.knots[1];
6619            SplineScanFit::from_state(&bad).expect_err("non-increasing knots must error");
6620        }
6621    }
6622
6623    /// A constant response shift is an exact null-space transformation for
6624    /// every supported spline order. Pin that theorem at the fixed-lambda
6625    /// evaluator seam, including tied rows with non-unit weights: all invariant
6626    /// quantities must be bit-identical, and the only moving state coordinate
6627    /// must be the published function level. Ties are essential here because
6628    /// centering only after their weighted pooling leaves the old origin leak
6629    /// alive before the recurrence starts.
6630    #[test]
6631    fn fixed_scan_evaluator_uses_a_response_translation_free_chart_2790() {
6632        let x = [0.0, 0.0, 0.25, 0.5, 0.5, 1.0, 1.5, 2.0];
6633        let y: [f64; 8] = [0.0, 0.25, -0.5, 0.75, 1.0, -0.25, 0.5, 0.125];
6634        let shift = 1024.0_f64;
6635        let shifted_y = y.map(|value| value + shift);
6636        let w = [0.3, 1.7, 2.25, 0.6, 1.4, 3.1, 0.75, 2.6];
6637
6638        for order in 1..=MAX_ORDER {
6639            let plain = fit_spline_scan_at(&x, &y, &w, -1.25, None, order)
6640                .expect("plain fixed-lambda scan");
6641            let shifted = fit_spline_scan_at(&x, &shifted_y, &w, -1.25, None, order)
6642                .expect("shifted fixed-lambda scan");
6643            assert!(
6644                plain.knots.len() < x.len(),
6645                "fixture must exercise tied-row pooling"
6646            );
6647            assert_eq!(plain.knots, shifted.knots);
6648            assert_eq!(plain.node_weight, shifted.node_weight);
6649
6650            for (label, left, right) in [
6651                ("sigma2", plain.sigma2, shifted.sigma2),
6652                (
6653                    "restricted log likelihood",
6654                    plain.restricted_loglik,
6655                    shifted.restricted_loglik,
6656                ),
6657                ("Gaussian log likelihood", plain.log_likelihood, shifted.log_likelihood),
6658                ("data SSE", plain.data_sse, shifted.data_sse),
6659                ("EDF", plain.edf(), shifted.edf()),
6660            ] {
6661                assert_eq!(
6662                    left.to_bits(),
6663                    right.to_bits(),
6664                    "order {order}: {label} moved under a constant response shift"
6665                );
6666            }
6667            for node in 0..plain.knots.len() {
6668                assert_eq!(
6669                    shifted.mean[node].to_bits(),
6670                    (plain.mean[node] + shift).to_bits(),
6671                    "order {order}: fitted level at node {node} is not exactly equivariant"
6672                );
6673                assert_eq!(
6674                    shifted.var[node].to_bits(),
6675                    plain.var[node].to_bits(),
6676                    "order {order}: posterior variance moved at node {node}"
6677                );
6678            }
6679            assert_eq!(shifted.deriv, plain.deriv);
6680        }
6681    }
6682
6683    /// `deviance()` must be the weighted DATA residual sum of squares at the
6684    /// fitted values, not the profiled REML quadratic. For order 1 on
6685    /// `x = (0, 1)`, `y = (0, 1)`, unit weights, λ = 1, the posterior mean is
6686    /// `(1/3, 2/3)`: the data SSE is `2·(1/3)² = 2/9`, while
6687    /// `σ̂²·(n − order) = 1/3` carries an extra `1/9` of process/roughness
6688    /// energy.
6689    #[test]
6690    fn deviance_is_data_sse_not_penalized_quadratic() {
6691        let x = [0.0, 1.0];
6692        let y = [0.0, 1.0];
6693        let w = [1.0, 1.0];
6694        let fit = fit_spline_scan_at(&x, &y, &w, 0.0, None, 1).expect("order-1 fit");
6695        // Self-consistency against a direct recomputation at the fitted values.
6696        let manual: f64 = x
6697            .iter()
6698            .zip(&y)
6699            .zip(&w)
6700            .map(|((&xi, &yi), &wi)| {
6701                let (m, _) = fit.predict(xi).expect("predict at knot");
6702                wi * (yi - m) * (yi - m)
6703            })
6704            .sum();
6705        assert!(
6706            (fit.deviance() - manual).abs() <= 1e-12 * manual.max(1e-300),
6707            "deviance {} != recomputed data SSE {manual}",
6708            fit.deviance()
6709        );
6710        assert!(
6711            (fit.deviance() - 2.0 / 9.0).abs() < 1e-10,
6712            "deviance {} != 2/9",
6713            fit.deviance()
6714        );
6715        // The old proxy is strictly larger: it includes penalty energy.
6716        let reml_quadratic = fit.sigma2 * (fit.training_sample_size() as f64 - fit.order as f64);
6717        assert!(fit.deviance() < reml_quadratic);
6718    }
6719
6720    #[test]
6721    fn full_gaussian_log_likelihood_keeps_raw_weight_normalizer_and_round_trips() {
6722        // The first two rows share an abscissa. Their individual log-weight
6723        // normalizers cannot be recovered from the pooled node weight.
6724        let x = [0.0, 0.0, 1.0, 2.0];
6725        let y = [0.2, -0.1, 0.8, 1.4];
6726        let w = [0.5, 2.0, 1.5, 3.0];
6727        let sigma2 = 1.7;
6728        let fit = fit_spline_scan_at(&x, &y, &w, 0.2, Some(sigma2), 1)
6729            .expect("weighted order-1 fit");
6730
6731        let sum_log_weights = w.iter().map(|weight| weight.ln()).sum::<f64>();
6732        let expected = -0.5
6733            * (fit.deviance() / sigma2
6734                + x.len() as f64 * (std::f64::consts::TAU.ln() + sigma2.ln())
6735                - sum_log_weights);
6736        assert!(
6737            (fit.log_likelihood - expected).abs() <= 1e-12 * expected.abs().max(1.0)
6738        );
6739
6740        let pooled_log_weights = fit
6741            .node_weight
6742            .iter()
6743            .map(|weight| weight.ln())
6744            .sum::<f64>();
6745        let pooled_wrong = -0.5
6746            * (fit.deviance() / sigma2
6747                + x.len() as f64 * (std::f64::consts::TAU.ln() + sigma2.ln())
6748                - pooled_log_weights);
6749        assert!(
6750            (fit.log_likelihood - pooled_wrong).abs() > 1e-3,
6751            "raw-row weight normalizer must not collapse to pooled weights"
6752        );
6753
6754        let restored = SplineScanFit::from_state(&fit.to_state()).expect("restore fit");
6755        assert_eq!(
6756            fit.log_likelihood.to_bits(),
6757            restored.log_likelihood.to_bits()
6758        );
6759
6760        let mut incomplete = serde_json::to_value(fit.to_state()).expect("serialize state");
6761        incomplete
6762            .as_object_mut()
6763            .expect("state serializes as an object")
6764            .remove("log_likelihood");
6765        let error = serde_json::from_value::<SplineScanState>(incomplete)
6766            .expect_err("log_likelihood is a required wire field");
6767        assert!(error.to_string().contains("missing field `log_likelihood`"));
6768    }
6769}