Skip to main content

kriging_rs/variogram/
nested.rs

1//! Nested (additive) variogram composition.
2//!
3//! A [`NestedVariogram`] is the sum of several component [`VariogramModel`]s. This lets you
4//! model multi-scale structure (e.g. short-range exponential + long-range Gaussian) by
5//! composing familiar building blocks.
6//!
7//! `NestedVariogram` exposes the same `semivariance` / `covariance` interface as a single
8//! [`VariogramModel`] but cannot currently be passed directly to
9//! [`OrdinaryKrigingModel`](crate::OrdinaryKrigingModel). It is intended for callers that
10//! need multi-scale semivariances (e.g. custom kriging solvers or diagnostic plots).
11
12use crate::Real;
13use crate::error::KrigingError;
14use crate::variogram::models::VariogramModel;
15
16/// Sum of one or more component variograms. `γ(h) = Σ_k γ_k(h)`.
17#[derive(Debug, Clone)]
18pub struct NestedVariogram {
19    components: Vec<VariogramModel>,
20}
21
22impl NestedVariogram {
23    /// Construct a nested variogram from at least one component.
24    pub fn new(components: Vec<VariogramModel>) -> Result<Self, KrigingError> {
25        if components.is_empty() {
26            return Err(KrigingError::InvalidInput(
27                "nested variogram requires at least one component".to_string(),
28            ));
29        }
30        Ok(Self { components })
31    }
32
33    /// Read-only view of the component models in insertion order.
34    pub fn components(&self) -> &[VariogramModel] {
35        &self.components
36    }
37
38    /// Sum of component semivariances at `distance`.
39    pub fn semivariance(&self, distance: Real) -> Real {
40        self.components
41            .iter()
42            .map(|m| m.semivariance(distance))
43            .sum()
44    }
45
46    /// Sum of component covariances at `distance`. The result is interpreted as the
47    /// covariance of the multi-scale Gaussian process under standard additivity assumptions.
48    ///
49    /// Note: components with no finite variance (e.g. the unbounded
50    /// [`VariogramType::Power`](crate::VariogramType::Power) model) return `0` from
51    /// [`VariogramModel::covariance`], so including them here is discouraged.
52    pub fn covariance(&self, distance: Real) -> Real {
53        self.components.iter().map(|m| m.covariance(distance)).sum()
54    }
55
56    /// Aggregate "variance at zero" — i.e. the effective sill `Σ_k sill_k` across bounded
57    /// components.
58    pub fn total_sill(&self) -> Real {
59        self.components
60            .iter()
61            .map(|m| m.covariance(0.0) + m.semivariance(0.0))
62            .sum()
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69    use crate::variogram::models::VariogramType;
70
71    #[test]
72    fn new_rejects_empty_components() {
73        assert!(NestedVariogram::new(vec![]).is_err());
74    }
75
76    #[test]
77    fn semivariance_is_sum_of_components() {
78        let a = VariogramModel::new(0.1, 1.0, 5.0, VariogramType::Exponential).unwrap();
79        let b = VariogramModel::new(0.0, 2.0, 30.0, VariogramType::Gaussian).unwrap();
80        let nested = NestedVariogram::new(vec![a, b]).unwrap();
81        let h = 7.0;
82        let sum = a.semivariance(h) + b.semivariance(h);
83        assert!((nested.semivariance(h) - sum).abs() < 1e-5);
84    }
85
86    #[test]
87    fn covariance_complements_semivariance() {
88        let a = VariogramModel::new(0.1, 1.0, 5.0, VariogramType::Exponential).unwrap();
89        let b = VariogramModel::new(0.0, 2.0, 30.0, VariogramType::Gaussian).unwrap();
90        let nested = NestedVariogram::new(vec![a, b]).unwrap();
91        let h = 4.0;
92        let total = nested.total_sill();
93        assert!((nested.covariance(h) + nested.semivariance(h) - total).abs() < 1e-4);
94    }
95}