use crate::advanced::enhanced_kriging::AnisotropicCovariance;
use crate::error::InterpolateResult;
use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2};
use scirs2_core::numeric::{Float, FromPrimitive};
use std::fmt::{Debug, Display};
use std::ops::{Add, Div, Mul, Sub};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum VariogramModel {
Spherical,
Exponential,
Gaussian,
Matern(f64),
Power(f64),
}
#[derive(Debug, Clone)]
pub struct VariogramBin<F: Float> {
pub distance: F,
pub semivariance: F,
pub count: usize,
}
#[allow(dead_code)]
pub fn compute_empirical_variogram<F>(
points: &ArrayView2<F>,
values: &ArrayView1<F>,
n_bins: usize,
max_distance: Option<F>,
) -> InterpolateResult<Vec<VariogramBin<F>>>
where
F: Float + FromPrimitive + Debug + Display,
{
let n_points = points.shape()[0];
let n_dims = points.shape()[1];
if n_points != values.len() {
return Err(crate::error::InterpolateError::DimensionMismatch(
"Number of points must match number of values".to_string(),
));
}
if n_points < 2 {
return Err(crate::error::InterpolateError::InvalidValue(
"At least 2 points are required for variogram estimation".to_string(),
));
}
let max_dist = match max_distance {
Some(dist) => dist,
None => {
let mut max_d = F::zero();
for i in 0..n_points {
for j in (i + 1)..n_points {
let mut dist_sq = F::zero();
for d in 0..n_dims {
let diff = points[[i, d]] - points[[j, d]];
dist_sq = dist_sq + diff * diff;
}
let dist = dist_sq.sqrt();
if dist > max_d {
max_d = dist;
}
}
}
max_d
}
};
let bin_width = max_dist / F::from_usize(n_bins).expect("Operation failed");
let mut _bins = vec![
VariogramBin {
_distance: F::zero(),
semivariance: F::zero(),
count: 0,
};
n_bins
];
for i in 0..n_bins {
bins[i]._distance = F::from_usize(i).expect("Operation failed") * bin_width + bin_width / F::from(2).expect("Failed to convert constant to float");
}
for i in 0..n_points {
for j in (i + 1)..n_points {
let mut dist_sq = F::zero();
for d in 0..n_dims {
let diff = points[[i, d]] - points[[j, d]];
dist_sq = dist_sq + diff * diff;
}
let dist = dist_sq.sqrt();
let value_diff = values[i] - values[j];
let semivariogram_value = value_diff * value_diff / F::from(2).expect("Failed to convert constant to float");
let bin_idx = (dist / bin_width).to_usize().unwrap_or(n_bins - 1);
if bin_idx < n_bins {
bins[bin_idx].semivariance = bins[bin_idx].semivariance + semivariogram_value;
bins[bin_idx].count += 1;
}
}
}
for bin in &mut _bins {
if bin.count > 0 {
bin.semivariance = bin.semivariance / F::from_usize(bin.count).expect("Operation failed");
}
}
let valid_bins: Vec<VariogramBin<F>> = bins.into_iter().filter(|bin| bin.count > 0).collect();
if valid_bins.is_empty() {
return Err(crate::error::InterpolateError::ComputationError(
"No valid _bins found for variogram estimation".to_string(),
));
}
Ok(valid_bins)
}
#[allow(dead_code)]
pub fn fit_variogram_model<F>(
bins: &[VariogramBin<F>],
model: VariogramModel,
) -> InterpolateResult<(F, F, F)>
where
F: Float + FromPrimitive + Debug + Display + 'static,
{
if bins.is_empty() {
return Err(crate::error::InterpolateError::InvalidValue(
"Cannot fit variogram model to empty bins".to_string(),
));
}
#[cfg(feature = "linalg")]
{
use ndarray_linalg::LeastSquaresSvd;
let max_semivariance = bins
.iter()
.map(|bin| bin.semivariance)
.fold(F::zero(), |a, b| if a > b { a } else { b });
let max_distance =
bins.iter()
.map(|bin| bin.distance)
.fold(F::zero(), |a, b| if a > b { a } else { b });
let mut nugget = F::from_f64(0.001).expect("Operation failed") * max_semivariance;
let mut sill = max_semivariance - nugget;
let mut range = max_distance / F::from_f64(3.0).expect("Operation failed");
let n_bins = bins.len();
let mut a = Array2::<f64>::zeros((n_bins, 3));
let mut b = Array1::<f64>::zeros(n_bins);
for i in 0..n_bins {
let h = bins[i].distance.to_f64().expect("Operation failed");
let gamma = bins[i].semivariance.to_f64().expect("Operation failed");
a[[i, 0]] = 1.0;
let model_val = match model {
VariogramModel::Spherical => {
let range_val = range.to_f64().expect("Operation failed");
if h <= range_val {
1.5 * (h / range_val) - 0.5 * (h / range_val).powi(3)
} else {
1.0
}
}
VariogramModel::Exponential => {
let range_val = range.to_f64().expect("Operation failed");
1.0 - (-3.0 * h / range_val).exp()
}
VariogramModel::Gaussian => {
let range_val = range.to_f64().expect("Operation failed");
1.0 - (-3.0 * (h / range_val).powi(2)).exp()
}
VariogramModel::Matern(nu) => {
let range_val = range.to_f64().expect("Operation failed");
if h <= 1e-6 {
0.0
} else {
1.0 - (-3.0 * h / range_val).powf(nu).exp()
}
}
VariogramModel::Power(exponent) => {
let range_val = range.to_f64().expect("Operation failed");
(h / range_val).powf(exponent)
}
};
a[[i, 1]] = model_val; a[[i, 2]] = h; b[i] = gamma;
}
match a.least_squares(&b) {
Ok(solution) => {
nugget = F::from_f64(solution[0]).expect("Operation failed");
sill = F::from_f64(solution[1]).expect("Operation failed");
range = F::from_f64(solution[2]).expect("Operation failed");
if nugget < F::zero() {
nugget = F::from_f64(0.001).expect("Operation failed") * max_semivariance;
}
if sill < F::zero() {
sill = max_semivariance - nugget;
}
if range < F::zero() {
range = max_distance / F::from_f64(3.0).expect("Operation failed");
}
Ok((nugget, sill, range))
}
Err(_) => {
Ok((nugget, sill, range))
}
}
}
#[cfg(not(feature = "linalg"))]
{
let max_semivariance = bins
.iter()
.map(|bin| bin.semivariance)
.fold(F::zero(), |a, b| if a > b { a } else { b });
let max_distance =
bins.iter()
.map(|bin| bin.distance)
.fold(F::zero(), |a, b| if a > b { a } else { b });
let mut nugget = if !bins.is_empty() {
let min_dist_bin = bins
.iter()
.min_by(|a, b| a.distance.partial_cmp(&b.distance).expect("Operation failed"))
.expect("Operation failed");
min_dist_bin.semivariance
} else {
F::from_f64(0.05).expect("Operation failed") * max_semivariance
};
if nugget <= F::zero() || nugget >= max_semivariance {
nugget = F::from_f64(0.05).expect("Operation failed") * max_semivariance;
}
let sill = max_semivariance - nugget;
let range = max_distance / F::from_f64(3.0).expect("Operation failed");
Ok((nugget, sill, range))
}
}
#[allow(dead_code)]
pub fn variogram_to_covariance<F>(
nugget: F,
sill: F,
range: F,
model: VariogramModel,
) -> AnisotropicCovariance<F>
where
F: Float + FromPrimitive + Debug + Display,
{
use crate::advanced::kriging::CovarianceFunction;
let (cov_fn, extra_params) = match model {
VariogramModel::Spherical => (CovarianceFunction::Matern52, F::zero()),
VariogramModel::Exponential => (CovarianceFunction::Exponential, F::zero()),
VariogramModel::Gaussian => (CovarianceFunction::SquaredExponential, F::zero()),
VariogramModel::Matern(nu) => {
if nu < 1.0 {
(CovarianceFunction::Exponential, F::zero())
} else if nu < 2.0 {
(CovarianceFunction::Matern32, F::zero())
} else {
(CovarianceFunction::Matern52, F::zero())
}
}
VariogramModel::Power(_) => (
CovarianceFunction::RationalQuadratic,
F::from_f64(0.5).expect("Operation failed"),
),
};
let length_scales = vec![range];
AnisotropicCovariance {
cov_fn,
length_scales,
sigma_sq: sill,
nugget,
extra_params,
}
}