1use ndarray::{Array1, ArrayView1, ArrayView2};
76
77use super::BasisError;
78
79pub 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 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 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 PerLevel(&'a [f64]),
194 Fused(f64),
197}
198
199pub 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 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 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 #[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 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 assert!(
384 (prev - total_ignorance(&lams)).abs() <= 1e-12,
385 "far-field variance must saturate at Σ 1/λ̂: got {prev}"
386 );
387 }
388
389 #[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 #[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 #[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 #[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 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 #[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 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 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 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 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 #[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 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 #[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}