#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum VariogramModel {
Nugget,
Spherical,
Exponential,
Gaussian,
}
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Variogram {
pub model: VariogramModel,
pub nugget: f64,
pub sill: f64,
pub range: f64,
}
impl Variogram {
pub fn new(
model: VariogramModel,
nugget: f64,
sill: f64,
range: f64,
) -> crate::foundation::Result<Variogram> {
use crate::foundation::AlgoError;
if !(nugget.is_finite() && sill.is_finite() && range.is_finite()) {
return Err(AlgoError::InvalidArgument(
"variogram: nugget, sill and range must be finite".to_string(),
));
}
if nugget < 0.0 || sill < 0.0 {
return Err(AlgoError::InvalidArgument(
"variogram: nugget and sill must be non-negative".to_string(),
));
}
let effective_sill = match model {
VariogramModel::Nugget => nugget,
_ => nugget + sill,
};
if effective_sill <= 0.0 {
return Err(AlgoError::InvalidArgument(
match model {
VariogramModel::Nugget => {
"variogram: the Nugget model uses only its nugget (sill is ignored), so nugget must be positive"
}
_ => "variogram: total sill (nugget + sill) must be positive",
}
.to_string(),
));
}
if range <= 0.0 {
return Err(AlgoError::InvalidArgument(
"variogram: range must be positive".to_string(),
));
}
Ok(Variogram {
model,
nugget,
sill,
range,
})
}
pub fn total_sill(&self) -> f64 {
self.nugget + self.sill
}
pub fn gamma(&self, h: f64) -> f64 {
let h = h.abs();
if h == 0.0 {
return 0.0;
}
let structured = match self.model {
VariogramModel::Nugget => 0.0,
VariogramModel::Spherical => {
let r = h / self.range;
if r >= 1.0 {
self.sill
} else {
self.sill * (1.5 * r - 0.5 * r * r * r)
}
}
VariogramModel::Exponential => self.sill * (1.0 - (-3.0 * h / self.range).exp()),
VariogramModel::Gaussian => {
let r = h / self.range;
self.sill * (1.0 - (-3.0 * r * r).exp())
}
};
self.nugget + structured
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn rejects_invalid_parameters() {
assert!(Variogram::new(VariogramModel::Spherical, -1.0, 1.0, 10.0).is_err());
assert!(Variogram::new(VariogramModel::Spherical, 0.0, -1.0, 10.0).is_err());
assert!(Variogram::new(VariogramModel::Spherical, 0.0, 0.0, 10.0).is_err()); assert!(Variogram::new(VariogramModel::Spherical, 0.0, 1.0, 0.0).is_err()); assert!(Variogram::new(VariogramModel::Spherical, 0.0, 1.0, f64::NAN).is_err());
assert!(Variogram::new(VariogramModel::Spherical, 0.0, 1.0, 10.0).is_ok());
assert!(matches!(
Variogram::new(VariogramModel::Spherical, 0.0, 1.0, 0.0),
Err(crate::foundation::AlgoError::InvalidArgument(_))
));
}
#[test]
fn nugget_model_rejects_zero_nugget() {
let err = Variogram::new(VariogramModel::Nugget, 0.0, 5.0, 10.0);
assert!(err.is_err(), "zero-nugget Nugget must fail at construction");
assert!(Variogram::new(VariogramModel::Nugget, 1.0, 0.0, 10.0).is_ok());
assert!(Variogram::new(VariogramModel::Spherical, 0.0, 1.0, 10.0).is_ok());
}
#[test]
fn gamma_is_zero_at_origin() {
for model in [
VariogramModel::Nugget,
VariogramModel::Spherical,
VariogramModel::Exponential,
VariogramModel::Gaussian,
] {
let v = Variogram::new(model, 0.5, 2.0, 10.0).unwrap();
assert_eq!(v.gamma(0.0), 0.0, "{model:?}");
}
}
#[test]
fn nugget_jumps_then_flat() {
let v = Variogram::new(VariogramModel::Nugget, 3.0, 5.0, 10.0).unwrap();
assert_eq!(v.gamma(0.0), 0.0);
assert_eq!(v.gamma(0.01), 3.0);
assert_eq!(v.gamma(1e6), 3.0);
}
#[test]
fn spherical_reaches_sill_exactly_at_range() {
let v = Variogram::new(VariogramModel::Spherical, 0.0, 4.0, 10.0).unwrap();
assert_relative_eq!(v.gamma(10.0), 4.0, epsilon = 1e-12);
assert_relative_eq!(v.gamma(15.0), 4.0, epsilon = 1e-12);
assert_relative_eq!(v.gamma(5.0), 4.0 * 0.687_5, epsilon = 1e-12);
}
#[test]
fn exponential_and_gaussian_hit_95pct_at_practical_range() {
let expected = 1.0 - (-3.0_f64).exp();
let e = Variogram::new(VariogramModel::Exponential, 0.0, 1.0, 20.0).unwrap();
assert_relative_eq!(e.gamma(20.0), expected, epsilon = 1e-12);
let g = Variogram::new(VariogramModel::Gaussian, 0.0, 1.0, 20.0).unwrap();
assert_relative_eq!(g.gamma(20.0), expected, epsilon = 1e-12);
}
#[test]
fn monotone_non_decreasing_and_bounded_by_total_sill() {
let v = Variogram::new(VariogramModel::Exponential, 1.0, 3.0, 15.0).unwrap();
let mut prev = v.gamma(1e-9);
let mut h = 0.0;
while h < 100.0 {
h += 0.5;
let g = v.gamma(h);
assert!(g >= prev - 1e-12, "not monotone at h={h}: {g} < {prev}");
assert!(g <= v.total_sill() + 1e-12, "exceeds sill at h={h}: {g}");
prev = g;
}
}
#[test]
fn gamma_is_symmetric_in_lag_sign() {
let v = Variogram::new(VariogramModel::Gaussian, 0.2, 1.0, 5.0).unwrap();
assert_eq!(v.gamma(3.0), v.gamma(-3.0));
}
}