use crate::error::{SpatialError, SpatialResult};
use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2};
use scirs2_core::numeric::Float;
use std::f64::consts::PI;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum VariogramModel {
Spherical,
Exponential,
Gaussian,
Linear,
Power,
Matern,
}
#[derive(Debug, Clone)]
pub struct FittedVariogram<T: Float> {
pub model: VariogramModel,
pub range: T,
pub sill: T,
pub nugget: T,
pub extra_params: Vec<T>,
pub r_squared: T,
}
impl<T: Float> FittedVariogram<T> {
pub fn evaluate(&self, distance: T) -> T {
match self.model {
VariogramModel::Spherical => {
if distance >= self.range {
self.nugget + self.sill
} else {
let h_over_r = distance / self.range;
let three_halves = T::from(1.5).expect("conversion failed");
let half = T::from(0.5).expect("conversion failed");
self.nugget
+ self.sill
* (three_halves * h_over_r - half * h_over_r * h_over_r * h_over_r)
}
}
VariogramModel::Exponential => {
self.nugget + self.sill * (T::one() - (-distance / self.range).exp())
}
VariogramModel::Gaussian => {
let h_over_r = distance / self.range;
self.nugget + self.sill * (T::one() - (-(h_over_r * h_over_r)).exp())
}
VariogramModel::Linear => {
let slope = if !self.extra_params.is_empty() {
self.extra_params[0]
} else {
self.sill / self.range
};
self.nugget + slope * distance
}
VariogramModel::Power => {
let power = if !self.extra_params.is_empty() {
self.extra_params[0]
} else {
T::from(0.5).expect("conversion failed")
};
self.nugget + self.sill * distance.powf(power)
}
VariogramModel::Matern => {
let nu = if !self.extra_params.is_empty() {
self.extra_params[0]
} else {
T::from(1.5).expect("conversion failed") };
if distance.is_zero() {
self.nugget
} else {
let scaled_dist = distance / self.range
* T::from(2.0).expect("conversion failed")
* nu.sqrt();
let term = (T::one() + scaled_dist) * (-scaled_dist).exp();
self.nugget + self.sill * (T::one() - term)
}
}
}
}
}
pub fn experimental_variogram<T: Float>(
coordinates: &ArrayView2<T>,
values: &ArrayView1<T>,
n_lags: usize,
lag_tolerance: Option<T>,
) -> SpatialResult<(Array1<T>, Array1<T>)> {
let n = coordinates.shape()[0];
if n != values.len() {
return Err(SpatialError::DimensionError(
"Number of coordinates must match number of values".to_string(),
));
}
if n < 2 {
return Err(SpatialError::ValueError(
"Need at least 2 points for variogram".to_string(),
));
}
let mut pairs = Vec::new();
for i in 0..n {
for j in (i + 1)..n {
let mut dist_sq = T::zero();
for k in 0..coordinates.shape()[1] {
let diff = coordinates[[i, k]] - coordinates[[j, k]];
dist_sq = dist_sq + diff * diff;
}
let distance = dist_sq.sqrt();
let value_diff = values[i] - values[j];
let gamma = value_diff * value_diff / (T::one() + T::one());
pairs.push((distance, gamma));
}
}
if pairs.is_empty() {
return Err(SpatialError::ValueError("No valid pairs found".to_string()));
}
let max_distance = pairs
.iter()
.map(|(d, _)| *d)
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.ok_or_else(|| SpatialError::ValueError("Failed to find max distance".to_string()))?;
let lag_size = max_distance / T::from(n_lags).expect("conversion failed");
let tolerance = lag_tolerance.unwrap_or(lag_size / (T::one() + T::one()));
let mut lag_bins: Vec<Vec<T>> = vec![Vec::new(); n_lags];
let mut lag_centers = Array1::zeros(n_lags);
for i in 0..n_lags {
let lag_center = lag_size
* (T::from(i).expect("conversion failed") + T::from(0.5).expect("conversion failed"));
lag_centers[i] = lag_center;
for &(distance, gamma) in &pairs {
if (distance - lag_center).abs() <= tolerance {
lag_bins[i].push(gamma);
}
}
}
let mut gamma_values = Array1::zeros(n_lags);
let mut valid_lags = Vec::new();
let mut valid_gammas = Vec::new();
for i in 0..n_lags {
if !lag_bins[i].is_empty() {
let sum: T = lag_bins[i]
.iter()
.copied()
.fold(T::zero(), |acc, x| acc + x);
let mean = sum / T::from(lag_bins[i].len()).expect("conversion failed");
gamma_values[i] = mean;
valid_lags.push(lag_centers[i]);
valid_gammas.push(mean);
}
}
if valid_lags.is_empty() {
return Err(SpatialError::ValueError(
"No valid lags computed".to_string(),
));
}
let lags_array = Array1::from_vec(valid_lags);
let gamma_array = Array1::from_vec(valid_gammas);
Ok((lags_array, gamma_array))
}
pub fn fit_variogram<T: Float>(
lags: &Array1<T>,
gamma: &Array1<T>,
model: VariogramModel,
) -> SpatialResult<FittedVariogram<T>> {
if lags.len() != gamma.len() {
return Err(SpatialError::DimensionError(
"Lags and gamma must have same length".to_string(),
));
}
if lags.is_empty() {
return Err(SpatialError::ValueError(
"Need at least one lag-gamma pair".to_string(),
));
}
let max_lag = lags
.iter()
.copied()
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.ok_or_else(|| SpatialError::ValueError("Failed to find max lag".to_string()))?;
let max_gamma = gamma
.iter()
.copied()
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.ok_or_else(|| SpatialError::ValueError("Failed to find max gamma".to_string()))?;
let initial_range = max_lag * T::from(0.7).expect("conversion failed");
let initial_sill = max_gamma * T::from(0.9).expect("conversion failed");
let initial_nugget = gamma[0] * T::from(0.1).expect("conversion failed");
let fitted = FittedVariogram {
model,
range: initial_range,
sill: initial_sill,
nugget: initial_nugget,
extra_params: vec![],
r_squared: T::from(0.0).expect("conversion failed"), };
let mean_gamma = gamma.sum() / T::from(gamma.len()).expect("conversion failed");
let mut ss_res = T::zero();
let mut ss_tot = T::zero();
for i in 0..lags.len() {
let predicted = fitted.evaluate(lags[i]);
let residual = gamma[i] - predicted;
ss_res = ss_res + residual * residual;
let deviation = gamma[i] - mean_gamma;
ss_tot = ss_tot + deviation * deviation;
}
let r_squared = if ss_tot > T::zero() {
T::one() - ss_res / ss_tot
} else {
T::zero()
};
Ok(FittedVariogram {
model: fitted.model,
range: fitted.range,
sill: fitted.sill,
nugget: fitted.nugget,
extra_params: fitted.extra_params,
r_squared,
})
}
pub fn directional_variogram<T: Float>(
coordinates: &ArrayView2<T>,
values: &ArrayView1<T>,
direction: T,
tolerance: T,
n_lags: usize,
) -> SpatialResult<(Array1<T>, Array1<T>)> {
let n = coordinates.shape()[0];
if coordinates.shape()[1] != 2 {
return Err(SpatialError::DimensionError(
"Directional variogram requires 2D coordinates".to_string(),
));
}
if n != values.len() {
return Err(SpatialError::DimensionError(
"Number of coordinates must match number of values".to_string(),
));
}
let mut pairs = Vec::new();
for i in 0..n {
for j in (i + 1)..n {
let dx = coordinates[[j, 0]] - coordinates[[i, 0]];
let dy = coordinates[[j, 1]] - coordinates[[i, 1]];
let distance = (dx * dx + dy * dy).sqrt();
let angle = dy.atan2(dx);
let angle_diff = (angle - direction).abs();
let pi_t = T::from(PI).expect("conversion failed");
let angle_diff_wrapped = if angle_diff > pi_t {
(T::one() + T::one()) * pi_t - angle_diff
} else {
angle_diff
};
if angle_diff_wrapped <= tolerance {
let value_diff = values[i] - values[j];
let gamma = value_diff * value_diff / (T::one() + T::one());
pairs.push((distance, gamma));
}
}
}
if pairs.is_empty() {
return Err(SpatialError::ValueError(
"No pairs found in specified direction".to_string(),
));
}
let max_distance = pairs
.iter()
.map(|(d, _)| *d)
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.ok_or_else(|| SpatialError::ValueError("Failed to find max distance".to_string()))?;
let lag_size = max_distance / T::from(n_lags).expect("conversion failed");
let mut lag_bins: Vec<Vec<T>> = vec![Vec::new(); n_lags];
let mut lag_centers = Array1::zeros(n_lags);
for i in 0..n_lags {
let lag_center = lag_size
* (T::from(i).expect("conversion failed") + T::from(0.5).expect("conversion failed"));
lag_centers[i] = lag_center;
for &(distance, gamma) in &pairs {
let lag_tolerance = lag_size / (T::one() + T::one());
if (distance - lag_center).abs() <= lag_tolerance {
lag_bins[i].push(gamma);
}
}
}
let mut valid_lags = Vec::new();
let mut valid_gammas = Vec::new();
for i in 0..n_lags {
if !lag_bins[i].is_empty() {
let sum: T = lag_bins[i]
.iter()
.copied()
.fold(T::zero(), |acc, x| acc + x);
let mean = sum / T::from(lag_bins[i].len()).expect("conversion failed");
valid_lags.push(lag_centers[i]);
valid_gammas.push(mean);
}
}
Ok((Array1::from_vec(valid_lags), Array1::from_vec(valid_gammas)))
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
use scirs2_core::ndarray::array;
#[test]
fn test_experimental_variogram() {
let coords = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]];
let values = array![1.0, 2.0, 1.5, 2.5];
let result = experimental_variogram(&coords.view(), &values.view(), 5, None);
assert!(result.is_ok());
let (lags, gamma) = result.expect("computation failed");
assert!(!lags.is_empty());
assert_eq!(lags.len(), gamma.len());
for &g in gamma.iter() {
assert!(g >= 0.0);
}
}
#[test]
fn test_fit_spherical_variogram() {
let lags = array![0.5, 1.0, 1.5, 2.0, 2.5];
let gamma = array![0.1, 0.3, 0.6, 0.85, 0.95];
let fitted = fit_variogram(&lags, &gamma, VariogramModel::Spherical);
assert!(fitted.is_ok());
let model = fitted.expect("fitting failed");
assert!(model.range > 0.0);
assert!(model.sill > 0.0);
assert!(model.nugget >= 0.0);
}
#[test]
fn test_fit_exponential_variogram() {
let lags = array![0.5, 1.0, 1.5, 2.0, 2.5];
let gamma = array![0.2, 0.4, 0.6, 0.75, 0.85];
let fitted = fit_variogram(&lags, &gamma, VariogramModel::Exponential);
assert!(fitted.is_ok());
let model = fitted.expect("fitting failed");
assert!(model.range > 0.0);
assert!(model.sill > 0.0);
}
#[test]
fn test_variogram_evaluate() {
let fitted = FittedVariogram {
model: VariogramModel::Spherical,
range: 2.0,
sill: 1.0,
nugget: 0.1,
extra_params: vec![],
r_squared: 0.95,
};
let gamma_0 = fitted.evaluate(0.0);
assert_relative_eq!(gamma_0, 0.1, epsilon = 0.01);
let gamma_range = fitted.evaluate(2.0);
assert!(gamma_range >= 1.0);
assert!(gamma_range <= 1.2);
let gamma_beyond = fitted.evaluate(5.0);
assert_relative_eq!(gamma_beyond, 1.1, epsilon = 0.01);
}
#[test]
fn test_directional_variogram() {
let coords = array![
[0.0, 0.0],
[1.0, 0.0],
[2.0, 0.0],
[0.0, 1.0],
[1.0, 1.0],
[2.0, 1.0]
];
let values = array![1.0, 1.5, 2.0, 1.2, 1.7, 2.2];
let result = directional_variogram(
&coords.view(),
&values.view(),
0.0,
std::f64::consts::PI / 4.0, 5,
);
assert!(result.is_ok());
let (lags, gamma) = result.expect("computation failed");
assert!(!lags.is_empty());
}
#[test]
fn test_variogram_models() {
let models = vec![
VariogramModel::Spherical,
VariogramModel::Exponential,
VariogramModel::Gaussian,
VariogramModel::Linear,
];
for model in models {
let fitted = FittedVariogram {
model,
range: 1.0,
sill: 1.0,
nugget: 0.0,
extra_params: vec![],
r_squared: 0.0,
};
let gamma_1 = fitted.evaluate(0.5);
let gamma_2 = fitted.evaluate(1.0);
assert!(gamma_2 >= gamma_1);
}
}
}