#![cfg(test)]
use ndarray::Array2;
use super::*;
fn no_ident_fixture() -> (Array2<f64>, DuchonBasisSpec) {
let n = 80usize;
let mut data = Array2::<f64>::zeros((n, 1));
for i in 0..n {
data[[i, 0]] = i as f64 / (n as f64 - 1.0);
}
let spec = DuchonBasisSpec {
radial_reparam: None,
periodic: None,
center_strategy: CenterStrategy::FarthestPoint { num_centers: 8 },
length_scale: Some(1.0),
power: 1.0,
nullspace_order: DuchonNullspaceOrder::Linear,
identifiability: SpatialIdentifiability::None,
aniso_log_scales: None,
operator_penalties: DuchonOperatorPenaltySpec::default(),
boundary: OneDimensionalBoundary::Open,
};
(data, spec)
}
fn constrained_fixture() -> (Array2<f64>, DuchonBasisSpec) {
let (data, mut spec) = no_ident_fixture();
spec.identifiability = SpatialIdentifiability::default();
(data, spec)
}
fn aniso_fixture() -> (Array2<f64>, DuchonBasisSpec) {
let n = 60usize;
let mut data = Array2::<f64>::zeros((n, 2));
for i in 0..n {
let t = i as f64 / (n as f64 - 1.0);
data[[i, 0]] = t;
data[[i, 1]] = 8.0 * (t * 7.0).sin();
}
let spec = DuchonBasisSpec {
radial_reparam: None,
periodic: None,
center_strategy: CenterStrategy::FarthestPoint { num_centers: 10 },
length_scale: Some(1.0),
power: 2.0,
nullspace_order: DuchonNullspaceOrder::Linear,
identifiability: SpatialIdentifiability::None,
aniso_log_scales: Some(vec![0.0, 0.0]),
operator_penalties: DuchonOperatorPenaltySpec::default(),
boundary: OneDimensionalBoundary::Open,
};
(data, spec)
}
fn fro(m: &Array2<f64>) -> f64 {
m.iter().map(|v| v * v).sum::<f64>().sqrt()
}
fn duchon_metadata_chart(
result: &BasisBuildResult,
) -> (Array2<f64>, Option<Array2<f64>>, Option<Array2<f64>>) {
match &result.metadata {
BasisMetadata::Duchon {
centers,
identifiability_transform,
radial_reparam,
..
} => (
centers.clone(),
radial_reparam.clone(),
identifiability_transform.clone(),
),
other => panic!(
"expected Duchon metadata, got {:?}",
std::mem::discriminant(other)
),
}
}
#[test]
fn duchon_resolve_chart_reproduces_the_cold_build() {
for (label, (data, spec)) in [
("no_ident", no_ident_fixture()),
("constrained", constrained_fixture()),
("aniso", aniso_fixture()),
] {
let mut workspace = BasisWorkspace::default();
let cold = build_duchon_basiswithworkspace(data.view(), &spec, &mut workspace)
.unwrap_or_else(|e| panic!("[{label}] cold build: {e:?}"));
let (cold_centers, cold_v, cold_t) = duchon_metadata_chart(&cold);
let resolved = duchon_resolve_chart(data.view(), &spec, &mut workspace)
.unwrap_or_else(|e| panic!("[{label}] resolve: {e:?}"));
assert_eq!(
resolved.centers, cold_centers,
"[{label}] resolved centers differ from the cold build's"
);
match (&resolved.spec.radial_reparam, &cold_v) {
(Some(a), Some(b)) => assert_eq!(
a, b,
"[{label}] resolved data-metric reparam V differs from the cold build's"
),
(None, None) => {}
(a, b) => panic!(
"[{label}] reparam adoption disagrees: resolved={:?} cold={:?}",
a.as_ref().map(|m| m.dim()),
b.as_ref().map(|m| m.dim())
),
}
match (&resolved.identifiability_transform, &cold_t) {
(Some(a), Some(b)) => assert_eq!(
a, b,
"[{label}] resolved identifiability transform differs from the cold build's \
— it must be read off the V-rotated design"
),
(None, None) => {}
(a, b) => panic!(
"[{label}] identifiability presence disagrees: resolved={:?} cold={:?}",
a.as_ref().map(|m| m.dim()),
b.as_ref().map(|m| m.dim())
),
}
let replay = build_duchon_basiswithworkspace(data.view(), &resolved.spec, &mut workspace)
.unwrap_or_else(|e| panic!("[{label}] replay build: {e:?}"));
assert_eq!(
replay.active_penalties.len(),
cold.active_penalties.len(),
"[{label}] replaying the resolved spec changed the penalty topology"
);
for (idx, (r, c)) in replay
.active_penalties
.iter()
.zip(cold.active_penalties.iter())
.enumerate()
{
assert_eq!(
r.info.source, c.info.source,
"[{label}] penalty {idx} source changed under replay"
);
assert_eq!(
r.matrix, c.matrix,
"[{label}] penalty {idx} ({:?}) changed under replay of the resolved spec",
c.info.source
);
}
let (replay_centers, replay_v, replay_t) = duchon_metadata_chart(&replay);
assert_eq!(
replay_centers, cold_centers,
"[{label}] replay moved centers"
);
assert_eq!(replay_v, cold_v, "[{label}] replay re-derived a different V");
assert_eq!(replay_t, cold_t, "[{label}] replay re-derived a different T");
}
}
#[test]
fn duchon_cold_spec_psi_jet_matches_fd_at_the_resolved_chart() {
for (label, eps, rel_arm, (data, spec)) in [
("no_ident", 1e-4_f64, 1e-5_f64, no_ident_fixture()),
("constrained", 1e-5_f64, 5e-4_f64, constrained_fixture()),
] {
let mut workspace = BasisWorkspace::default();
let jet = build_duchon_basis_log_kappa_derivatives(data.view(), &spec)
.unwrap_or_else(|e| panic!("[{label}] cold-spec ψ-jet must build, got {e:?}"));
let resolved = duchon_resolve_chart(data.view(), &spec, &mut workspace)
.unwrap_or_else(|e| panic!("[{label}] resolve: {e:?}"));
assert!(
resolved.spec.radial_reparam.is_some(),
"[{label}] fixture must adopt a V, otherwise this gate is vacuous"
);
let kappa = 1.0
/ resolved
.spec
.length_scale
.expect("hybrid Duchon length_scale");
let mut plus_spec = resolved.spec.clone();
let mut minus_spec = resolved.spec.clone();
plus_spec.length_scale = Some(1.0 / (kappa * eps.exp()));
minus_spec.length_scale = Some(1.0 / (kappa * (-eps).exp()));
let plus = build_duchon_basiswithworkspace(data.view(), &plus_spec, &mut workspace)
.unwrap_or_else(|e| panic!("[{label}] plus build: {e:?}"));
let minus = build_duchon_basiswithworkspace(data.view(), &minus_spec, &mut workspace)
.unwrap_or_else(|e| panic!("[{label}] minus build: {e:?}"));
for (side, built) in [("plus", &plus), ("minus", &minus)] {
let (c, v, t) = duchon_metadata_chart(built);
assert_eq!(
c, resolved.centers,
"[{label}/{side}] ε-rebuild moved centers"
);
assert_eq!(
v, resolved.spec.radial_reparam,
"[{label}/{side}] ε-rebuild re-derived V — the FD would differentiate chart motion"
);
assert_eq!(
t, resolved.identifiability_transform,
"[{label}/{side}] ε-rebuild re-derived T"
);
}
assert_eq!(
jet.first.penalties_derivative.len(),
plus.active_penalties.len(),
"[{label}] ψ-jet block count does not match the forward's penalty list"
);
assert!(
!jet.first.penalties_derivative.is_empty(),
"[{label}] an empty derivative list would satisfy the loop below by absence"
);
for (idx, analytic) in jet.first.penalties_derivative.iter().enumerate() {
let fd = (&plus.active_penalties[idx].matrix - &minus.active_penalties[idx].matrix)
/ (2.0 * eps);
let err = fro(&(analytic - &fd));
let a_norm = fro(analytic);
let fd_norm = fro(&fd);
eprintln!(
"[2638:{label}] penalty {idx} ({:?}) analytic={a_norm:.6e} fd={fd_norm:.6e} \
err={err:.6e}",
plus.active_penalties[idx].info.source,
);
let entries = plus.active_penalties[idx].matrix.len() as f64;
let floor = 1e2 * 1e-14 * entries.sqrt() / (2.0 * eps);
let scale = a_norm.max(fd_norm).max(1.0);
assert!(
err.is_finite(),
"[{label}] penalty {idx} residual is not finite"
);
assert!(
err <= floor || err <= rel_arm * scale,
"[{label}] penalty {idx} ({:?}) cold-spec ψ-jet disagrees with the finite \
difference of the forward at the RESOLVED chart: analytic={a_norm:.6e} \
fd={fd_norm:.6e} err={err:.6e} rel={:.6e} floor={floor:.6e} arm={rel_arm:.0e}",
plus.active_penalties[idx].info.source,
err / scale,
);
}
}
}
#[test]
fn zz_measure_2638_fd_step_sweep() {
for (label, (data, spec)) in [
("no_ident", no_ident_fixture()),
("constrained", constrained_fixture()),
] {
let mut workspace = BasisWorkspace::default();
let jet = build_duchon_basis_log_kappa_derivatives(data.view(), &spec).expect("jet");
let resolved = duchon_resolve_chart(data.view(), &spec, &mut workspace).expect("resolve");
let kappa = 1.0 / resolved.spec.length_scale.expect("ls");
for eps in [1e-2_f64, 1e-3, 1e-4, 1e-5, 1e-6, 1e-7] {
let mut plus_spec = resolved.spec.clone();
let mut minus_spec = resolved.spec.clone();
plus_spec.length_scale = Some(1.0 / (kappa * eps.exp()));
minus_spec.length_scale = Some(1.0 / (kappa * (-eps).exp()));
let plus =
build_duchon_basiswithworkspace(data.view(), &plus_spec, &mut workspace)
.expect("plus");
let minus =
build_duchon_basiswithworkspace(data.view(), &minus_spec, &mut workspace)
.expect("minus");
let mut worst = 0.0_f64;
let mut worst_idx = 0usize;
for (idx, analytic) in jet.first.penalties_derivative.iter().enumerate() {
let fd = (&plus.active_penalties[idx].matrix
- &minus.active_penalties[idx].matrix)
/ (2.0 * eps);
let scale = fro(analytic).max(fro(&fd)).max(1.0);
let rel = fro(&(analytic - &fd)) / scale;
if rel > worst {
worst = rel;
worst_idx = idx;
}
}
eprintln!(
"[2638:sweep:{label}] eps={eps:.0e} worst_rel={worst:.4e} \
at penalty {worst_idx} ({:?})",
plus.active_penalties[worst_idx].info.source,
);
}
}
}
#[test]
fn zz_measure_2638_chart_motion() {
let (data, spec) = no_ident_fixture();
let mut workspace = BasisWorkspace::default();
let eps = 1e-5_f64;
let resolved = duchon_resolve_chart(data.view(), &spec, &mut workspace).expect("resolve");
eprintln!(
"[2638b] resolved chart: V={:?} T={:?}",
resolved.spec.radial_reparam.as_ref().map(|v| v.dim()),
resolved.identifiability_transform.as_ref().map(|t| t.dim()),
);
let build = |s: &DuchonBasisSpec, ls: f64| {
let mut local = s.clone();
local.length_scale = Some(ls);
build_duchon_basis(data.view(), &local).expect("build")
};
let cold_p = build(&spec, 1.0 / eps.exp());
let cold_m = build(&spec, 1.0 / (-eps).exp());
let frz_p = build(&resolved.spec, 1.0 / eps.exp());
let frz_m = build(&resolved.spec, 1.0 / (-eps).exp());
let base_cold = build_duchon_basis(data.view(), &spec).expect("cold base");
let base_frz = build_duchon_basis(data.view(), &resolved.spec).expect("frozen base");
let jet = build_duchon_basis_log_kappa_derivatives(data.view(), &spec).expect("jet");
for (idx, analytic) in jet.first.penalties_derivative.iter().enumerate() {
let fd_cold =
(&cold_p.active_penalties[idx].matrix - &cold_m.active_penalties[idx].matrix)
/ (2.0 * eps);
let fd_frz = (&frz_p.active_penalties[idx].matrix
- &frz_m.active_penalties[idx].matrix)
/ (2.0 * eps);
eprintln!(
"[2638b] penalty {idx} ({:?}): |A|={:.4e} |FD_cold|={:.4e} |FD_frz|={:.4e} \
|A-FD_frz|={:.4e} |FD_cold-FD_frz|={:.4e} |S_cold(0)-S_frz(0)|={:.4e}",
base_frz.active_penalties[idx].info.source,
fro(analytic),
fro(&fd_cold),
fro(&fd_frz),
fro(&(analytic - &fd_frz)),
fro(&(&fd_cold - &fd_frz)),
fro(
&(&base_cold.active_penalties[idx].matrix
- &base_frz.active_penalties[idx].matrix)
),
);
}
}