use crate::foundation::{AlgoError, Lattice, Result};
use ndarray::Array2;
const SNAP_EPS: f64 = 1e-9;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResampleMethod {
Bilinear,
Nearest,
}
pub fn resample(
src_grid: &Array2<f64>,
src_georef: &Lattice,
target: &Lattice,
method: ResampleMethod,
) -> Result<Array2<f64>> {
let (ncol, nrow) = (src_georef.ncol, src_georef.nrow);
if src_grid.dim() != (ncol, nrow) {
return Err(AlgoError::InvalidArgument(format!(
"resample: src_grid shape {:?} does not match src_georef ({ncol}, {nrow})",
src_grid.dim()
)));
}
if ncol == 0 || nrow == 0 || src_georef.xinc == 0.0 || src_georef.yinc == 0.0 {
return Err(AlgoError::InvalidGeometry(
"resample: degenerate source geometry (empty grid or zero node spacing)",
));
}
let mut out = Array2::from_elem((target.ncol, target.nrow), f64::NAN);
let imax = (ncol - 1) as f64;
let jmax = (nrow - 1) as f64;
for tj in 0..target.nrow {
for ti in 0..target.ncol {
let (x, y) = target.node_xy(ti, tj);
let Some((fi, fj)) = src_georef.xy_to_ij(x, y) else {
continue; };
let fi = snap(fi);
let fj = snap(fj);
if fi < 0.0 || fi > imax || fj < 0.0 || fj > jmax {
continue;
}
out[[ti, tj]] = match method {
ResampleMethod::Nearest => sample_nearest(src_grid, fi, fj),
ResampleMethod::Bilinear => sample_bilinear(src_grid, fi, fj, ncol, nrow),
};
}
}
Ok(out)
}
fn snap(f: f64) -> f64 {
if (f - f.round()).abs() < SNAP_EPS {
f.round()
} else {
f
}
}
fn sample_nearest(src: &Array2<f64>, fi: f64, fj: f64) -> f64 {
src[[fi.round() as usize, fj.round() as usize]]
}
fn sample_bilinear(src: &Array2<f64>, fi: f64, fj: f64, ncol: usize, nrow: usize) -> f64 {
if src[[fi.round() as usize, fj.round() as usize]].is_nan() {
return f64::NAN;
}
let i0 = fi.floor() as usize;
let j0 = fj.floor() as usize;
let i1 = (i0 + 1).min(ncol - 1);
let j1 = (j0 + 1).min(nrow - 1);
let ri = fi - i0 as f64; let rj = fj - j0 as f64;
let corners = [
(src[[i0, j0]], (1.0 - ri) * (1.0 - rj)),
(src[[i1, j0]], ri * (1.0 - rj)),
(src[[i0, j1]], (1.0 - ri) * rj),
(src[[i1, j1]], ri * rj),
];
let mut acc = 0.0;
let mut wsum = 0.0;
for (v, w) in corners {
if w > 0.0 && v.is_finite() {
acc += v * w;
wsum += w;
}
}
if wsum > 0.0 {
acc / wsum
} else {
f64::NAN
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
use ndarray::arr2;
fn plane(x: f64, y: f64) -> f64 {
3.0 + 0.5 * x - 0.25 * y
}
fn sample_lattice(lat: &Lattice, f: impl Fn(f64, f64) -> f64) -> Array2<f64> {
let mut a = Array2::zeros((lat.ncol, lat.nrow));
for j in 0..lat.nrow {
for i in 0..lat.ncol {
let (x, y) = lat.node_xy(i, j);
a[[i, j]] = f(x, y);
}
}
a
}
#[test]
fn identity_resample_is_bit_equal() {
let lat = Lattice::regular(1000.0, 2000.0, 25.0, 50.0, 6, 5);
let mut src = sample_lattice(&lat, plane);
src[[2, 3]] = f64::NAN; for method in [ResampleMethod::Bilinear, ResampleMethod::Nearest] {
let out = resample(&src, &lat, &lat, method).unwrap();
assert_eq!(out.dim(), src.dim());
for (o, s) in out.iter().zip(src.iter()) {
if s.is_nan() {
assert!(o.is_nan(), "hole must stay NaN under {method:?}");
} else {
assert_eq!(o, s, "identity must be bit-equal under {method:?}");
}
}
}
}
#[test]
fn bilinear_exact_on_affine_2x_refinement() {
let src_lat = Lattice::regular(0.0, 0.0, 10.0, 10.0, 5, 5);
let src = sample_lattice(&src_lat, plane);
let target = Lattice::regular(0.0, 0.0, 5.0, 5.0, 9, 9);
let out = resample(&src, &src_lat, &target, ResampleMethod::Bilinear).unwrap();
for j in 0..target.nrow {
for i in 0..target.ncol {
let (x, y) = target.node_xy(i, j);
assert_relative_eq!(out[[i, j]], plane(x, y), epsilon = 1e-9);
}
}
}
#[test]
fn nearest_snaps_to_closest_node() {
let src_lat = Lattice::regular(0.0, 0.0, 10.0, 10.0, 3, 1);
let src = arr2(&[[0.0], [10.0], [20.0]]); let target = Lattice::regular(2.0, 0.0, 6.0, 10.0, 2, 1);
let out = resample(&src, &src_lat, &target, ResampleMethod::Nearest).unwrap();
assert_eq!(out[[0, 0]], 0.0);
assert_eq!(out[[1, 0]], 10.0);
}
#[test]
fn null_hole_propagation_bilinear() {
let src_lat = Lattice::regular(0.0, 0.0, 10.0, 10.0, 3, 3);
let mut src = arr2(&[[0.0, 10.0, 0.0], [10.0, 0.0, 0.0], [0.0, 0.0, 0.0]]);
src[[1, 1]] = f64::NAN;
let t_hole = Lattice::regular(5.0, 5.0, 10.0, 10.0, 1, 1);
let oh = resample(&src, &src_lat, &t_hole, ResampleMethod::Bilinear).unwrap();
assert!(oh[[0, 0]].is_nan(), "nearest corner NaN ⇒ NaN");
let t_fringe = Lattice::regular(3.0, 3.0, 10.0, 10.0, 1, 1);
let of = resample(&src, &src_lat, &t_fringe, ResampleMethod::Bilinear).unwrap();
let expected = (0.0 * 0.49 + 10.0 * 0.21 + 10.0 * 0.21) / (0.49 + 0.21 + 0.21);
assert!(
of[[0, 0]].is_finite(),
"finite corners must fill the fringe"
);
assert_relative_eq!(of[[0, 0]], expected, epsilon = 1e-12);
}
#[test]
fn outside_extent_is_nan() {
let src_lat = Lattice::regular(0.0, 0.0, 10.0, 10.0, 3, 3); let src = sample_lattice(&src_lat, plane);
let target = Lattice::regular(-50.0, 0.0, 60.0, 10.0, 3, 3);
let out = resample(&src, &src_lat, &target, ResampleMethod::Bilinear).unwrap();
assert!(out[[0, 0]].is_nan(), "left of extent → NaN");
assert!(out[[2, 0]].is_nan(), "right of extent → NaN");
assert!(out[[1, 0]].is_finite(), "inside extent → finite");
}
#[test]
fn offset_origin_honours_world_coords() {
let src_lat = Lattice::regular(1000.0, 2000.0, 10.0, 10.0, 5, 5);
let src = sample_lattice(&src_lat, plane);
let target = Lattice::regular(1005.0, 2005.0, 10.0, 10.0, 3, 3);
let out = resample(&src, &src_lat, &target, ResampleMethod::Bilinear).unwrap();
let (x, y) = target.node_xy(0, 0); assert_relative_eq!(out[[0, 0]], plane(x, y), epsilon = 1e-9);
assert!((out[[0, 0]] - plane(1000.0, 2000.0)).abs() > 1e-6);
}
#[test]
fn shape_mismatch_and_degenerate_error() {
let src_lat = Lattice::regular(0.0, 0.0, 10.0, 10.0, 3, 3);
let wrong = Array2::<f64>::zeros((2, 2));
assert!(matches!(
resample(&wrong, &src_lat, &src_lat, ResampleMethod::Nearest),
Err(AlgoError::InvalidArgument(_))
));
let degen = Lattice::regular(0.0, 0.0, 0.0, 10.0, 3, 3);
let ok_shape = Array2::<f64>::zeros((3, 3));
assert!(matches!(
resample(&ok_shape, °en, &src_lat, ResampleMethod::Nearest),
Err(AlgoError::InvalidGeometry(_))
));
}
}