Skip to main content

gam_terms/basis/
measure_jet_predict.rs

1//! Predict-side measure-jet honesty: the closed-form extrapolation variance
2//! from the frame notes (`docs/measure_jet_frame.md` §5).
3//!
4//! The current Gaussian representers decay off-support toward the parametric
5//! backbone with small posterior variance — confident reversion, which the
6//! honesty contract forbids. The structural fix prices ignorance off the web from the SAME
7//! fitted spectrum that smooths on it: every band level ℓ carries a fitted
8//! amplitude λ̂_ℓ (prior precision of the level's innovations), and a query
9//! that the level-ℓ kernel mass does not cover simply has an UNKNOWN level-ℓ
10//! innovation — prior variance λ̂_ℓ⁻¹, collected in full.
11//!
12//! # The formula (and its algebraic relation to §5)
13//!
14//! With `q̄_ℓ = (Σ_i m_i q_ℓ(c_i)) / (Σ_i m_i)` the web-averaged scale-ℓ
15//! support and `a_ℓ(x★) = min(q_ℓ(x★)/q̄_ℓ, 1)` the scale-correct on-web-ness
16//! weight in `[0, 1]`, let
17//! `ℓ★ = min{ℓ : q_ℓ(x★) ≥ coverage_floor · q̄_ℓ}` be the first covering
18//! level (ε★ = ε_{ℓ★}). Then, for per-level spectra,
19//!
20//! ```text
21//!   Var_extrap(x★) = Σ_{ℓ < ℓ★} λ̂_ℓ⁻¹  +  Σ_{ℓ ≥ ℓ★} (1 − a_ℓ(x★)) · λ̂_ℓ⁻¹
22//!                  = Σ_ℓ λ̂_ℓ⁻¹  −  Σ_{ℓ: ε_ℓ ≥ ε★} a_ℓ(x★) · λ̂_ℓ⁻¹ .
23//! ```
24//!
25//! The second line is the §5 statement: the total prior ignorance of the
26//! spectrum minus the part the query's coverage recovers — the recovered sum
27//! runs over the covered levels `ε_ℓ ≥ ε★` exactly as written in the charter.
28//! In fused mode the band has one precision, so the same coverage idea reduces
29//! to one charge: `λ_fused⁻¹` if no level clears its floor, otherwise
30//! `(1 − max_ℓ a_ℓ(x★)) · λ_fused⁻¹`.
31//! On-web queries (ε★ = ε_0, a_ℓ ≈ 1 everywhere) recover the full spectrum
32//! and pay ≈ 0 extra; far-off queries recover (almost) nothing and pay the
33//! full Σ_ℓ λ̂_ℓ⁻¹. Levels FINER than the first covering scale get no credit
34//! for stray sub-floor kernel mass: below ε★ the prediction is a jet
35//! extension, not an interpolation, so those innovations are charged as pure
36//! ignorance.
37//!
38//! # Never-covered convention
39//!
40//! If no band level clears the coverage floor (ε★ lies past the band), the
41//! covered set is EMPTY: in per-level mode every level contributes its full
42//! λ̂_ℓ⁻¹ and `Var_extrap = Σ_ℓ λ̂_ℓ⁻¹`; in fused mode the single band
43//! amplitude contributes once. The variance saturates at the spectrum's total
44//! prior ignorance instead of growing without bound, which is the honest
45//! statement: the model's coefficient prior is the only information it ever
46//! claimed about such a point.
47//!
48//! # Monotonicity (the distance-honesty theorem)
49//!
50//! Claim: if `q ≤ q′` pointwise (the support row of the farther query is
51//! nowhere larger), then `Var_extrap(q) ≥ Var_extrap(q′)`.
52//!
53//! Proof. `{ℓ : q_ℓ ≥ coverage_floor · q̄_ℓ} ⊆
54//! {ℓ : q′_ℓ ≥ coverage_floor · q̄_ℓ}` for the scale-specific floors, so
55//! `ℓ★(q) ≥ ℓ★(q′)`. Compare the per-level weights `w_ℓ`:
56//! - `ℓ < ℓ★(q′)`: both weights are 1;
57//! - `ℓ ≥ ℓ★(q)`: `w_ℓ(q) = 1 − a_ℓ(q) ≥ 1 − a_ℓ(q′) = w_ℓ(q′)`;
58//! - `ℓ★(q′) ≤ ℓ < ℓ★(q)`: `w_ℓ(q) = 1 ≥ 1 − a_ℓ(q′) = w_ℓ(q′)`.
59//! Every weight is no smaller and every `λ̂_ℓ⁻¹ > 0`, so the sum is no
60//! smaller. ∎
61//!
62//! Since the Gaussian kernel mass `q_ℓ(x★)` is pointwise nonincreasing as
63//! `x★` recedes from every center simultaneously, intervals widen
64//! monotonically with distance from the web. The ε★ gate introduces the only
65//! discontinuity, and it is bounded: a level crossing the floor changes its
66//! weight by at most `a_ℓ ≤ coverage_floor`, so the total jump is at most
67//! `coverage_floor · Σ_ℓ λ̂_ℓ⁻¹` and vanishes as the floor tightens.
68//!
69//! # Units
70//!
71//! The result is on the scale of physical `λ̂⁻¹`: callers must unnormalize the
72//! fitted Frobenius-normalized precision first (`λ_phys = λ_tilde / c`). Family
73//! dispersion scaling remains outside this pure spectrum-side kernel.
74
75use ndarray::{Array1, ArrayView1, ArrayView2};
76
77use super::BasisError;
78
79/// Analytic ambient gradient `∇f̂(x★)` of a frozen measure-jet term's fitted
80/// contribution to the linear predictor, in the (standardized) ambient
81/// coordinates the frozen geometry lives in.
82///
83/// The term's fitted contribution is the augmented representer expansion
84/// `f̂(x) = Σ_i z_i · K(x, c_i)  +  Σ_h h_h · ([1 | x]ᵀ T_aff)_h`, where
85/// `K(x, c_i) = exp(−‖x − c_i‖² / (2 ℓ²))` is the Gaussian representer, `z`
86/// the raw-space representer coefficients (`z_full[..m]`), `T_aff` the AFFINE
87/// head lift (`(d+1) × head_width`, row 0 the constant — see
88/// [`crate::basis::measure_jet_affine_head_lift`]) and `h` the raw-space head
89/// coefficients (`z_full[m..]`). Both coefficient blocks are the raw (pre-gauge)
90/// coefficients — lift the fitted reduced coefficients through the frozen
91/// identifiability transform first (`β_raw = Z · β̂`).
92///
93/// Differentiating in `x` (exact, hand-derived — no FD, no autodiff):
94///
95/// ```text
96///   ∂K(x, c_i)/∂x_a       = K(x, c_i) · (c_{i,a} − x_a) / ℓ² ,
97///   ∂([1|x]ᵀ T_aff)_h/∂x_a = T_aff_{a+1,h} ,
98/// ```
99///
100/// (the constant row contributes nothing to the gradient), so the ambient
101/// gradient is
102///
103/// ```text
104///   ∇f̂(x)_a = Σ_i z_i · K(x, c_i) · (c_{i,a} − x_a) / ℓ²  +  Σ_h h_h · T_aff_{a+1,h} .
105/// ```
106///
107/// This is the first-order (jet) term of the measure-jet expansion the frame
108/// notes already carry; here it is read out in closed form for the
109/// errors-in-variables predictive-variance term
110/// `Var_input(x★) = ∇f̂(x★)ᵀ Σ_x ∇f̂(x★)` (issue #2225). `head_transform`/
111/// `head_coeffs` are `None`/empty when the term carries no ambient-linear head.
112pub fn measure_jet_ambient_gradient(
113    query: ArrayView1<'_, f64>,
114    centers: ArrayView2<'_, f64>,
115    representer_coeffs: ArrayView1<'_, f64>,
116    length_scale: f64,
117    head_transform: Option<ArrayView2<'_, f64>>,
118    head_coeffs: ArrayView1<'_, f64>,
119) -> Result<Array1<f64>, BasisError> {
120    let d = query.len();
121    let m = centers.nrows();
122    if centers.ncols() != d {
123        crate::bail_dim_basis!(
124            "measure-jet ambient gradient: query dimension {d} disagrees with centers ({} × {})",
125            m,
126            centers.ncols()
127        );
128    }
129    if representer_coeffs.len() != m {
130        crate::bail_dim_basis!(
131            "measure-jet ambient gradient: {} representer coefficients for {m} centers",
132            representer_coeffs.len()
133        );
134    }
135    if !(length_scale.is_finite() && length_scale > 0.0) {
136        crate::bail_invalid_basis!(
137            "measure-jet ambient gradient needs a positive finite length_scale; got {length_scale}"
138        );
139    }
140    let inv_l2 = 1.0 / (length_scale * length_scale);
141    let mut grad = Array1::<f64>::zeros(d);
142    for i in 0..m {
143        let center = centers.row(i);
144        let mut sq = 0.0_f64;
145        for a in 0..d {
146            let delta = query[a] - center[a];
147            sq += delta * delta;
148        }
149        let k = (-0.5 * sq * inv_l2).exp();
150        let coeff = representer_coeffs[i] * k * inv_l2;
151        for a in 0..d {
152            // ∂K/∂x_a = K · (c_{i,a} − x_a) / ℓ².
153            grad[a] += coeff * (center[a] - query[a]);
154        }
155    }
156    if let Some(t) = head_transform {
157        let head_width = t.ncols();
158        if head_coeffs.len() != head_width {
159            crate::bail_dim_basis!(
160                "measure-jet ambient gradient: {} head coefficients for a head lift with \
161                 {head_width} columns",
162                head_coeffs.len()
163            );
164        }
165        if t.nrows() != d + 1 {
166            crate::bail_dim_basis!(
167                "measure-jet ambient gradient: affine head lift has {} rows but ambient \
168                 dimension is {d} (expected d+1)",
169                t.nrows()
170            );
171        }
172        for a in 0..d {
173            let mut acc = 0.0_f64;
174            for h in 0..head_width {
175                // Row 0 of the affine lift is the constant; the gradient in
176                // coordinate `a` reads row `a + 1`.
177                acc += head_coeffs[h] * t[(a + 1, h)];
178            }
179            grad[a] += acc;
180        }
181    } else if !head_coeffs.is_empty() {
182        crate::bail_dim_basis!(
183            "measure-jet ambient gradient: {} head coefficients supplied without a head lift",
184            head_coeffs.len()
185        );
186    }
187    Ok(grad)
188}
189
190#[derive(Clone, Copy)]
191pub enum MeasureJetExtrapolationSpectrum<'a> {
192    /// One physical precision per band level.
193    PerLevel(&'a [f64]),
194    /// One physical precision for the fused band. It is charged once, with the
195    /// band's best coverage fraction.
196    Fused(f64),
197}
198
199/// Frame-note §5: closed-form extrapolation variance at a query — the price of
200/// ignorance off the web, read from the fitted spectrum. `ε★` = the first
201/// covering scale (smallest band scale at which the query's kernel mass
202/// clears `coverage_floor` × `q̄_ℓ`); levels finer than `ε★` contribute
203/// their full prior variance `λ̂_ℓ⁻¹`, levels from `ε★` up contribute the
204/// uncovered fraction `(1 − a_ℓ(x★)) · λ̂_ℓ⁻¹` with
205/// `a_ℓ(x★) = min(q_ℓ(x★)/q̄_ℓ, 1)` the smooth on-web-ness weight.
206/// Equivalently (see the module docs) the total prior ignorance
207/// `Σ_ℓ λ̂_ℓ⁻¹` minus the §5 coverage-recovered sum
208/// `Σ_{ℓ: ε_ℓ ≥ ε★} a_ℓ(x★)/λ̂_ℓ`. On-web queries (ε★ = ε_0, a ≈ 1) pay
209/// ≈ 0 extra; queries never covered by the band pay `Σ_ℓ λ̂_ℓ⁻¹` exactly —
210/// intervals widen monotonically with distance (theorem in the module docs).
211///
212/// Inputs: `support_row` = `q_ℓ(x★)` per band scale (one row of
213/// [`super::measure_jet_support_curve`]), `eps_band` the realized ascending
214/// band, `support_means` = `q̄_ℓ` per band scale, `spectrum` the physical
215/// precision spectrum, `coverage_floor` ∈ (0, 1) (e.g. 0.05).
216pub fn measure_jet_extrapolation_variance(
217    support_row: ArrayView1<'_, f64>,
218    eps_band: &[f64],
219    support_means: &[f64],
220    spectrum: MeasureJetExtrapolationSpectrum<'_>,
221    coverage_floor: f64,
222) -> Result<f64, BasisError> {
223    let n_levels = eps_band.len();
224    if n_levels == 0 {
225        crate::bail_invalid_basis!("measure-jet extrapolation variance needs a nonempty band");
226    }
227    if support_row.len() != n_levels || support_means.len() != n_levels {
228        crate::bail_dim_basis!(
229            "measure-jet extrapolation variance needs one support value and one support mean per \
230             band scale: {} support values, {} support means, {} scales",
231            support_row.len(),
232            support_means.len(),
233            n_levels
234        );
235    }
236    for (l, pair) in eps_band.windows(2).enumerate() {
237        if pair[1] <= pair[0] {
238            crate::bail_invalid_basis!(
239                "measure-jet band must be strictly ascending: eps[{l}] = {} vs eps[{}] = {}",
240                pair[0],
241                l + 1,
242                pair[1]
243            );
244        }
245    }
246    if eps_band.iter().any(|e| !(e.is_finite() && *e > 0.0)) {
247        crate::bail_invalid_basis!("measure-jet band scales must be finite and positive");
248    }
249    if support_row.iter().any(|q| !(q.is_finite() && *q >= 0.0)) {
250        crate::bail_invalid_basis!(
251            "measure-jet support row must be finite and nonnegative (kernel masses)"
252        );
253    }
254    if support_means.iter().any(|q| !(q.is_finite() && *q > 0.0)) {
255        crate::bail_invalid_basis!("measure-jet support means must be finite and positive");
256    }
257    if !(coverage_floor.is_finite() && coverage_floor > 0.0 && coverage_floor < 1.0) {
258        crate::bail_invalid_basis!(
259            "measure-jet coverage floor must lie strictly in (0, 1); got {coverage_floor}"
260        );
261    }
262    match spectrum {
263        MeasureJetExtrapolationSpectrum::PerLevel(lambda_hat) => {
264            if lambda_hat.len() != n_levels {
265                crate::bail_dim_basis!(
266                    "measure-jet per-level extrapolation variance needs one physical precision per \
267                     band scale: {} precisions, {} scales",
268                    lambda_hat.len(),
269                    n_levels
270                );
271            }
272            if lambda_hat.iter().any(|l| !(l.is_finite() && *l > 0.0)) {
273                crate::bail_invalid_basis!(
274                    "measure-jet per-scale amplitudes must be finite and positive (physical precisions)"
275                );
276            }
277            let first_covering = support_row
278                .iter()
279                .zip(support_means.iter())
280                .position(|(q, q_bar)| *q >= coverage_floor * *q_bar)
281                .unwrap_or(n_levels);
282            let mut variance = 0.0_f64;
283            for (l, ((&q, &q_bar), &lam)) in support_row
284                .iter()
285                .zip(support_means.iter())
286                .zip(lambda_hat.iter())
287                .enumerate()
288            {
289                let weight = if l < first_covering {
290                    1.0
291                } else {
292                    1.0 - (q / q_bar).min(1.0)
293                };
294                variance += weight / lam;
295            }
296            Ok(variance)
297        }
298        MeasureJetExtrapolationSpectrum::Fused(lambda_hat) => {
299            if !(lambda_hat.is_finite() && lambda_hat > 0.0) {
300                crate::bail_invalid_basis!(
301                    "measure-jet fused amplitude must be finite and positive (physical precision)"
302                );
303            }
304            let mut best_coverage = 0.0_f64;
305            let mut covered = false;
306            for (&q, &q_bar) in support_row.iter().zip(support_means.iter()) {
307                let coverage = (q / q_bar).min(1.0);
308                best_coverage = best_coverage.max(coverage);
309                if q >= coverage_floor * q_bar {
310                    covered = true;
311                }
312            }
313            let weight = if covered { 1.0 - best_coverage } else { 1.0 };
314            Ok(weight / lambda_hat)
315        }
316    }
317}
318
319#[cfg(test)]
320mod tests {
321    use super::*;
322    use ndarray::{Array1, arr1};
323
324    /// Shared deterministic fixture: a 5-level dyadic band with a
325    /// non-constant fitted spectrum.
326    pub(crate) fn band() -> Vec<f64> {
327        vec![0.05, 0.1, 0.2, 0.4, 0.8]
328    }
329
330    pub(crate) fn lambdas() -> Vec<f64> {
331        vec![40.0, 11.0, 3.5, 1.25, 0.6]
332    }
333
334    pub(crate) fn support_means(eps: &[f64]) -> Vec<f64> {
335        vec![TOTAL; eps.len()]
336    }
337
338    pub(crate) const FLOOR: f64 = 0.05;
339    pub(crate) const TOTAL: f64 = 1.0;
340
341    pub(crate) fn total_ignorance(lams: &[f64]) -> f64 {
342        lams.iter().map(|l| 1.0 / l).sum()
343    }
344
345    /// The exact single-unit-mass support curve at distance `d`:
346    /// q_ℓ(d) = total · exp(−d²/(2ε_ℓ²)) — the physical family the support
347    /// diagnostic produces for a one-center web.
348    pub(crate) fn support_at_distance(d: f64, eps: &[f64]) -> Array1<f64> {
349        Array1::from_iter(eps.iter().map(|e| TOTAL * (-d * d / (2.0 * e * e)).exp()))
350    }
351
352    /// (a) Monotone in distance: along the exact kernel-mass family the
353    /// support row is pointwise nonincreasing in d, so the variance must be
354    /// nondecreasing — including across every coverage-floor crossing in the
355    /// sweep.
356    #[test]
357    pub(crate) fn extrapolation_variance_is_monotone_in_distance() {
358        let eps = band();
359        let lams = lambdas();
360        let q_bar = support_means(&eps);
361        let mut prev = -1.0_f64;
362        // 0 → 6 in steps of 0.015: spans on-web through far-off, crossing
363        // the floor at every band level along the way.
364        for step in 0..400 {
365            let d = 0.015 * step as f64;
366            let row = support_at_distance(d, &eps);
367            let v = measure_jet_extrapolation_variance(
368                row.view(),
369                &eps,
370                &q_bar,
371                MeasureJetExtrapolationSpectrum::PerLevel(&lams),
372                FLOOR,
373            )
374            .expect("valid inputs");
375            assert!(
376                v >= prev,
377                "variance decreased with distance: variance({d:.3}) = {v:.12} < {prev:.12}"
378            );
379            prev = v;
380        }
381        // And the saturation: the far end of the sweep reaches the full
382        // prior ignorance (never-covered convention).
383        assert!(
384            (prev - total_ignorance(&lams)).abs() <= 1e-12,
385            "far-field variance must saturate at Σ 1/λ̂: got {prev}"
386        );
387    }
388
389    /// (a′) Pointwise domination, no geometric family assumed: a support row
390    /// that is pointwise smaller never yields smaller variance — exercised
391    /// on a NON-monotone-in-ℓ row pair as well.
392    #[test]
393    pub(crate) fn extrapolation_variance_is_monotone_under_pointwise_domination() {
394        let eps = band();
395        let lams = lambdas();
396        let q_bar = support_means(&eps);
397        let rows = [
398            arr1(&[0.9, 0.95, 0.99, 1.0, 1.0]),
399            arr1(&[0.02, 0.3, 0.06, 0.8, 0.97]),
400            arr1(&[0.0, 0.0, 0.04, 0.2, 0.6]),
401            arr1(&[0.04, 0.04, 0.04, 0.04, 0.049]),
402        ];
403        for row in &rows {
404            for shrink in [1.0, 0.9, 0.7, 0.3, 0.0] {
405                let smaller = row.mapv(|q| shrink * q);
406                let v_big = measure_jet_extrapolation_variance(
407                    row.view(),
408                    &eps,
409                    &q_bar,
410                    MeasureJetExtrapolationSpectrum::PerLevel(&lams),
411                    FLOOR,
412                )
413                .expect("valid inputs");
414                let v_small = measure_jet_extrapolation_variance(
415                    smaller.view(),
416                    &eps,
417                    &q_bar,
418                    MeasureJetExtrapolationSpectrum::PerLevel(&lams),
419                    FLOOR,
420                )
421                .expect("valid inputs");
422                assert!(
423                    v_small >= v_big,
424                    "pointwise-smaller support gave smaller variance: {v_small} < {v_big} \
425                     (row {row:?}, shrink {shrink})"
426                );
427            }
428        }
429    }
430
431    /// (b) On-web limit: full kernel mass at every scale prices ZERO extra
432    /// variance; near-full mass prices at most the uncovered fraction of the
433    /// total prior ignorance.
434    #[test]
435    pub(crate) fn extrapolation_variance_vanishes_on_web() {
436        let eps = band();
437        let lams = lambdas();
438        let q_bar = support_means(&eps);
439        let full = Array1::from_elem(eps.len(), TOTAL);
440        let v_full = measure_jet_extrapolation_variance(
441            full.view(),
442            &eps,
443            &q_bar,
444            MeasureJetExtrapolationSpectrum::PerLevel(&lams),
445            FLOOR,
446        )
447        .expect("valid inputs");
448        assert_eq!(v_full, 0.0, "full coverage must price zero extra variance");
449
450        let near = Array1::from_elem(eps.len(), 0.97 * TOTAL);
451        let v_near = measure_jet_extrapolation_variance(
452            near.view(),
453            &eps,
454            &q_bar,
455            MeasureJetExtrapolationSpectrum::PerLevel(&lams),
456            FLOOR,
457        )
458        .expect("valid inputs");
459        let budget = total_ignorance(&lams);
460        assert!(
461            v_near <= 0.05 * budget,
462            "near-full coverage must price a small fraction of Σ 1/λ̂: {v_near} vs budget {budget}"
463        );
464    }
465
466    /// (c) Off-web limit: zero support everywhere (never covered) collects
467    /// the spectrum's total prior ignorance Σ 1/λ̂ EXACTLY.
468    #[test]
469    pub(crate) fn extrapolation_variance_saturates_off_web() {
470        let eps = band();
471        let lams = lambdas();
472        let q_bar = support_means(&eps);
473        let zero = Array1::<f64>::zeros(eps.len());
474        let v = measure_jet_extrapolation_variance(
475            zero.view(),
476            &eps,
477            &q_bar,
478            MeasureJetExtrapolationSpectrum::PerLevel(&lams),
479            FLOOR,
480        )
481        .expect("valid inputs");
482        assert_eq!(
483            v,
484            total_ignorance(&lams),
485            "never-covered query must pay Σ 1/λ̂ exactly"
486        );
487    }
488
489    /// (d) Spectrum scaling: doubling every fitted amplitude halves the
490    /// variance — the λ̂⁻¹ pricing is exact, in every coverage regime.
491    #[test]
492    pub(crate) fn extrapolation_variance_halves_when_amplitudes_double() {
493        let eps = band();
494        let lams = lambdas();
495        let q_bar = support_means(&eps);
496        let doubled: Vec<f64> = lams.iter().map(|l| 2.0 * l).collect();
497        // Mixed regime: some levels below the floor, some covered partially,
498        // some fully — both weight branches exercised.
499        let rows = [
500            support_at_distance(0.35, &eps),
501            Array1::<f64>::zeros(eps.len()),
502            Array1::from_elem(eps.len(), 0.5),
503        ];
504        for row in &rows {
505            let v1 = measure_jet_extrapolation_variance(
506                row.view(),
507                &eps,
508                &q_bar,
509                MeasureJetExtrapolationSpectrum::PerLevel(&lams),
510                FLOOR,
511            )
512            .expect("valid inputs");
513            let v2 = measure_jet_extrapolation_variance(
514                row.view(),
515                &eps,
516                &q_bar,
517                MeasureJetExtrapolationSpectrum::PerLevel(&doubled),
518                FLOOR,
519            )
520            .expect("valid inputs");
521            assert!(
522                (2.0 * v2 - v1).abs() <= 1e-15 * v1.max(1.0),
523                "doubling λ̂ must halve the variance: {v1} vs 2×{v2}"
524            );
525        }
526    }
527
528    /// Convention pin: the ε★ gate. Sub-floor mass at every level is
529    /// never-covered (full Σ 1/λ̂, no credit for stray mass); the moment ONE
530    /// level clears the floor, that level and every coarser one switch to
531    /// the smooth uncovered-fraction weight while finer levels stay fully
532    /// charged.
533    #[test]
534    pub(crate) fn extrapolation_variance_gate_convention() {
535        let eps = band();
536        let lams = lambdas();
537        let q_bar = support_means(&eps);
538        let sub_floor = Array1::from_elem(eps.len(), 0.049 * TOTAL);
539        let v_sub = measure_jet_extrapolation_variance(
540            sub_floor.view(),
541            &eps,
542            &q_bar,
543            MeasureJetExtrapolationSpectrum::PerLevel(&lams),
544            FLOOR,
545        )
546        .expect("valid inputs");
547        assert_eq!(
548            v_sub,
549            total_ignorance(&lams),
550            "sub-floor mass earns no credit: full Σ 1/λ̂"
551        );
552
553        // Coverage exactly at the floor on the coarsest level only.
554        let mut at_floor = sub_floor.clone();
555        at_floor[eps.len() - 1] = FLOOR * TOTAL;
556        let v_floor = measure_jet_extrapolation_variance(
557            at_floor.view(),
558            &eps,
559            &q_bar,
560            MeasureJetExtrapolationSpectrum::PerLevel(&lams),
561            FLOOR,
562        )
563        .expect("valid inputs");
564        let expected: f64 = lams[..eps.len() - 1].iter().map(|l| 1.0 / l).sum::<f64>()
565            + (1.0 - FLOOR) / lams[eps.len() - 1];
566        assert!(
567            (v_floor - expected).abs() <= 1e-15,
568            "floor-clearing coarsest level must take weight 1 − a: {v_floor} vs {expected}"
569        );
570        // The gate's discontinuity is bounded by the documented
571        // coverage_floor · Σ 1/λ̂ budget.
572        assert!(
573            v_sub - v_floor <= FLOOR * total_ignorance(&lams) + 1e-15,
574            "gate jump exceeds the documented coverage_floor bound"
575        );
576    }
577
578    #[test]
579    pub(crate) fn fused_extrapolation_charges_single_band_amplitude_once() {
580        let eps = band();
581        let q_bar = support_means(&eps);
582        let lam = 2.5;
583        let zero = Array1::<f64>::zeros(eps.len());
584        let v_zero = measure_jet_extrapolation_variance(
585            zero.view(),
586            &eps,
587            &q_bar,
588            MeasureJetExtrapolationSpectrum::Fused(lam),
589            FLOOR,
590        )
591        .expect("valid inputs");
592        assert_eq!(
593            v_zero,
594            1.0 / lam,
595            "never-covered fused band must pay one amplitude, not one per level"
596        );
597
598        let covered = arr1(&[0.01, 0.2, 0.4, 0.75, 0.5]);
599        let v_covered = measure_jet_extrapolation_variance(
600            covered.view(),
601            &eps,
602            &q_bar,
603            MeasureJetExtrapolationSpectrum::Fused(lam),
604            FLOOR,
605        )
606        .expect("valid inputs");
607        let expected = (1.0 - 0.75) / lam;
608        assert!(
609            (v_covered - expected).abs() <= 1e-15,
610            "fused band must use the best covered level once: {v_covered} vs {expected}"
611        );
612    }
613
614    /// Closed-form `f̂(x)`: the augmented representer expansion the analytic
615    /// gradient differentiates — used only as the finite-difference oracle.
616    fn eval_fitted(
617        query: ArrayView1<'_, f64>,
618        centers: ArrayView2<'_, f64>,
619        z: ArrayView1<'_, f64>,
620        length_scale: f64,
621        head: Option<ArrayView2<'_, f64>>,
622        head_coeffs: ArrayView1<'_, f64>,
623    ) -> f64 {
624        let inv_two_l2 = 1.0 / (2.0 * length_scale * length_scale);
625        let mut val = 0.0_f64;
626        for i in 0..centers.nrows() {
627            let mut sq = 0.0_f64;
628            for a in 0..query.len() {
629                let dlt = query[a] - centers[(i, a)];
630                sq += dlt * dlt;
631            }
632            val += z[i] * (-sq * inv_two_l2).exp();
633        }
634        if let Some(t) = head {
635            for h in 0..t.ncols() {
636                // Affine lift: row 0 multiplies the constant 1, row a+1 the
637                // coordinate a.
638                let mut proj = t[(0, h)];
639                for a in 0..query.len() {
640                    proj += query[a] * t[(a + 1, h)];
641                }
642                val += head_coeffs[h] * proj;
643            }
644        }
645        val
646    }
647
648    /// The analytic ambient gradient matches a central finite difference of the
649    /// fitted surface to FD accuracy — the delta-method propagator #2225 wires
650    /// into the predictive variance is the true ∇f̂ (representers + head).
651    #[test]
652    pub(crate) fn ambient_gradient_matches_central_difference() {
653        use ndarray::{arr1, arr2};
654        let centers = arr2(&[[0.0, 0.0], [1.0, 0.5], [-0.7, 0.9], [0.4, -1.1]]);
655        let z = arr1(&[0.8, -1.3, 0.5, 2.0]);
656        let length_scale = 0.6;
657        // A rank-2 AFFINE head lift T_aff (d=2, head_width=3): row 0 is the
658        // constant, rows 1..=2 the two ambient-linear directions. The constant
659        // column must contribute nothing to the gradient, which is exactly what
660        // the FD comparison below pins.
661        let t = arr2(&[[1.0, 0.0, 0.0], [0.0, 1.0, 0.2], [0.0, -0.3, 0.9]]);
662        let head_coeffs = arr1(&[1.9, 0.7, -0.4]);
663        let query = arr1(&[0.15, -0.2]);
664
665        let grad = measure_jet_ambient_gradient(
666            query.view(),
667            centers.view(),
668            z.view(),
669            length_scale,
670            Some(t.view()),
671            head_coeffs.view(),
672        )
673        .expect("valid gradient");
674
675        let h = 1e-6;
676        for a in 0..query.len() {
677            let mut qp = query.clone();
678            let mut qm = query.clone();
679            qp[a] += h;
680            qm[a] -= h;
681            let fp = eval_fitted(
682                qp.view(),
683                centers.view(),
684                z.view(),
685                length_scale,
686                Some(t.view()),
687                head_coeffs.view(),
688            );
689            let fm = eval_fitted(
690                qm.view(),
691                centers.view(),
692                z.view(),
693                length_scale,
694                Some(t.view()),
695                head_coeffs.view(),
696            );
697            let fd = (fp - fm) / (2.0 * h);
698            assert!(
699                (grad[a] - fd).abs() <= 1e-6 * (1.0 + fd.abs()),
700                "axis {a}: analytic {} vs central FD {fd}",
701                grad[a]
702            );
703        }
704    }
705
706    /// No head: the representer-only gradient still matches FD, and a stray
707    /// head coefficient without a lift is rejected.
708    #[test]
709    pub(crate) fn ambient_gradient_representer_only_and_head_guard() {
710        use ndarray::{arr1, arr2};
711        let centers = arr2(&[[0.0], [0.5], [-0.4]]);
712        let z = arr1(&[1.0, -2.0, 0.5]);
713        let length_scale = 0.3;
714        let query = arr1(&[0.1]);
715        let empty = Array1::<f64>::zeros(0);
716        let grad = measure_jet_ambient_gradient(
717            query.view(),
718            centers.view(),
719            z.view(),
720            length_scale,
721            None,
722            empty.view(),
723        )
724        .expect("valid gradient");
725        let h = 1e-6;
726        let mut qp = query.clone();
727        let mut qm = query.clone();
728        qp[0] += h;
729        qm[0] -= h;
730        let fd = (eval_fitted(
731            qp.view(),
732            centers.view(),
733            z.view(),
734            length_scale,
735            None,
736            empty.view(),
737        ) - eval_fitted(
738            qm.view(),
739            centers.view(),
740            z.view(),
741            length_scale,
742            None,
743            empty.view(),
744        )) / (2.0 * h);
745        assert!((grad[0] - fd).abs() <= 1e-6 * (1.0 + fd.abs()));
746
747        let stray = arr1(&[1.0]);
748        assert!(
749            measure_jet_ambient_gradient(
750                query.view(),
751                centers.view(),
752                z.view(),
753                length_scale,
754                None,
755                stray.view(),
756            )
757            .is_err(),
758            "head coefficients without a head lift must be rejected"
759        );
760    }
761}