use crate::foundation::{AlgoError, Result};
use crate::geostat::experimental::ExperimentalVariogram;
use crate::gridding::kriging::{Variogram, VariogramModel};
fn normalized_structural(model: VariogramModel, r: f64) -> f64 {
match model {
VariogramModel::Nugget => 0.0,
VariogramModel::Spherical => {
if r >= 1.0 {
1.0
} else {
1.5 * r - 0.5 * r * r * r
}
}
VariogramModel::Exponential => 1.0 - (-3.0 * r).exp(),
VariogramModel::Gaussian => 1.0 - (-3.0 * r * r).exp(),
}
}
fn best_c0_c_for_range(
model: VariogramModel,
exp: &ExperimentalVariogram,
range: f64,
) -> (f64, f64, f64) {
let g: Vec<f64> = exp
.lags
.iter()
.map(|&h| normalized_structural(model, h / range))
.collect();
let (mut sw, mut swg, mut swgg, mut swy, mut swgy) = (0.0, 0.0, 0.0, 0.0, 0.0);
for ((&gk, &count), &y) in g.iter().zip(&exp.counts).zip(&exp.semivariances) {
let w = count as f64;
sw += w;
swg += w * gk;
swgg += w * gk * gk;
swy += w * y;
swgy += w * gk * y;
}
let wsse = |c0: f64, c: f64| -> f64 {
(0..exp.len())
.map(|k| {
let w = exp.counts[k] as f64;
let r = c0 + c * g[k] - exp.semivariances[k];
w * r * r
})
.sum()
};
let mut candidates: Vec<(f64, f64)> = Vec::new();
let det = sw * swgg - swg * swg;
if det.abs() > 1e-300 {
let c0 = (swy * swgg - swg * swgy) / det;
let c = (sw * swgy - swg * swy) / det;
candidates.push((c0, c));
}
if swgg > 0.0 {
candidates.push((0.0, swgy / swgg));
}
if sw > 0.0 {
candidates.push((swy / sw, 0.0));
}
candidates.push((0.0, 0.0));
let mut best = (0.0, 0.0, f64::INFINITY);
for (c0, c) in candidates {
if c0 < -1e-12 || c < -1e-12 {
continue;
}
let (c0, c) = (c0.max(0.0), c.max(0.0));
let e = wsse(c0, c);
if e < best.2 {
best = (c0, c, e);
}
}
best
}
impl Variogram {
pub fn fit(model: VariogramModel, exp: &ExperimentalVariogram) -> Result<Variogram> {
if exp.is_empty() {
return Err(AlgoError::EmptyInput(
"Variogram::fit: empty experimental variogram",
));
}
let h_max = exp.lags.iter().cloned().fold(0.0_f64, f64::max);
if model == VariogramModel::Nugget {
let sw: f64 = exp.counts.iter().map(|&n| n as f64).sum();
let swy: f64 = exp
.semivariances
.iter()
.zip(&exp.counts)
.map(|(y, &n)| n as f64 * y)
.sum();
let c0 = if sw > 0.0 { swy / sw } else { 0.0 };
return Variogram::new(VariogramModel::Nugget, c0, 0.0, h_max.max(1.0));
}
let sweep = |lo: f64, hi: f64, steps: usize| -> (f64, f64, f64, f64) {
let mut best = (0.0, 0.0, 0.0, f64::INFINITY); for s in 0..steps {
let range = lo + (hi - lo) * s as f64 / (steps.max(2) - 1) as f64;
if range <= 0.0 {
continue;
}
let (c0, c, sse) = best_c0_c_for_range(model, exp, range);
if sse < best.3 {
best = (range, c0, c, sse);
}
}
best
};
let coarse = sweep(h_max / 50.0, h_max * 2.0, 400);
let step = (h_max * 2.0 - h_max / 50.0) / 399.0;
let fine = sweep((coarse.0 - step).max(h_max / 100.0), coarse.0 + step, 100);
let best = if fine.3 < coarse.3 { fine } else { coarse };
Variogram::new(model, best.1, best.2, best.0.max(1e-9))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::geostat::experimental::experimental_variogram;
fn synth_from_model(truth: &Variogram, h_max: f64, n: usize) -> ExperimentalVariogram {
let lags: Vec<f64> = (1..=n).map(|k| h_max * k as f64 / n as f64).collect();
let semivariances: Vec<f64> = lags.iter().map(|&h| truth.gamma(h)).collect();
ExperimentalVariogram {
lags,
counts: vec![100; n],
semivariances,
}
}
#[test]
fn recovers_spherical_parameters() {
let truth = Variogram::new(VariogramModel::Spherical, 0.5, 3.0, 40.0).unwrap();
let exp = synth_from_model(&truth, 80.0, 40);
let fit = Variogram::fit(VariogramModel::Spherical, &exp).unwrap();
assert!((fit.nugget - 0.5).abs() < 0.1, "nugget {}", fit.nugget);
assert!((fit.sill - 3.0).abs() < 0.15, "sill {}", fit.sill);
assert!((fit.range - 40.0).abs() < 2.0, "range {}", fit.range);
}
#[test]
fn recovers_exponential_parameters() {
let truth = Variogram::new(VariogramModel::Exponential, 0.0, 2.0, 25.0).unwrap();
let exp = synth_from_model(&truth, 80.0, 40);
let fit = Variogram::fit(VariogramModel::Exponential, &exp).unwrap();
assert!(fit.nugget < 0.1, "nugget {}", fit.nugget);
assert!((fit.sill - 2.0).abs() < 0.2, "sill {}", fit.sill);
assert!((fit.range - 25.0).abs() < 3.0, "range {}", fit.range);
}
#[test]
fn empty_experimental_errors() {
let ev = ExperimentalVariogram {
lags: vec![],
semivariances: vec![],
counts: vec![],
};
assert!(Variogram::fit(VariogramModel::Spherical, &ev).is_err());
}
#[test]
fn end_to_end_from_scattered_data() {
let mut coords = Vec::new();
for i in 0..10 {
for j in 0..10 {
coords.push([i as f64, j as f64, i as f64]);
}
}
let exp = experimental_variogram(&coords, 1.0, 8).unwrap();
let fit = Variogram::fit(VariogramModel::Spherical, &exp).unwrap();
assert!(fit.total_sill() > 0.0);
assert!(fit.range > 0.0);
}
}