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::ArrayView1;
76
77use super::BasisError;
78
79#[derive(Clone, Copy)]
80pub enum MeasureJetExtrapolationSpectrum<'a> {
81 /// One physical precision per band level.
82 PerLevel(&'a [f64]),
83 /// One physical precision for the fused band. It is charged once, with the
84 /// band's best coverage fraction.
85 Fused(f64),
86}
87
88/// Frame-note §5: closed-form extrapolation variance at a query — the price of
89/// ignorance off the web, read from the fitted spectrum. `ε★` = the first
90/// covering scale (smallest band scale at which the query's kernel mass
91/// clears `coverage_floor` × `q̄_ℓ`); levels finer than `ε★` contribute
92/// their full prior variance `λ̂_ℓ⁻¹`, levels from `ε★` up contribute the
93/// uncovered fraction `(1 − a_ℓ(x★)) · λ̂_ℓ⁻¹` with
94/// `a_ℓ(x★) = min(q_ℓ(x★)/q̄_ℓ, 1)` the smooth on-web-ness weight.
95/// Equivalently (see the module docs) the total prior ignorance
96/// `Σ_ℓ λ̂_ℓ⁻¹` minus the §5 coverage-recovered sum
97/// `Σ_{ℓ: ε_ℓ ≥ ε★} a_ℓ(x★)/λ̂_ℓ`. On-web queries (ε★ = ε_0, a ≈ 1) pay
98/// ≈ 0 extra; queries never covered by the band pay `Σ_ℓ λ̂_ℓ⁻¹` exactly —
99/// intervals widen monotonically with distance (theorem in the module docs).
100///
101/// Inputs: `support_row` = `q_ℓ(x★)` per band scale (one row of
102/// [`super::measure_jet_support_curve`]), `eps_band` the realized ascending
103/// band, `support_means` = `q̄_ℓ` per band scale, `spectrum` the physical
104/// precision spectrum, `coverage_floor` ∈ (0, 1) (e.g. 0.05).
105pub fn measure_jet_extrapolation_variance(
106 support_row: ArrayView1<'_, f64>,
107 eps_band: &[f64],
108 support_means: &[f64],
109 spectrum: MeasureJetExtrapolationSpectrum<'_>,
110 coverage_floor: f64,
111) -> Result<f64, BasisError> {
112 let n_levels = eps_band.len();
113 if n_levels == 0 {
114 crate::bail_invalid_basis!("measure-jet extrapolation variance needs a nonempty band");
115 }
116 if support_row.len() != n_levels || support_means.len() != n_levels {
117 crate::bail_dim_basis!(
118 "measure-jet extrapolation variance needs one support value and one support mean per \
119 band scale: {} support values, {} support means, {} scales",
120 support_row.len(),
121 support_means.len(),
122 n_levels
123 );
124 }
125 for (l, pair) in eps_band.windows(2).enumerate() {
126 if pair[1] <= pair[0] {
127 crate::bail_invalid_basis!(
128 "measure-jet band must be strictly ascending: eps[{l}] = {} vs eps[{}] = {}",
129 pair[0],
130 l + 1,
131 pair[1]
132 );
133 }
134 }
135 if eps_band.iter().any(|e| !(e.is_finite() && *e > 0.0)) {
136 crate::bail_invalid_basis!("measure-jet band scales must be finite and positive");
137 }
138 if support_row.iter().any(|q| !(q.is_finite() && *q >= 0.0)) {
139 crate::bail_invalid_basis!(
140 "measure-jet support row must be finite and nonnegative (kernel masses)"
141 );
142 }
143 if support_means.iter().any(|q| !(q.is_finite() && *q > 0.0)) {
144 crate::bail_invalid_basis!("measure-jet support means must be finite and positive");
145 }
146 if !(coverage_floor.is_finite() && coverage_floor > 0.0 && coverage_floor < 1.0) {
147 crate::bail_invalid_basis!(
148 "measure-jet coverage floor must lie strictly in (0, 1); got {coverage_floor}"
149 );
150 }
151 match spectrum {
152 MeasureJetExtrapolationSpectrum::PerLevel(lambda_hat) => {
153 if lambda_hat.len() != n_levels {
154 crate::bail_dim_basis!(
155 "measure-jet per-level extrapolation variance needs one physical precision per \
156 band scale: {} precisions, {} scales",
157 lambda_hat.len(),
158 n_levels
159 );
160 }
161 if lambda_hat.iter().any(|l| !(l.is_finite() && *l > 0.0)) {
162 crate::bail_invalid_basis!(
163 "measure-jet per-scale amplitudes must be finite and positive (physical precisions)"
164 );
165 }
166 let first_covering = support_row
167 .iter()
168 .zip(support_means.iter())
169 .position(|(q, q_bar)| *q >= coverage_floor * *q_bar)
170 .unwrap_or(n_levels);
171 let mut variance = 0.0_f64;
172 for (l, ((&q, &q_bar), &lam)) in support_row
173 .iter()
174 .zip(support_means.iter())
175 .zip(lambda_hat.iter())
176 .enumerate()
177 {
178 let weight = if l < first_covering {
179 1.0
180 } else {
181 1.0 - (q / q_bar).min(1.0)
182 };
183 variance += weight / lam;
184 }
185 Ok(variance)
186 }
187 MeasureJetExtrapolationSpectrum::Fused(lambda_hat) => {
188 if !(lambda_hat.is_finite() && lambda_hat > 0.0) {
189 crate::bail_invalid_basis!(
190 "measure-jet fused amplitude must be finite and positive (physical precision)"
191 );
192 }
193 let mut best_coverage = 0.0_f64;
194 let mut covered = false;
195 for (&q, &q_bar) in support_row.iter().zip(support_means.iter()) {
196 let coverage = (q / q_bar).min(1.0);
197 best_coverage = best_coverage.max(coverage);
198 if q >= coverage_floor * q_bar {
199 covered = true;
200 }
201 }
202 let weight = if covered { 1.0 - best_coverage } else { 1.0 };
203 Ok(weight / lambda_hat)
204 }
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211 use ndarray::{Array1, arr1};
212
213 /// Shared deterministic fixture: a 5-level dyadic band with a
214 /// non-constant fitted spectrum.
215 pub(crate) fn band() -> Vec<f64> {
216 vec![0.05, 0.1, 0.2, 0.4, 0.8]
217 }
218
219 pub(crate) fn lambdas() -> Vec<f64> {
220 vec![40.0, 11.0, 3.5, 1.25, 0.6]
221 }
222
223 pub(crate) fn support_means(eps: &[f64]) -> Vec<f64> {
224 vec![TOTAL; eps.len()]
225 }
226
227 pub(crate) const FLOOR: f64 = 0.05;
228 pub(crate) const TOTAL: f64 = 1.0;
229
230 pub(crate) fn total_ignorance(lams: &[f64]) -> f64 {
231 lams.iter().map(|l| 1.0 / l).sum()
232 }
233
234 /// The exact single-unit-mass support curve at distance `d`:
235 /// q_ℓ(d) = total · exp(−d²/(2ε_ℓ²)) — the physical family the support
236 /// diagnostic produces for a one-center web.
237 pub(crate) fn support_at_distance(d: f64, eps: &[f64]) -> Array1<f64> {
238 Array1::from_iter(eps.iter().map(|e| TOTAL * (-d * d / (2.0 * e * e)).exp()))
239 }
240
241 /// (a) Monotone in distance: along the exact kernel-mass family the
242 /// support row is pointwise nonincreasing in d, so the variance must be
243 /// nondecreasing — including across every coverage-floor crossing in the
244 /// sweep.
245 #[test]
246 pub(crate) fn extrapolation_variance_is_monotone_in_distance() {
247 let eps = band();
248 let lams = lambdas();
249 let q_bar = support_means(&eps);
250 let mut prev = -1.0_f64;
251 // 0 → 6 in steps of 0.015: spans on-web through far-off, crossing
252 // the floor at every band level along the way.
253 for step in 0..400 {
254 let d = 0.015 * step as f64;
255 let row = support_at_distance(d, &eps);
256 let v = measure_jet_extrapolation_variance(
257 row.view(),
258 &eps,
259 &q_bar,
260 MeasureJetExtrapolationSpectrum::PerLevel(&lams),
261 FLOOR,
262 )
263 .expect("valid inputs");
264 assert!(
265 v >= prev,
266 "variance decreased with distance: variance({d:.3}) = {v:.12} < {prev:.12}"
267 );
268 prev = v;
269 }
270 // And the saturation: the far end of the sweep reaches the full
271 // prior ignorance (never-covered convention).
272 assert!(
273 (prev - total_ignorance(&lams)).abs() <= 1e-12,
274 "far-field variance must saturate at Σ 1/λ̂: got {prev}"
275 );
276 }
277
278 /// (a′) Pointwise domination, no geometric family assumed: a support row
279 /// that is pointwise smaller never yields smaller variance — exercised
280 /// on a NON-monotone-in-ℓ row pair as well.
281 #[test]
282 pub(crate) fn extrapolation_variance_is_monotone_under_pointwise_domination() {
283 let eps = band();
284 let lams = lambdas();
285 let q_bar = support_means(&eps);
286 let rows = [
287 arr1(&[0.9, 0.95, 0.99, 1.0, 1.0]),
288 arr1(&[0.02, 0.3, 0.06, 0.8, 0.97]),
289 arr1(&[0.0, 0.0, 0.04, 0.2, 0.6]),
290 arr1(&[0.04, 0.04, 0.04, 0.04, 0.049]),
291 ];
292 for row in &rows {
293 for shrink in [1.0, 0.9, 0.7, 0.3, 0.0] {
294 let smaller = row.mapv(|q| shrink * q);
295 let v_big = measure_jet_extrapolation_variance(
296 row.view(),
297 &eps,
298 &q_bar,
299 MeasureJetExtrapolationSpectrum::PerLevel(&lams),
300 FLOOR,
301 )
302 .expect("valid inputs");
303 let v_small = measure_jet_extrapolation_variance(
304 smaller.view(),
305 &eps,
306 &q_bar,
307 MeasureJetExtrapolationSpectrum::PerLevel(&lams),
308 FLOOR,
309 )
310 .expect("valid inputs");
311 assert!(
312 v_small >= v_big,
313 "pointwise-smaller support gave smaller variance: {v_small} < {v_big} \
314 (row {row:?}, shrink {shrink})"
315 );
316 }
317 }
318 }
319
320 /// (b) On-web limit: full kernel mass at every scale prices ZERO extra
321 /// variance; near-full mass prices at most the uncovered fraction of the
322 /// total prior ignorance.
323 #[test]
324 pub(crate) fn extrapolation_variance_vanishes_on_web() {
325 let eps = band();
326 let lams = lambdas();
327 let q_bar = support_means(&eps);
328 let full = Array1::from_elem(eps.len(), TOTAL);
329 let v_full = measure_jet_extrapolation_variance(
330 full.view(),
331 &eps,
332 &q_bar,
333 MeasureJetExtrapolationSpectrum::PerLevel(&lams),
334 FLOOR,
335 )
336 .expect("valid inputs");
337 assert_eq!(v_full, 0.0, "full coverage must price zero extra variance");
338
339 let near = Array1::from_elem(eps.len(), 0.97 * TOTAL);
340 let v_near = measure_jet_extrapolation_variance(
341 near.view(),
342 &eps,
343 &q_bar,
344 MeasureJetExtrapolationSpectrum::PerLevel(&lams),
345 FLOOR,
346 )
347 .expect("valid inputs");
348 let budget = total_ignorance(&lams);
349 assert!(
350 v_near <= 0.05 * budget,
351 "near-full coverage must price a small fraction of Σ 1/λ̂: {v_near} vs budget {budget}"
352 );
353 }
354
355 /// (c) Off-web limit: zero support everywhere (never covered) collects
356 /// the spectrum's total prior ignorance Σ 1/λ̂ EXACTLY.
357 #[test]
358 pub(crate) fn extrapolation_variance_saturates_off_web() {
359 let eps = band();
360 let lams = lambdas();
361 let q_bar = support_means(&eps);
362 let zero = Array1::<f64>::zeros(eps.len());
363 let v = measure_jet_extrapolation_variance(
364 zero.view(),
365 &eps,
366 &q_bar,
367 MeasureJetExtrapolationSpectrum::PerLevel(&lams),
368 FLOOR,
369 )
370 .expect("valid inputs");
371 assert_eq!(
372 v,
373 total_ignorance(&lams),
374 "never-covered query must pay Σ 1/λ̂ exactly"
375 );
376 }
377
378 /// (d) Spectrum scaling: doubling every fitted amplitude halves the
379 /// variance — the λ̂⁻¹ pricing is exact, in every coverage regime.
380 #[test]
381 pub(crate) fn extrapolation_variance_halves_when_amplitudes_double() {
382 let eps = band();
383 let lams = lambdas();
384 let q_bar = support_means(&eps);
385 let doubled: Vec<f64> = lams.iter().map(|l| 2.0 * l).collect();
386 // Mixed regime: some levels below the floor, some covered partially,
387 // some fully — both weight branches exercised.
388 let rows = [
389 support_at_distance(0.35, &eps),
390 Array1::<f64>::zeros(eps.len()),
391 Array1::from_elem(eps.len(), 0.5),
392 ];
393 for row in &rows {
394 let v1 = measure_jet_extrapolation_variance(
395 row.view(),
396 &eps,
397 &q_bar,
398 MeasureJetExtrapolationSpectrum::PerLevel(&lams),
399 FLOOR,
400 )
401 .expect("valid inputs");
402 let v2 = measure_jet_extrapolation_variance(
403 row.view(),
404 &eps,
405 &q_bar,
406 MeasureJetExtrapolationSpectrum::PerLevel(&doubled),
407 FLOOR,
408 )
409 .expect("valid inputs");
410 assert!(
411 (2.0 * v2 - v1).abs() <= 1e-15 * v1.max(1.0),
412 "doubling λ̂ must halve the variance: {v1} vs 2×{v2}"
413 );
414 }
415 }
416
417 /// Convention pin: the ε★ gate. Sub-floor mass at every level is
418 /// never-covered (full Σ 1/λ̂, no credit for stray mass); the moment ONE
419 /// level clears the floor, that level and every coarser one switch to
420 /// the smooth uncovered-fraction weight while finer levels stay fully
421 /// charged.
422 #[test]
423 pub(crate) fn extrapolation_variance_gate_convention() {
424 let eps = band();
425 let lams = lambdas();
426 let q_bar = support_means(&eps);
427 let sub_floor = Array1::from_elem(eps.len(), 0.049 * TOTAL);
428 let v_sub = measure_jet_extrapolation_variance(
429 sub_floor.view(),
430 &eps,
431 &q_bar,
432 MeasureJetExtrapolationSpectrum::PerLevel(&lams),
433 FLOOR,
434 )
435 .expect("valid inputs");
436 assert_eq!(
437 v_sub,
438 total_ignorance(&lams),
439 "sub-floor mass earns no credit: full Σ 1/λ̂"
440 );
441
442 // Coverage exactly at the floor on the coarsest level only.
443 let mut at_floor = sub_floor.clone();
444 at_floor[eps.len() - 1] = FLOOR * TOTAL;
445 let v_floor = measure_jet_extrapolation_variance(
446 at_floor.view(),
447 &eps,
448 &q_bar,
449 MeasureJetExtrapolationSpectrum::PerLevel(&lams),
450 FLOOR,
451 )
452 .expect("valid inputs");
453 let expected: f64 = lams[..eps.len() - 1].iter().map(|l| 1.0 / l).sum::<f64>()
454 + (1.0 - FLOOR) / lams[eps.len() - 1];
455 assert!(
456 (v_floor - expected).abs() <= 1e-15,
457 "floor-clearing coarsest level must take weight 1 − a: {v_floor} vs {expected}"
458 );
459 // The gate's discontinuity is bounded by the documented
460 // coverage_floor · Σ 1/λ̂ budget.
461 assert!(
462 v_sub - v_floor <= FLOOR * total_ignorance(&lams) + 1e-15,
463 "gate jump exceeds the documented coverage_floor bound"
464 );
465 }
466
467 #[test]
468 pub(crate) fn fused_extrapolation_charges_single_band_amplitude_once() {
469 let eps = band();
470 let q_bar = support_means(&eps);
471 let lam = 2.5;
472 let zero = Array1::<f64>::zeros(eps.len());
473 let v_zero = measure_jet_extrapolation_variance(
474 zero.view(),
475 &eps,
476 &q_bar,
477 MeasureJetExtrapolationSpectrum::Fused(lam),
478 FLOOR,
479 )
480 .expect("valid inputs");
481 assert_eq!(
482 v_zero,
483 1.0 / lam,
484 "never-covered fused band must pay one amplitude, not one per level"
485 );
486
487 let covered = arr1(&[0.01, 0.2, 0.4, 0.75, 0.5]);
488 let v_covered = measure_jet_extrapolation_variance(
489 covered.view(),
490 &eps,
491 &q_bar,
492 MeasureJetExtrapolationSpectrum::Fused(lam),
493 FLOOR,
494 )
495 .expect("valid inputs");
496 let expected = (1.0 - 0.75) / lam;
497 assert!(
498 (v_covered - expected).abs() <= 1e-15,
499 "fused band must use the best covered level once: {v_covered} vs {expected}"
500 );
501 }
502}