use crate::foundation::Lattice;
use ndarray::Array2;
use super::idw::grid_idw;
pub(crate) fn grid_min_curvature(
coords: &[[f64; 3]],
lattice: &Lattice,
seed: Option<&Array2<f64>>,
) -> Array2<f64> {
let (nc, nr) = (lattice.ncol, lattice.nrow);
let mut z = match seed {
Some(s) if s.dim() == (nc, nr) => s.clone(),
_ => grid_idw(coords, lattice),
};
let mut fixed = Array2::from_elem((nc, nr), false);
let mut acc: std::collections::HashMap<(usize, usize), (f64, usize)> =
std::collections::HashMap::new();
for c in coords {
if let Some((fi, fj)) = lattice.xy_to_ij(c[0], c[1]) {
let i = fi.round();
let j = fj.round();
if i < 0.0 || j < 0.0 {
continue;
}
let (i, j) = (i as usize, j as usize);
if i < nc && j < nr {
let e = acc.entry((i, j)).or_insert((0.0, 0));
e.0 += c[2];
e.1 += 1;
}
}
}
for ((i, j), (sum, n)) in acc {
z[[i, j]] = sum / n as f64;
fixed[[i, j]] = true;
}
if nc < 2 || nr < 2 {
return z;
}
const MAX_ITERS: usize = 5000;
const TOL: f64 = 1e-6;
const OMEGA: f64 = 1.5;
let range = {
let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
for c in coords {
lo = lo.min(c[2]);
hi = hi.max(c[2]);
}
(hi - lo).abs().max(1.0)
};
for _ in 0..MAX_ITERS {
let mut max_delta = 0.0_f64;
for j in 0..nr {
for i in 0..nc {
if fixed[[i, j]] {
continue;
}
let target = if i >= 2 && i + 2 < nc && j >= 2 && j + 2 < nr {
biharmonic_target(&z, i, j)
} else {
harmonic_target(&z, i, j, nc, nr)
};
let old = z[[i, j]];
let new = old + OMEGA * (target - old);
z[[i, j]] = new;
max_delta = max_delta.max((new - old).abs());
}
}
if max_delta < TOL * range {
break;
}
}
z
}
fn biharmonic_target(z: &Array2<f64>, i: usize, j: usize) -> f64 {
let n = z[[i, j + 1]];
let s = z[[i, j - 1]];
let e = z[[i + 1, j]];
let w = z[[i - 1, j]];
let ne = z[[i + 1, j + 1]];
let nw = z[[i - 1, j + 1]];
let se = z[[i + 1, j - 1]];
let sw = z[[i - 1, j - 1]];
let nn = z[[i, j + 2]];
let ss = z[[i, j - 2]];
let ee = z[[i + 2, j]];
let ww = z[[i - 2, j]];
(8.0 * (n + s + e + w) - 2.0 * (ne + nw + se + sw) - (nn + ss + ee + ww)) / 20.0
}
fn harmonic_target(z: &Array2<f64>, i: usize, j: usize, nc: usize, nr: usize) -> f64 {
let mut sum = 0.0;
let mut n = 0.0;
if i > 0 {
sum += z[[i - 1, j]];
n += 1.0;
}
if i + 1 < nc {
sum += z[[i + 1, j]];
n += 1.0;
}
if j > 0 {
sum += z[[i, j - 1]];
n += 1.0;
}
if j + 1 < nr {
sum += z[[i, j + 1]];
n += 1.0;
}
if n > 0.0 {
sum / n
} else {
z[[i, j]]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn linear_trend_reproduced_exactly() {
let lattice = Lattice::regular(0.0, 0.0, 1.0, 1.0, 7, 7);
let (a, b, c) = (2.0, -3.0, 5.0);
let mut coords = Vec::new();
for j in 0..7 {
for i in 0..7 {
let (x, y) = lattice.node_xy(i, j);
coords.push([x, y, a * x + b * y + c]);
}
}
let out = grid_min_curvature(&coords, &lattice, None);
for j in 0..7 {
for i in 0..7 {
let (x, y) = lattice.node_xy(i, j);
let expected = a * x + b * y + c;
assert!(
(out[[i, j]] - expected).abs() < 1e-6,
"node ({i},{j}): got {}, want {expected}",
out[[i, j]]
);
}
}
}
#[test]
fn sparse_linear_samples_recover_plane() {
let lattice = Lattice::regular(0.0, 0.0, 1.0, 1.0, 9, 9);
let (a, b, c) = (1.0, 0.5, -2.0);
let mut coords = Vec::new();
for j in 0..9 {
for i in 0..9 {
if i == 0 || i == 8 || j == 0 || j == 8 {
let (x, y) = lattice.node_xy(i, j);
coords.push([x, y, a * x + b * y + c]);
}
}
}
let out = grid_min_curvature(&coords, &lattice, None);
let (x, y) = lattice.node_xy(4, 4);
let expected = a * x + b * y + c;
assert!(
(out[[4, 4]] - expected).abs() < 1e-3,
"interior node: got {}, want {expected}",
out[[4, 4]]
);
}
#[test]
fn anchors_are_honoured() {
let lattice = Lattice::regular(0.0, 0.0, 1.0, 1.0, 5, 5);
let coords = [[0.0, 0.0, 0.0], [4.0, 4.0, 100.0], [2.0, 2.0, 50.0]];
let out = grid_min_curvature(&coords, &lattice, None);
assert!((out[[0, 0]] - 0.0).abs() < 1e-9);
assert!((out[[4, 4]] - 100.0).abs() < 1e-9);
assert!((out[[2, 2]] - 50.0).abs() < 1e-9);
}
#[test]
fn degenerate_single_row_does_not_panic() {
let lattice = Lattice::regular(0.0, 0.0, 1.0, 1.0, 5, 1);
let coords = [[0.0, 0.0, 10.0], [4.0, 0.0, 20.0]];
let out = grid_min_curvature(&coords, &lattice, None);
assert_eq!(out.dim(), (5, 1));
assert!(out.iter().all(|v| v.is_finite()));
}
#[test]
fn warm_start_matches_cold_to_tolerance() {
let lattice = Lattice::regular(0.0, 0.0, 1.0, 1.0, 12, 10);
let coords = [
[1.0, 1.0, 10.0],
[9.0, 2.0, 25.0],
[3.0, 8.0, 5.0],
[10.0, 8.0, 40.0],
[5.0, 5.0, 18.0],
];
let cold = grid_min_curvature(&coords, &lattice, None);
let warm = grid_min_curvature(&coords, &lattice, Some(&cold));
for (w, c) in warm.iter().zip(cold.iter()) {
assert!((w - c).abs() < 1e-3, "warm {w} vs cold {c}");
}
}
#[test]
fn wrong_shape_seed_falls_back_to_cold() {
let lattice = Lattice::regular(0.0, 0.0, 1.0, 1.0, 6, 6);
let coords = [[0.0, 0.0, 0.0], [5.0, 5.0, 50.0], [2.0, 3.0, 20.0]];
let cold = grid_min_curvature(&coords, &lattice, None);
let bogus = Array2::from_elem((3, 3), 999.0); let out = grid_min_curvature(&coords, &lattice, Some(&bogus));
for (o, c) in out.iter().zip(cold.iter()) {
assert_eq!(o, c);
}
}
}