use super::sphere_half_angle::{
HalfAngleSeparation, SphereTrig, ambient_half_angle_separation, half_angle_partials,
half_angle_separation_scalar,
};
use super::sphere_kernels::{wahba_sphere_kernel_derivative_dhav_kind, wahba_sphere_kernel_kind};
use super::sphere_spec::SphereWahbaKernel;
use ndarray::{Array2, array};
const DEG: f64 = std::f64::consts::PI / 180.0;
fn legacy_u(a: SphereTrig<f64>, b: SphereTrig<f64>) -> f64 {
let dlon_cos = a.cos_lon * b.cos_lon + a.sin_lon * b.sin_lon;
let cos_gamma = a.sin_lat * b.sin_lat + a.cos_lat * b.cos_lat * dlon_cos;
(1.0 - cos_gamma.clamp(-1.0, 1.0)) * 0.5
}
fn reference_u_lat_offset(offset_deg: f64) -> f64 {
let half = 0.5 * offset_deg * DEG;
half.sin().powi(2)
}
#[test]
fn zz_measure_2489_haversine_u_survives_where_the_dot_product_collapses() {
let base_lat = 37.7749_f64;
let lon = -122.4194_f64;
let offsets = [1e-2, 1e-3, 1e-4, 1e-5, 1e-6, 1e-7, 1e-8, 1e-9, 1e-10];
println!(
"\n{:>10} {:>24} {:>24} {:>11} {:>24} {:>11} {:>11}",
"offset(°)", "reference u", "chord u", "rel", "dot-product u", "rel", "bound 8ε/γ"
);
for offset in offsets {
let a = SphereTrig::from_radians(base_lat * DEG, lon * DEG);
let b = SphereTrig::from_radians((base_lat + offset) * DEG, lon * DEG);
let want = reference_u_lat_offset(offset);
let chord = half_angle_separation_scalar(a, b).u;
let dot = legacy_u(a, b);
let rel_chord = (chord - want).abs() / want;
let rel_dot = (dot - want).abs() / want;
let bound = 8.0 * f64::EPSILON / (offset * DEG);
println!(
"{offset:>10.0e} {want:>24.16e} {chord:>24.16e} {rel_chord:>11.2e} \
{dot:>24.16e} {rel_dot:>11.2e} {bound:>11.2e}"
);
assert!(
rel_chord <= bound,
"chord-form u is {rel_chord:.3e} off at {offset:.0e}°, outside the \
O(ε/γ) bound {bound:.3e} its conditioning allows"
);
assert!(
rel_dot > bound,
"the dot-product route came in at {rel_dot:.3e} at {offset:.0e}°, \
inside the {bound:.3e} bound it is not supposed to reach — either \
the legacy reconstruction here no longer matches what shipped, or \
the premise of #2489 has changed"
);
}
let a = SphereTrig::from_radians(base_lat * DEG, lon * DEG);
let b = SphereTrig::from_radians((base_lat + 1e-10) * DEG, lon * DEG);
assert_eq!(
legacy_u(a, b),
0.0,
"the dot-product route is supposed to report two points 1e-10° apart as \
exactly coincident — if it no longer does, this measurement has drifted"
);
assert!(
half_angle_separation_scalar(a, b).u > 0.0,
"the chord form must separate them"
);
}
#[test]
fn zz_measure_2489_coincidence_is_exact_and_the_pair_sums_to_one() {
let coords = [
(37.7749_f64, -122.4194_f64),
(0.0, 0.0),
(90.0, 0.0),
(-90.0, 180.0),
(-33.8688, 151.2093),
(1e-9, 1e-9),
(89.999_999, -0.000_001),
];
for (lat, lon) in coords {
let t = SphereTrig::from_radians(lat * DEG, lon * DEG);
let sep = half_angle_separation_scalar(t, t);
assert_eq!(
sep.u, 0.0,
"self-separation at ({lat}, {lon}) must be exactly 0, not {:.3e}",
sep.u
);
let closure = (sep.u + sep.v - 1.0).abs();
assert!(
closure < 4.0 * f64::EPSILON,
"u + v = 1 violated by {closure:.3e} at ({lat}, {lon})"
);
}
for (lat, lon) in [(0.0_f64, 0.0_f64), (37.5, 12.25), (-64.0, -90.0)] {
let t = SphereTrig::from_radians(lat * DEG, lon * DEG);
let anti = SphereTrig::from_radians(-lat * DEG, (lon + 180.0) * DEG);
let sep = half_angle_separation_scalar(t, anti);
assert!(
sep.v < 1e-30,
"antipodal v at ({lat}, {lon}) is {:.3e}, not ~0",
sep.v
);
}
}
#[test]
fn zz_measure_2489_gram_diagonal_is_one_number_across_centers() {
let mut centers = Vec::<(f64, f64)>::new();
for i in 0..24 {
let t = i as f64;
centers.push((
-85.0 + 170.0 * (t * 0.041_666_7).fract(),
-180.0 + 360.0 * (t * 0.137).fract(),
));
}
let mut legacy_diag = std::collections::BTreeSet::<u64>::new();
let mut shipped_diag = std::collections::BTreeSet::<u64>::new();
for &(lat, lon) in ¢ers {
let t = SphereTrig::from_radians(lat * DEG, lon * DEG);
let legacy_sep = HalfAngleSeparation {
u: legacy_u(t, t),
v: 1.0 - legacy_u(t, t),
};
let legacy = wahba_sphere_kernel_kind(legacy_sep, 2, SphereWahbaKernel::Pseudo)
.expect("pseudo m=2 is finite at coincidence");
let shipped = wahba_sphere_kernel_kind(
half_angle_separation_scalar(t, t),
2,
SphereWahbaKernel::Pseudo,
)
.expect("pseudo m=2 is finite at coincidence");
legacy_diag.insert(legacy.to_bits());
shipped_diag.insert(shipped.to_bits());
}
println!(
"\n distinct Gram diagonal values over {} centers: legacy {}, shipped {}\n",
centers.len(),
legacy_diag.len(),
shipped_diag.len()
);
assert_eq!(
shipped_diag.len(),
1,
"a zonal kernel's Gram diagonal must be ONE number; got {} distinct \
values across {} centers",
shipped_diag.len(),
centers.len()
);
}
fn legacy_jet_dphi(
a: SphereTrig<f64>,
b: SphereTrig<f64>,
m: usize,
kind: SphereWahbaKernel,
) -> f64 {
let dlon_cos = a.cos_lon * b.cos_lon + a.sin_lon * b.sin_lon;
let cos_gamma = a.sin_lat * b.sin_lat + a.cos_lat * b.cos_lat * dlon_cos;
let sep = HalfAngleSeparation {
u: (1.0 - cos_gamma.clamp(-1.0, 1.0)) * 0.5,
v: (1.0 + cos_gamma.clamp(-1.0, 1.0)) * 0.5,
};
let dk_dcos = -0.5 * wahba_sphere_kernel_derivative_dhav_kind(sep, m, kind);
let dcos_dphi = a.cos_lat * b.sin_lat - a.sin_lat * b.cos_lat * dlon_cos;
dk_dcos * dcos_dphi * DEG
}
fn shipped_jet_dphi(
a: SphereTrig<f64>,
b: SphereTrig<f64>,
m: usize,
kind: SphereWahbaKernel,
) -> f64 {
let sep = half_angle_separation_scalar(a, b);
if sep.u <= 0.0 {
return 0.0;
}
let (du_dphi, _) = half_angle_partials(a, b);
wahba_sphere_kernel_derivative_dhav_kind(sep, m, kind) * du_dphi * DEG
}
#[test]
fn zz_measure_2489_pseudo_cusp_gradient_is_recovered_at_every_offset() {
let want = -DEG / (2.0 * std::f64::consts::PI);
let base_lat = 12.5_f64;
let lon = 44.25_f64;
let offsets = [1e-2, 1e-4, 1e-6, 1e-8, 1e-10];
println!(
"\n true one-sided cusp gradient: {want:.10e} per degree\n\n{:>10} {:>22} {:>10} {:>22} {:>10}",
"offset(°)", "shipped ∂K/∂φ", "err", "legacy ∂K/∂φ", "err"
);
for offset in offsets {
let row = SphereTrig::from_radians((base_lat + offset) * DEG, lon * DEG);
let center = SphereTrig::from_radians(base_lat * DEG, lon * DEG);
let shipped = shipped_jet_dphi(row, center, 1, SphereWahbaKernel::Pseudo);
let legacy = legacy_jet_dphi(row, center, 1, SphereWahbaKernel::Pseudo);
let rel_shipped = (shipped - want).abs() / want.abs();
let rel_legacy = (legacy - want).abs() / want.abs();
println!(
"{offset:>10.0e} {shipped:>22.12e} {rel_shipped:>10.2e} \
{legacy:>22.12e} {rel_legacy:>10.2e}"
);
assert!(
rel_shipped < 1e-2,
"pseudo m=1 cusp gradient is {rel_shipped:.3e} off at {offset:.0e}° \
(got {shipped:.9e}, want {want:.9e}); the pre-registered bar is 1%"
);
}
let t = SphereTrig::from_radians(base_lat * DEG, lon * DEG);
assert_eq!(
shipped_jet_dphi(t, t, 1, SphereWahbaKernel::Pseudo),
0.0,
"at a center the jet must be the symmetric subgradient 0"
);
let (du_dphi, du_dpsi) = half_angle_partials(t, t);
assert_eq!(
(du_dphi, du_dpsi),
(0.0, 0.0),
"∂u/∂φ and ∂u/∂ψ at a center must be exactly zero, so the cusp is \
resolved by the caller rather than reached as ∞·0"
);
}
#[test]
fn zz_measure_2489_smooth_jet_matches_a_finite_difference() {
let lon = -3.75_f64;
for &(kind, m) in &[
(SphereWahbaKernel::Pseudo, 2usize),
(SphereWahbaKernel::Pseudo, 3),
(SphereWahbaKernel::Sobolev, 2),
(SphereWahbaKernel::Sobolev, 3),
(SphereWahbaKernel::SobolevTruncated { lmax: 64 }, 2),
] {
for &(row_lat, center_lat) in &[(20.0_f64, 35.0_f64), (-5.0, 5.0), (60.0, 61.5)] {
let center = SphereTrig::from_radians(center_lat * DEG, lon * DEG);
let h = 1e-5_f64;
let k_at = |lat: f64| -> f64 {
let row = SphereTrig::from_radians(lat * DEG, lon * DEG);
wahba_sphere_kernel_kind(half_angle_separation_scalar(row, center), m, kind)
.expect("finite away from coincidence")
};
let fd = (k_at(row_lat + h) - k_at(row_lat - h)) / (2.0 * h);
let row = SphereTrig::from_radians(row_lat * DEG, lon * DEG);
let analytic = shipped_jet_dphi(row, center, m, kind);
let rel = (analytic - fd).abs() / fd.abs().max(1e-300);
assert!(
rel < 1e-6,
"{kind:?} m={m}: analytic ∂K/∂φ {analytic:.9e} vs central \
difference {fd:.9e} (rel {rel:.3e}) at row {row_lat}, center \
{center_lat}"
);
}
}
}
#[test]
fn zz_measure_2489_ambient_separation_is_exact_at_coincidence() {
let points: Array2<f64> = array![
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[
0.577_350_269_189_625_7,
0.577_350_269_189_625_7,
0.577_350_269_189_625_7
],
];
for i in 0..points.nrows() {
let sep = ambient_half_angle_separation(points.row(i), points.row(i));
assert_eq!(sep.u, 0.0, "ambient self-separation must be exactly 0");
assert!((sep.u + sep.v - 1.0).abs() < 4.0 * f64::EPSILON);
}
let sep = ambient_half_angle_separation(points.row(0), points.row(1));
assert!((sep.u - 0.5).abs() < 4.0 * f64::EPSILON);
assert!((sep.v - 0.5).abs() < 4.0 * f64::EPSILON);
let a = array![1.0, 0.0, 0.0];
let b = array![1.0, 1e-12, 0.0];
let sep = ambient_half_angle_separation(a.view(), b.view());
let want = 1e-24 / 4.0;
let rel = (sep.u - want).abs() / want;
let legacy = (1.0 - (a[0] * b[0] + a[1] * b[1] + a[2] * b[2])) * 0.5;
println!(
"\n ambient 1e-12 perturbation: chord u = {:.6e} (rel {rel:.2e}), \
dot-product u = {legacy:.6e}\n",
sep.u
);
assert!(rel < 1e-6, "ambient chord u is {rel:.3e} off");
assert_eq!(
legacy, 0.0,
"the dot-product route is supposed to lose this pair entirely"
);
}