use crate::foundation::{AlgoError, Lattice, Result};
use ndarray::Array2;
use super::prep::{dedup_coincident, dist2d};
use super::solve::LuFactorization;
use super::variogram::Variogram;
use crate::gridding::Gridder;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct OrdinaryKriging {
variogram: Variogram,
}
impl OrdinaryKriging {
pub fn new(variogram: Variogram) -> OrdinaryKriging {
OrdinaryKriging { variogram }
}
pub fn variogram(&self) -> &Variogram {
&self.variogram
}
pub fn krige(
&self,
coords: &[[f64; 3]],
lattice: &Lattice,
) -> Result<(Array2<f64>, Array2<f64>)> {
if coords.is_empty() {
return Err(AlgoError::EmptyInput("krige: no points to grid"));
}
let data = dedup_coincident(coords);
let n = data.len();
let m = n + 1;
let mut a = vec![0.0_f64; m * m];
for i in 0..n {
for j in 0..n {
a[i * m + j] = self
.variogram
.gamma(dist2d([data[i][0], data[i][1]], [data[j][0], data[j][1]]));
}
a[i * m + n] = 1.0; a[n * m + i] = 1.0; }
a[n * m + n] = 0.0;
let lu = LuFactorization::factor(a, m).ok_or(AlgoError::InvalidGeometry(
"kriging: system is singular (check the variogram and for duplicate points)",
))?;
let mut est = Array2::from_elem((lattice.ncol, lattice.nrow), f64::NAN);
let mut var = Array2::from_elem((lattice.ncol, lattice.nrow), f64::NAN);
let mut rhs = vec![0.0_f64; m];
rhs[n] = 1.0;
let mut sol: Vec<f64> = Vec::with_capacity(m);
for jj in 0..lattice.nrow {
for ii in 0..lattice.ncol {
let (x, y) = lattice.node_xy(ii, jj);
for (k, d) in data.iter().enumerate() {
rhs[k] = self.variogram.gamma(dist2d([d[0], d[1]], [x, y]));
}
lu.solve_into(&rhs, &mut sol);
let mut z = 0.0;
let mut sigma2 = sol[n]; for (k, d) in data.iter().enumerate() {
z += sol[k] * d[2];
sigma2 += sol[k] * rhs[k]; }
est[[ii, jj]] = z;
var[[ii, jj]] = sigma2.max(0.0);
}
}
Ok((est, var))
}
}
impl Gridder for OrdinaryKriging {
fn grid(&self, coords: &[[f64; 3]], lattice: &Lattice) -> Result<Array2<f64>> {
self.krige(coords, lattice).map(|(est, _var)| est)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gridding::kriging::VariogramModel;
use approx::assert_relative_eq;
fn spherical(nugget: f64) -> Variogram {
Variogram::new(VariogramModel::Spherical, nugget, 1.0, 10.0).unwrap()
}
#[test]
fn empty_input_errors() {
let ok = OrdinaryKriging::new(spherical(0.0));
let g = Lattice::regular(0.0, 0.0, 1.0, 1.0, 4, 4);
assert!(matches!(ok.grid(&[], &g), Err(AlgoError::EmptyInput(_))));
}
#[test]
fn exact_at_data_points_with_no_nugget() {
let lattice = Lattice::regular(0.0, 0.0, 1.0, 1.0, 6, 6);
let coords = [
[0.0, 0.0, 10.0],
[5.0, 0.0, 22.0],
[0.0, 5.0, 7.0],
[5.0, 5.0, 40.0],
[2.0, 3.0, 18.0],
];
let ok = OrdinaryKriging::new(spherical(0.0));
let (est, var) = ok.krige(&coords, &lattice).unwrap();
for c in &coords {
let (fi, fj) = lattice.xy_to_ij(c[0], c[1]).unwrap();
let (i, j) = (fi.round() as usize, fj.round() as usize);
assert_relative_eq!(est[[i, j]], c[2], epsilon = 1e-7);
assert!(var[[i, j]].abs() < 1e-7, "variance {} not ~0", var[[i, j]]);
}
}
#[test]
fn constant_data_reproduced_everywhere() {
let lattice = Lattice::regular(0.0, 0.0, 1.0, 1.0, 7, 5);
let coords = [
[0.0, 0.0, 12.5],
[6.0, 0.0, 12.5],
[3.0, 4.0, 12.5],
[6.0, 4.0, 12.5],
];
let ok = OrdinaryKriging::new(spherical(0.5));
let est = ok.grid(&coords, &lattice).unwrap();
for v in est.iter() {
assert_relative_eq!(*v, 12.5, epsilon = 1e-9);
}
}
#[test]
fn symmetric_midpoint_averages_two_points() {
let lattice = Lattice::regular(1.0, 0.0, 1.0, 1.0, 1, 1); let coords = [[0.0, 0.0, 10.0], [2.0, 0.0, 20.0]];
let ok = OrdinaryKriging::new(spherical(0.0));
let est = ok.grid(&coords, &lattice).unwrap();
assert_relative_eq!(est[[0, 0]], 15.0, epsilon = 1e-9);
}
#[test]
fn symmetric_centre_of_square_averages_four_points() {
let lattice = Lattice::regular(1.0, 1.0, 1.0, 1.0, 1, 1); let coords = [
[0.0, 0.0, 4.0],
[2.0, 0.0, 8.0],
[0.0, 2.0, 12.0],
[2.0, 2.0, 16.0],
];
let ok = OrdinaryKriging::new(spherical(0.0));
let est = ok.grid(&coords, &lattice).unwrap();
assert_relative_eq!(est[[0, 0]], 10.0, epsilon = 1e-9);
}
#[test]
fn estimate_is_bounded_by_the_data_range() {
let lattice = Lattice::regular(0.0, 0.0, 1.0, 1.0, 11, 11);
let coords = [
[0.0, 0.0, 5.0],
[10.0, 0.0, 25.0],
[0.0, 10.0, 15.0],
[10.0, 10.0, 35.0],
[5.0, 5.0, 20.0],
];
let (lo, hi) = (5.0, 35.0);
let ok = OrdinaryKriging::new(spherical(0.0));
let est = ok.grid(&coords, &lattice).unwrap();
for v in est.iter() {
assert!(*v >= lo - 1e-6 && *v <= hi + 1e-6, "out of range: {v}");
}
}
#[test]
fn duplicate_points_are_averaged_not_singular() {
let lattice = Lattice::regular(0.0, 0.0, 1.0, 1.0, 4, 4);
let coords = [
[0.0, 0.0, 0.0],
[1.0, 1.0, 10.0],
[1.0, 1.0, 20.0],
[3.0, 3.0, 30.0],
];
let ok = OrdinaryKriging::new(spherical(0.0));
let (est, _var) = ok.krige(&coords, &lattice).unwrap();
assert_relative_eq!(est[[1, 1]], 15.0, epsilon = 1e-7);
}
#[test]
fn variance_grows_away_from_data() {
let lattice = Lattice::regular(0.0, 0.0, 1.0, 1.0, 21, 21);
let coords = [[0.0, 0.0, 10.0], [20.0, 20.0, 30.0]];
let ok = OrdinaryKriging::new(spherical(0.0));
let (_est, var) = ok.krige(&coords, &lattice).unwrap();
let near = var[[0, 0]]; let far = var[[10, 10]]; assert!(near < far, "near {near} should be < far {far}");
}
}