use crate::Real;
use crate::error::KrigingError;
use crate::variogram::models::{VariogramModel, VariogramType};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SpaceTimeVariogram {
Separable {
spatial: VariogramModel,
temporal: VariogramModel,
},
ProductSum {
spatial: VariogramModel,
temporal: VariogramModel,
k1: Real,
k2: Real,
k3: Real,
},
}
impl SpaceTimeVariogram {
pub fn new_separable(
spatial: VariogramModel,
temporal: VariogramModel,
) -> Result<Self, KrigingError> {
reject_power_component(spatial, "spatial")?;
reject_power_component(temporal, "temporal")?;
Ok(Self::Separable { spatial, temporal })
}
pub fn new_product_sum(
spatial: VariogramModel,
temporal: VariogramModel,
k1: Real,
k2: Real,
k3: Real,
) -> Result<Self, KrigingError> {
reject_power_component(spatial, "spatial")?;
reject_power_component(temporal, "temporal")?;
for (name, k) in [("k1", k1), ("k2", k2), ("k3", k3)] {
if !k.is_finite() || k < 0.0 {
return Err(KrigingError::InvalidInput(format!(
"product-sum {name} must be finite and non-negative, got {k}"
)));
}
}
if k1 + k2 + k3 <= 0.0 {
return Err(KrigingError::InvalidInput(
"product-sum coefficients must sum to a positive value".to_string(),
));
}
Ok(Self::ProductSum {
spatial,
temporal,
k1,
k2,
k3,
})
}
pub fn spatial(&self) -> VariogramModel {
match *self {
Self::Separable { spatial, .. } | Self::ProductSum { spatial, .. } => spatial,
}
}
pub fn temporal(&self) -> VariogramModel {
match *self {
Self::Separable { temporal, .. } | Self::ProductSum { temporal, .. } => temporal,
}
}
pub fn covariance(&self, h_spatial: Real, h_temporal: Real) -> Real {
let hs = h_spatial.max(0.0);
let ht = h_temporal.max(0.0);
match *self {
Self::Separable { spatial, temporal } => {
spatial.covariance(hs) * temporal.covariance(ht)
}
Self::ProductSum {
spatial,
temporal,
k1,
k2,
k3,
} => {
let cs = spatial.covariance(hs);
let ct = temporal.covariance(ht);
k1 * cs * ct + k2 * cs + k3 * ct
}
}
}
pub fn semivariance(&self, h_spatial: Real, h_temporal: Real) -> Real {
self.c_at_zero() - self.covariance(h_spatial, h_temporal)
}
pub fn c_at_zero(&self) -> Real {
match *self {
Self::Separable { spatial, temporal } => {
spatial.covariance(0.0) * temporal.covariance(0.0)
}
Self::ProductSum {
spatial,
temporal,
k1,
k2,
k3,
} => {
let cs0 = spatial.covariance(0.0);
let ct0 = temporal.covariance(0.0);
k1 * cs0 * ct0 + k2 * cs0 + k3 * ct0
}
}
}
pub(crate) fn marginal_variogram_types(&self) -> [VariogramType; 2] {
match *self {
Self::Separable { spatial, temporal }
| Self::ProductSum {
spatial, temporal, ..
} => [spatial.variogram_type(), temporal.variogram_type()],
}
}
}
fn reject_power_component(v: VariogramModel, label: &str) -> Result<(), KrigingError> {
if matches!(v, VariogramModel::Power { .. }) {
Err(KrigingError::InvalidInput(format!(
"{label} marginal cannot be a Power variogram (unbounded covariance at origin)"
)))
} else {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
fn spatial_exp() -> VariogramModel {
VariogramModel::new(0.0, 1.0, 10.0, VariogramType::Exponential).unwrap()
}
fn temporal_exp() -> VariogramModel {
VariogramModel::new(0.0, 2.0, 5.0, VariogramType::Exponential).unwrap()
}
#[test]
fn separable_covariance_is_product_of_marginals() {
let stv = SpaceTimeVariogram::new_separable(spatial_exp(), temporal_exp()).unwrap();
let hs = 3.0;
let ht = 1.5;
let expected = spatial_exp().covariance(hs) * temporal_exp().covariance(ht);
assert_relative_eq!(stv.covariance(hs, ht), expected, epsilon = 1e-6);
}
#[test]
fn separable_c_at_zero_is_product_of_sills() {
let stv = SpaceTimeVariogram::new_separable(spatial_exp(), temporal_exp()).unwrap();
let expected = spatial_exp().covariance(0.0) * temporal_exp().covariance(0.0);
assert_relative_eq!(stv.c_at_zero(), expected, epsilon = 1e-6);
}
#[test]
fn semivariance_complements_covariance() {
let stv = SpaceTimeVariogram::new_separable(spatial_exp(), temporal_exp()).unwrap();
let hs = 2.2;
let ht = 0.8;
assert_relative_eq!(
stv.covariance(hs, ht) + stv.semivariance(hs, ht),
stv.c_at_zero(),
epsilon = 1e-5
);
}
#[test]
fn separable_rejects_power_marginals() {
let power = VariogramModel::new_power(0.0, 0.1, 1.0).unwrap();
assert!(SpaceTimeVariogram::new_separable(power, temporal_exp()).is_err());
assert!(SpaceTimeVariogram::new_separable(spatial_exp(), power).is_err());
}
#[test]
fn product_sum_reduces_to_separable_when_k2_and_k3_are_zero() {
let stv_sep = SpaceTimeVariogram::new_separable(spatial_exp(), temporal_exp()).unwrap();
let stv_ps =
SpaceTimeVariogram::new_product_sum(spatial_exp(), temporal_exp(), 1.0, 0.0, 0.0)
.unwrap();
assert_relative_eq!(
stv_sep.covariance(2.0, 1.0),
stv_ps.covariance(2.0, 1.0),
epsilon = 1e-6
);
}
#[test]
fn product_sum_rejects_negative_coefficients() {
assert!(
SpaceTimeVariogram::new_product_sum(spatial_exp(), temporal_exp(), -0.1, 1.0, 1.0)
.is_err()
);
}
#[test]
fn product_sum_rejects_all_zero_coefficients() {
assert!(
SpaceTimeVariogram::new_product_sum(spatial_exp(), temporal_exp(), 0.0, 0.0, 0.0)
.is_err()
);
}
}