use ndarray::Array2;
use petektools::{grid_min_curvature_conditioned, Conditioning, Lattice};
const N: usize = 21; const SPACING: f64 = 100.0; const EXTENT: f64 = (N - 1) as f64 * SPACING; const MARGIN: f64 = 20.0; const SCATTER_STEP: f64 = 65.0; const REGIONAL: f64 = 2000.0;
const DIP_X: f64 = 0.02; const DIP_Y: f64 = -0.015;
const DOME_AMP: f64 = 60.0;
const DOME_X: f64 = 1100.0;
const DOME_Y: f64 = 900.0;
const DOME_SIGMA: f64 = 300.0;
const ORIGIN_X: f64 = 700_000.0; const ORIGIN_Y: f64 = 7_100_000.0;
fn truth(x: f64, y: f64) -> f64 {
let (dx, dy) = ((x - DOME_X) / DOME_SIGMA, (y - DOME_Y) / DOME_SIGMA);
REGIONAL + DIP_X * x + DIP_Y * y - DOME_AMP * (-(dx * dx + dy * dy)).exp()
}
fn scatter_xy(lo: f64, hi: f64) -> Vec<(f64, f64)> {
let mut pts = Vec::new();
let mut y = lo + 13.0;
while y <= hi {
let mut x = lo + 17.0;
while x <= hi {
pts.push((x, y));
x += SCATTER_STEP;
}
y += SCATTER_STEP;
}
pts
}
fn eval_local(field: &Array2<f64>, lattice: &Lattice, x: f64, y: f64) -> f64 {
let (nc, nr) = (lattice.ncol, lattice.nrow);
let (fi, fj) = lattice
.xy_to_ij(lattice.xori + x, lattice.yori + y)
.unwrap();
let fi = fi.clamp(0.0, (nc - 1) as f64);
let fj = fj.clamp(0.0, (nr - 1) as f64);
let (i0, j0) = (fi.floor() as usize, fj.floor() as usize);
let (i1, j1) = ((i0 + 1).min(nc - 1), (j0 + 1).min(nr - 1));
let (tx, ty) = (fi - i0 as f64, fj - j0 as f64);
field[[i0, j0]] * (1.0 - tx) * (1.0 - ty)
+ field[[i1, j0]] * tx * (1.0 - ty)
+ field[[i0, j1]] * (1.0 - tx) * ty
+ field[[i1, j1]] * tx * ty
}
fn rms(errs: &[f64]) -> f64 {
(errs.iter().map(|e| e * e).sum::<f64>() / errs.len() as f64).sqrt()
}
fn max_abs(errs: &[f64]) -> f64 {
errs.iter().fold(0.0_f64, |a, e| a.max(e.abs()))
}
fn coords_for(lattice: &Lattice, pts: &[(f64, f64)]) -> Vec<[f64; 3]> {
pts.iter()
.map(|&(x, y)| [lattice.xori + x, lattice.yori + y, truth(x, y)])
.collect()
}
fn on_scatter_errs(field: &Array2<f64>, lattice: &Lattice, pts: &[(f64, f64)]) -> Vec<f64> {
pts.iter()
.map(|&(x, y)| eval_local(field, lattice, x, y) - truth(x, y))
.collect()
}
fn nearest_vs_bilinear(label: &str, lattice: &Lattice) -> ((f64, f64), (f64, f64)) {
let pts = scatter_xy(MARGIN, EXTENT - MARGIN);
let coords = coords_for(lattice, &pts);
let nearest =
grid_min_curvature_conditioned(&coords, lattice, None, Conditioning::NearestNode).unwrap();
let bilinear =
grid_min_curvature_conditioned(&coords, lattice, None, Conditioning::Bilinear).unwrap();
let ne = on_scatter_errs(&nearest, lattice, &pts);
let be = on_scatter_errs(&bilinear, lattice, &pts);
let out = ((rms(&ne), max_abs(&ne)), (rms(&be), max_abs(&be)));
eprintln!(
"[{label}] {} off-node pts | NearestNode rms {:.3} m max {:.3} m | Bilinear rms {:.3} m max {:.3} m ({:.0}% rms)",
pts.len(),
(out.0).0,
(out.0).1,
(out.1).0,
(out.1).1,
100.0 * (1.0 - (out.1).0 / (out.0).0),
);
out
}
#[test]
fn snap_defect_reproduced_then_fixed() {
let lattice = Lattice::regular(0.0, 0.0, SPACING, SPACING, N, N);
let ((n_rms, n_max), (b_rms, b_max)) = nearest_vs_bilinear("unit-origin 100 m", &lattice);
assert!(
n_rms > 0.5 && n_max > 2.0,
"the snap fixture must demonstrate the defect: rms {n_rms} max {n_max}"
);
assert!(
b_rms < 0.30 && b_rms < 0.35 * n_rms,
"Bilinear must remove most of the snap error: rms {b_rms} (nearest {n_rms})"
);
assert!(
b_max < 0.35 * n_max,
"Bilinear must cut the worst-point error: max {b_max} (nearest {n_max})"
);
}
#[test]
fn world_scale_variant_holds() {
let lattice = Lattice::regular(ORIGIN_X, ORIGIN_Y, SPACING, SPACING, N, N);
let ((n_rms, _), (b_rms, _)) = nearest_vs_bilinear("world-scale georef", &lattice);
assert!(n_rms > 0.5, "world-scale snap defect present: {n_rms}");
assert!(
b_rms < 0.30 && b_rms < 0.35 * n_rms,
"world-scale Bilinear must remove the snap error too: {b_rms} (nearest {n_rms})"
);
}
#[test]
fn on_node_controls_are_bit_exact_in_both_modes() {
let lattice = Lattice::regular(ORIGIN_X, ORIGIN_Y, SPACING, SPACING, N, N);
let node_ij = [
(0, 0),
(20, 0),
(0, 20),
(20, 20),
(10, 10),
(5, 15),
(15, 5),
];
let coords: Vec<[f64; 3]> = node_ij
.iter()
.map(|&(i, j)| {
let (x, y) = lattice.node_xy(i, j);
[x, y, truth(i as f64 * SPACING, j as f64 * SPACING)]
})
.collect();
let nearest =
grid_min_curvature_conditioned(&coords, &lattice, None, Conditioning::NearestNode).unwrap();
let bilinear =
grid_min_curvature_conditioned(&coords, &lattice, None, Conditioning::Bilinear).unwrap();
for (&(i, j), c) in node_ij.iter().zip(&coords) {
assert!(
(nearest[[i, j]] - c[2]).abs() < 1e-9,
"NearestNode control ({i},{j}) not exact"
);
assert!(
(bilinear[[i, j]] - c[2]).abs() < 1e-9,
"Bilinear control ({i},{j}) not exact"
);
}
assert_eq!(
nearest, bilinear,
"on-node-only inputs must make Bilinear bit-identical to NearestNode"
);
}
#[test]
fn bilinear_is_deterministic() {
let lattice = Lattice::regular(0.0, 0.0, SPACING, SPACING, N, N);
let coords = coords_for(&lattice, &scatter_xy(MARGIN, EXTENT - MARGIN));
let a =
grid_min_curvature_conditioned(&coords, &lattice, None, Conditioning::Bilinear).unwrap();
let b =
grid_min_curvature_conditioned(&coords, &lattice, None, Conditioning::Bilinear).unwrap();
assert_eq!(a, b, "Bilinear must be bit-deterministic");
}
#[test]
fn bilinear_warm_equals_cold_when_well_determined() {
let lattice = Lattice::regular(0.0, 0.0, SPACING, SPACING, N, N);
let coords = coords_for(&lattice, &scatter_xy(0.0, EXTENT));
let cold =
grid_min_curvature_conditioned(&coords, &lattice, None, Conditioning::Bilinear).unwrap();
let warm =
grid_min_curvature_conditioned(&coords, &lattice, Some(&cold), Conditioning::Bilinear)
.unwrap();
let mut moved = 0.0_f64;
for (w, c) in warm.iter().zip(cold.iter()) {
moved = moved.max((w - c).abs());
}
eprintln!("well-determined Bilinear warm-restart max move: {moved:.2e} m");
assert!(
moved < 1e-3,
"warm must reproduce cold to tolerance, moved {moved}"
);
}