use super::OrdinaryKriging;
use crate::variogram::VariogramModel;
use crate::{GeostatError, GeostatResult};
use nalgebra as na;
use std::sync::Arc;
use rayon::prelude::*;
#[derive(Debug, Clone)]
pub struct UniversalKrigingResult {
pub prediction: f64,
pub variance: f64,
pub std_error: f64,
pub ci_lower: f64,
pub ci_upper: f64,
}
#[derive(Debug)]
pub struct UniversalKriging {
training_coords: Vec<(f64, f64)>,
#[allow(dead_code)]
training_values: Vec<f64>,
variogram: VariogramModel,
trend_degree: usize,
trend_coefficients: Vec<f64>,
residuals: Vec<f64>,
ordinary_kriging: Arc<OrdinaryKriging>,
}
impl UniversalKriging {
pub fn new(
training_coords: Vec<(f64, f64)>,
training_values: Vec<f64>,
variogram: VariogramModel,
trend_degree: usize,
) -> GeostatResult<Self> {
if training_coords.len() < 4 {
return Err(GeostatError::InsufficientData(format!(
"Minimum 4 training points required; got {}",
training_coords.len()
)));
}
if training_coords.len() != training_values.len() {
return Err(GeostatError::InvalidParameters(format!(
"Coordinate/value length mismatch: {} coords, {} values",
training_coords.len(),
training_values.len()
)));
}
if trend_degree > 2 {
return Err(GeostatError::InvalidParameters(format!(
"Trend degree must be 0, 1, or 2; got {}",
trend_degree
)));
}
let _n = training_coords.len();
let trend_coefficients = Self::fit_polynomial(&training_coords, &training_values, trend_degree)?;
let residuals = Self::compute_residuals(
&training_coords,
&training_values,
&trend_coefficients,
trend_degree,
);
let ordinary_kriging = Arc::new(OrdinaryKriging::new(
training_coords.clone(),
residuals.clone(),
variogram.clone(),
)?);
Ok(UniversalKriging {
training_coords,
training_values,
variogram,
trend_degree,
trend_coefficients,
residuals,
ordinary_kriging,
})
}
pub fn predict(&self, x: f64, y: f64) -> GeostatResult<UniversalKrigingResult> {
let trend_pred =
Self::evaluate_polynomial(&[x, y], &self.trend_coefficients, self.trend_degree);
let residual_prediction = self.ordinary_kriging.predict((x, y))?;
let prediction = trend_pred + residual_prediction.prediction;
let variance = residual_prediction.variance;
let std_error = variance.sqrt();
Ok(UniversalKrigingResult {
prediction,
variance,
std_error,
ci_lower: prediction - 1.96 * std_error,
ci_upper: prediction + 1.96 * std_error,
})
}
pub fn predict_batch(
&self,
coords: Vec<(f64, f64)>,
) -> GeostatResult<Vec<UniversalKrigingResult>> {
if coords.is_empty() {
return Err(GeostatError::InvalidParameters(
"Empty coordinate array".to_string(),
));
}
coords
.par_iter()
.map(|(x, y)| self.predict(*x, *y))
.collect()
}
pub fn n_training(&self) -> usize {
self.training_coords.len()
}
pub fn trend_degree(&self) -> usize {
self.trend_degree
}
pub fn trend_coefficients(&self) -> &[f64] {
&self.trend_coefficients
}
pub fn variogram(&self) -> &VariogramModel {
&self.variogram
}
pub fn residuals(&self) -> &[f64] {
&self.residuals
}
fn fit_polynomial(
coords: &[(f64, f64)],
values: &[f64],
degree: usize,
) -> GeostatResult<Vec<f64>> {
let n = coords.len();
let num_coeffs = match degree {
0 => 1, 1 => 3, 2 => 6, _ => return Err(GeostatError::InvalidParameters(
"Degree must be 0, 1, or 2".to_string()
)),
};
let mut x_data = vec![];
for (xi, yi) in coords {
let mut row = vec![1.0]; if degree >= 1 {
row.push(*xi);
row.push(*yi);
}
if degree >= 2 {
row.push(xi * xi);
row.push(yi * yi);
row.push(xi * yi);
}
x_data.extend(row);
}
let x = na::DMatrix::from_row_slice(n, num_coeffs, &x_data);
let y = na::DVector::from_vec(values.to_vec());
let xt_x = x.transpose() * &x;
let xt_y = x.transpose() * y;
let coeffs = match xt_x.clone().lu().solve(&xt_y) {
Some(beta) => beta.as_slice().to_vec(),
None => {
let svd = xt_x.svd(true, true);
svd.solve(&xt_y, 1e-10)
.map_err(|_| GeostatError::KrigingSolveFailed(
"Singular matrix in trend fitting".to_string()
))?
.as_slice()
.to_vec()
}
};
Ok(coeffs)
}
fn evaluate_polynomial(xy: &[f64], coeffs: &[f64], degree: usize) -> f64 {
let mut result = coeffs[0];
if degree >= 1 && xy.len() >= 2 {
result += coeffs[1] * xy[0]; result += coeffs[2] * xy[1]; }
if degree >= 2 && xy.len() >= 2 && coeffs.len() >= 6 {
result += coeffs[3] * xy[0] * xy[0]; result += coeffs[4] * xy[1] * xy[1]; result += coeffs[5] * xy[0] * xy[1]; }
result
}
fn compute_residuals(
coords: &[(f64, f64)],
values: &[f64],
coeffs: &[f64],
degree: usize,
) -> Vec<f64> {
coords
.iter()
.zip(values.iter())
.map(|((x, y), z)| {
let trend = Self::evaluate_polynomial(&[*x, *y], coeffs, degree);
z - trend
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::variogram::VariogramFitter;
#[test]
fn test_universal_kriging_creation() {
let coords = vec![(0.0, 0.0), (1.0, 1.0), (2.0, 0.5), (1.5, 2.0)];
let values = vec![1.0, 2.5, 2.0, 3.5]; let vario = VariogramModel {
family: crate::variogram::VariogramModelFamily::Spherical,
nugget: 0.0,
partial_sill: 0.5,
range: 3.0,
wrss: 0.01,
condition_number: 10.0,
};
let uk = UniversalKriging::new(coords, values, vario, 1);
assert!(uk.is_ok());
assert_eq!(uk.unwrap().n_training(), 4);
}
#[test]
fn test_universal_kriging_insufficient_points() {
let coords = vec![(0.0, 0.0), (1.0, 1.0), (2.0, 0.5)];
let values = vec![1.0, 2.5, 2.0];
let vario = VariogramModel {
family: crate::variogram::VariogramModelFamily::Spherical,
nugget: 0.0,
partial_sill: 0.5,
range: 3.0,
wrss: 0.01,
condition_number: 10.0,
};
let uk = UniversalKriging::new(coords, values, vario, 1);
assert!(uk.is_err());
}
#[test]
fn test_universal_kriging_mismatch() {
let coords = vec![(0.0, 0.0), (1.0, 1.0), (2.0, 0.5), (1.5, 2.0)];
let values = vec![1.0, 2.5, 2.0]; let vario = VariogramModel {
family: crate::variogram::VariogramModelFamily::Spherical,
nugget: 0.0,
partial_sill: 0.5,
range: 3.0,
wrss: 0.01,
condition_number: 10.0,
};
let uk = UniversalKriging::new(coords, values, vario, 1);
assert!(uk.is_err());
}
#[test]
fn test_universal_kriging_degree_validation() {
let coords = vec![(0.0, 0.0), (1.0, 1.0), (2.0, 0.5), (1.5, 2.0)];
let values = vec![1.0, 2.5, 2.0, 3.5];
let vario = VariogramModel {
family: crate::variogram::VariogramModelFamily::Spherical,
nugget: 0.0,
partial_sill: 0.5,
range: 3.0,
wrss: 0.01,
condition_number: 10.0,
};
let uk = UniversalKriging::new(coords, values, vario, 3); assert!(uk.is_err());
}
#[test]
fn test_universal_kriging_prediction() {
let coords = vec![(0.0, 0.0), (1.0, 1.0), (2.0, 0.5), (1.5, 2.0)];
let values = vec![1.0, 2.0, 2.2, 3.0];
let vario = VariogramModel {
family: crate::variogram::VariogramModelFamily::Spherical,
nugget: 0.0,
partial_sill: 0.3,
range: 2.5,
wrss: 0.01,
condition_number: 10.0,
};
let uk = UniversalKriging::new(coords, values, vario, 1).unwrap();
let result = uk.predict(0.5, 0.5);
assert!(result.is_ok());
let res = result.unwrap();
assert!(res.prediction.is_finite());
assert!(res.variance >= 0.0);
assert!(res.std_error >= 0.0);
assert!(res.ci_lower <= res.prediction);
assert!(res.prediction <= res.ci_upper);
}
#[test]
fn test_universal_kriging_trend_removal() {
let coords = vec![(0.0, 0.0), (1.0, 0.0), (0.0, 1.0), (1.0, 1.0)];
let values = vec![
1.0,
1.0 + 0.5,
1.0 + 0.3,
1.0 + 0.5 + 0.3,
];
let vario = VariogramModel {
family: crate::variogram::VariogramModelFamily::Spherical,
nugget: 0.0,
partial_sill: 0.01,
range: 10.0,
wrss: 0.01,
condition_number: 10.0,
};
let uk = UniversalKriging::new(coords, values, vario, 1).unwrap();
let coeffs = uk.trend_coefficients();
assert!((coeffs[0] - 1.0).abs() < 0.01);
assert!((coeffs[1] - 0.5).abs() < 0.01);
assert!((coeffs[2] - 0.3).abs() < 0.01);
}
#[test]
fn test_universal_kriging_batch_prediction() {
let coords = vec![(0.0, 0.0), (1.0, 1.0), (2.0, 0.5), (1.5, 2.0)];
let values = vec![1.0, 2.0, 2.2, 3.0];
let vario = VariogramModel {
family: crate::variogram::VariogramModelFamily::Spherical,
nugget: 0.0,
partial_sill: 0.3,
range: 2.5,
wrss: 0.01,
condition_number: 10.0,
};
let uk = UniversalKriging::new(coords, values, vario, 1).unwrap();
let test_coords = vec![(0.5, 0.5), (1.0, 0.0), (0.0, 1.5)];
let results = uk.predict_batch(test_coords);
assert!(results.is_ok());
let res = results.unwrap();
assert_eq!(res.len(), 3);
for r in res {
assert!(r.prediction.is_finite());
assert!(r.variance >= 0.0);
}
}
#[test]
fn test_universal_kriging_zero_degree() {
let coords = vec![(0.0, 0.0), (1.0, 1.0), (2.0, 0.5), (1.5, 2.0)];
let values = vec![1.5, 1.5, 1.5, 1.5]; let vario = VariogramModel {
family: crate::variogram::VariogramModelFamily::Spherical,
nugget: 0.0,
partial_sill: 0.1,
range: 3.0,
wrss: 0.01,
condition_number: 10.0,
};
let uk = UniversalKriging::new(coords, values, vario, 0).unwrap();
let result = uk.predict(0.5, 0.5);
assert!(result.is_ok());
let res = result.unwrap();
assert!((res.prediction - 1.5).abs() < 0.5);
}
#[test]
fn test_universal_kriging_two_degree() {
let coords = vec![(0.0, 0.0), (1.0, 0.0), (0.0, 1.0), (1.0, 1.0)];
let values = vec![
1.0,
1.0 + 0.1,
1.0 + 0.2,
1.0 + 0.1 + 0.2,
];
let vario = VariogramModel {
family: crate::variogram::VariogramModelFamily::Spherical,
nugget: 0.0,
partial_sill: 0.01,
range: 10.0,
wrss: 0.01,
condition_number: 10.0,
};
let uk = UniversalKriging::new(coords, values, vario, 2).unwrap();
let result = uk.predict(0.5, 0.5);
assert!(result.is_ok());
}
#[test]
fn test_universal_kriging_residuals() {
let coords = vec![(0.0, 0.0), (1.0, 1.0), (2.0, 0.5), (1.5, 2.0)];
let values = vec![1.0, 2.0, 2.2, 3.0];
let vario = VariogramModel {
family: crate::variogram::VariogramModelFamily::Spherical,
nugget: 0.0,
partial_sill: 0.3,
range: 2.5,
wrss: 0.01,
condition_number: 10.0,
};
let uk = UniversalKriging::new(coords, values, vario, 1).unwrap();
let residuals = uk.residuals();
assert_eq!(residuals.len(), 4);
let mean: f64 = residuals.iter().sum::<f64>() / residuals.len() as f64;
assert!(mean.abs() < 0.1);
}
}