use crate::kernels::Kernel;
use crate::utils;
use scirs2_core::ndarray::{s, Array1, Array2, ArrayView1, Axis};
use sklears_core::error::{Result as SklResult, SklearsError};
use sklears_core::traits::{Estimator, Fit, Predict};
use std::f64::consts::PI;
#[derive(Debug, Clone)]
pub struct Untrained;
#[derive(Debug, Clone)]
pub struct Trained {
pub spatial_kernel: SpatialKernel,
pub kriging_type: KrigingType,
pub training_data: (Array2<f64>, Array1<f64>),
pub alpha: Array1<f64>,
pub cholesky: Array2<f64>,
pub log_likelihood: f64,
pub variogram: Option<Variogram>,
pub anisotropy_matrix: Option<Array2<f64>>,
}
#[derive(Debug, Clone, Copy)]
pub enum KrigingType {
Simple { mean: f64 },
Ordinary,
Universal,
CoKriging,
}
#[derive(Debug, Clone)]
pub enum SpatialKernel {
Spherical {
sill: f64, range: f64, nugget: f64, },
Exponential { sill: f64, range: f64, nugget: f64 },
Gaussian { sill: f64, range: f64, nugget: f64 },
Matern {
sill: f64,
range: f64,
nugget: f64,
nu: f64, },
Power {
scale: f64,
exponent: f64, },
Linear { slope: f64, nugget: f64 },
HoleEffect {
sill: f64,
range: f64,
nugget: f64,
damping: f64,
},
Anisotropic {
base_kernel: Box<SpatialKernel>,
anisotropy_matrix: Array2<f64>,
},
}
impl SpatialKernel {
pub fn spherical(sill: f64, range: f64) -> Self {
Self::Spherical {
sill,
range,
nugget: 0.0,
}
}
pub fn spherical_with_nugget(sill: f64, range: f64, nugget: f64) -> Self {
Self::Spherical {
sill,
range,
nugget,
}
}
pub fn exponential(sill: f64, range: f64) -> Self {
Self::Exponential {
sill,
range,
nugget: 0.0,
}
}
pub fn gaussian(sill: f64, range: f64) -> Self {
Self::Gaussian {
sill,
range,
nugget: 0.0,
}
}
pub fn matern(sill: f64, range: f64, nu: f64) -> Self {
Self::Matern {
sill,
range,
nugget: 0.0,
nu,
}
}
pub fn anisotropic(base_kernel: SpatialKernel, anisotropy_matrix: Array2<f64>) -> Self {
Self::Anisotropic {
base_kernel: Box::new(base_kernel),
anisotropy_matrix,
}
}
fn compute_distance(&self, x1: &[f64], x2: &[f64]) -> f64 {
match self {
Self::Anisotropic {
anisotropy_matrix, ..
} => {
let diff: Array1<f64> =
Array1::from_vec(x1.iter().zip(x2.iter()).map(|(a, b)| a - b).collect());
let transformed = anisotropy_matrix.dot(&diff);
transformed.iter().map(|x| x.powi(2)).sum::<f64>().sqrt()
}
_ => {
x1.iter()
.zip(x2.iter())
.map(|(a, b)| (a - b).powi(2))
.sum::<f64>()
.sqrt()
}
}
}
pub fn compute_covariance(&self, distance: f64) -> f64 {
match self {
Self::Spherical {
sill,
range,
nugget,
} => {
if distance == 0.0 {
sill + nugget
} else if distance <= *range {
let h_r = distance / range;
sill * (1.0 - 1.5 * h_r + 0.5 * h_r.powi(3))
+ if distance == 0.0 { *nugget } else { 0.0 }
} else {
if distance == 0.0 {
*nugget
} else {
0.0
}
}
}
Self::Exponential {
sill,
range,
nugget,
} => {
if distance == 0.0 {
sill + nugget
} else {
sill * (-3.0 * distance / range).exp()
}
}
Self::Gaussian {
sill,
range,
nugget,
} => {
if distance == 0.0 {
sill + nugget
} else {
sill * (-3.0 * (distance / range).powi(2)).exp()
}
}
Self::Matern {
sill,
range,
nugget,
nu,
} => {
if distance == 0.0 {
return sill + nugget;
}
let h = distance / range;
match nu {
0.5 => sill * (-h).exp(),
1.5 => sill * (1.0 + h * 3.0_f64.sqrt()) * (-h * 3.0_f64.sqrt()).exp(),
2.5 => {
sill * (1.0 + h * 5.0_f64.sqrt() + 5.0 * h.powi(2) / 3.0)
* (-h * 5.0_f64.sqrt()).exp()
}
_ => {
sill * (1.0 + h).exp() * (-h).exp()
}
}
}
Self::Power { scale, exponent } => {
if distance == 0.0 {
0.0
} else {
scale * distance.powf(*exponent)
}
}
Self::Linear { slope, nugget } => {
if distance == 0.0 {
*nugget
} else {
slope * distance
}
}
Self::HoleEffect {
sill,
range,
nugget,
damping,
} => {
if distance == 0.0 {
sill + nugget
} else {
let h_r = distance / range;
sill * (1.0 - h_r) * (-damping * h_r).exp().max(0.0)
}
}
Self::Anisotropic { base_kernel, .. } => {
base_kernel.compute_covariance(distance)
}
}
}
pub fn compute_variogram(&self, distance: f64) -> f64 {
match self {
Self::Spherical {
sill,
range,
nugget,
} => {
if distance == 0.0 {
0.0
} else if distance <= *range {
let h_r = distance / range;
nugget + sill * (1.5 * h_r - 0.5 * h_r.powi(3))
} else {
nugget + sill
}
}
Self::Exponential {
sill,
range,
nugget,
} => {
if distance == 0.0 {
0.0
} else {
nugget + sill * (1.0 - (-3.0 * distance / range).exp())
}
}
Self::Gaussian {
sill,
range,
nugget,
} => {
if distance == 0.0 {
0.0
} else {
nugget + sill * (1.0 - (-3.0 * (distance / range).powi(2)).exp())
}
}
_ => {
let c0 = self.compute_covariance(0.0);
let ch = self.compute_covariance(distance);
(c0 - ch).max(0.0)
}
}
}
pub fn effective_range(&self) -> f64 {
match self {
Self::Spherical { range, .. }
| Self::Exponential { range, .. }
| Self::Gaussian { range, .. }
| Self::Matern { range, .. }
| Self::HoleEffect { range, .. } => *range,
Self::Power { .. } => f64::INFINITY,
Self::Linear { .. } => f64::INFINITY,
Self::Anisotropic { base_kernel, .. } => base_kernel.effective_range(),
}
}
pub fn sill(&self) -> f64 {
match self {
Self::Spherical { sill, .. }
| Self::Exponential { sill, .. }
| Self::Gaussian { sill, .. }
| Self::Matern { sill, .. }
| Self::HoleEffect { sill, .. } => *sill,
Self::Power { scale, .. } => *scale,
Self::Linear { slope, .. } => *slope,
Self::Anisotropic { base_kernel, .. } => base_kernel.sill(),
}
}
pub fn nugget(&self) -> f64 {
match self {
Self::Spherical { nugget, .. }
| Self::Exponential { nugget, .. }
| Self::Gaussian { nugget, .. }
| Self::Matern { nugget, .. }
| Self::HoleEffect { nugget, .. }
| Self::Linear { nugget, .. } => *nugget,
Self::Power { .. } => 0.0,
Self::Anisotropic { base_kernel, .. } => base_kernel.nugget(),
}
}
}
#[allow(non_snake_case)]
impl Kernel for SpatialKernel {
fn compute_kernel_matrix(
&self,
x1: &Array2<f64>,
x2: Option<&Array2<f64>>,
) -> SklResult<Array2<f64>> {
let x2 = x2.unwrap_or(x1);
let n1 = x1.nrows();
let n2 = x2.nrows();
if x1.ncols() < 2 || x2.ncols() < 2 {
return Err(SklearsError::InvalidInput(
"Spatial kernels require at least 2D coordinates".to_string(),
));
}
let mut K = Array2::zeros((n1, n2));
for i in 0..n1 {
for j in 0..n2 {
let x1_coords = x1.row(i).to_vec();
let x2_coords = x2.row(j).to_vec();
let distance = self.compute_distance(&x1_coords, &x2_coords);
K[[i, j]] = self.compute_covariance(distance);
}
}
Ok(K)
}
fn clone_box(&self) -> Box<dyn Kernel> {
Box::new(self.clone())
}
fn kernel(&self, x1: &ArrayView1<f64>, x2: &ArrayView1<f64>) -> f64 {
if x1.len() < 2 || x2.len() < 2 {
return 0.0; }
let distance = self.compute_distance(&x1.to_vec(), &x2.to_vec());
self.compute_covariance(distance)
}
fn get_params(&self) -> Vec<f64> {
match self {
Self::Spherical {
range,
sill,
nugget,
} => vec![*range, *sill, *nugget],
Self::Exponential {
range,
sill,
nugget,
} => vec![*range, *sill, *nugget],
Self::Gaussian {
range,
sill,
nugget,
} => vec![*range, *sill, *nugget],
Self::Matern {
range,
sill,
nugget,
nu,
} => vec![*range, *sill, *nugget, *nu],
Self::Linear { slope, nugget } => vec![*slope, *nugget],
Self::Power { scale, exponent } => vec![*scale, *exponent],
Self::HoleEffect {
range,
sill,
nugget,
damping,
} => vec![*range, *sill, *nugget, *damping],
Self::Anisotropic {
base_kernel,
anisotropy_matrix,
} => {
let mut params = base_kernel.get_params();
for row in anisotropy_matrix.rows() {
for &val in row {
params.push(val);
}
}
params
}
}
}
fn set_params(&mut self, params: &[f64]) -> SklResult<()> {
match self {
Self::Spherical {
range,
sill,
nugget,
} => {
if params.len() != 3 {
return Err(SklearsError::InvalidInput(
"Spherical kernel requires 3 parameters".to_string(),
));
}
*range = params[0];
*sill = params[1];
*nugget = params[2];
}
Self::Exponential {
range,
sill,
nugget,
} => {
if params.len() != 3 {
return Err(SklearsError::InvalidInput(
"Exponential kernel requires 3 parameters".to_string(),
));
}
*range = params[0];
*sill = params[1];
*nugget = params[2];
}
Self::Gaussian {
range,
sill,
nugget,
} => {
if params.len() != 3 {
return Err(SklearsError::InvalidInput(
"Gaussian kernel requires 3 parameters".to_string(),
));
}
*range = params[0];
*sill = params[1];
*nugget = params[2];
}
Self::Matern {
range,
sill,
nugget,
nu,
} => {
if params.len() != 4 {
return Err(SklearsError::InvalidInput(
"Matern kernel requires 4 parameters".to_string(),
));
}
*range = params[0];
*sill = params[1];
*nugget = params[2];
*nu = params[3];
}
Self::Linear { slope, nugget } => {
if params.len() != 2 {
return Err(SklearsError::InvalidInput(
"Linear kernel requires 2 parameters".to_string(),
));
}
*slope = params[0];
*nugget = params[1];
}
Self::Power { scale, exponent } => {
if params.len() != 2 {
return Err(SklearsError::InvalidInput(
"Power kernel requires 2 parameters".to_string(),
));
}
*scale = params[0];
*exponent = params[1];
}
Self::HoleEffect {
range,
sill,
nugget,
damping,
} => {
if params.len() != 4 {
return Err(SklearsError::InvalidInput(
"HoleEffect kernel requires 4 parameters".to_string(),
));
}
*range = params[0];
*sill = params[1];
*nugget = params[2];
*damping = params[3];
}
Self::Anisotropic {
base_kernel,
anisotropy_matrix,
} => {
let base_params_len = base_kernel.get_params().len();
if params.len() < base_params_len {
return Err(SklearsError::InvalidInput(
"Not enough parameters for anisotropic kernel".to_string(),
));
}
base_kernel.set_params(¶ms[..base_params_len])?;
let matrix_size = anisotropy_matrix.nrows() * anisotropy_matrix.ncols();
if params.len() != base_params_len + matrix_size {
return Err(SklearsError::InvalidInput(
"Incorrect number of parameters for anisotropy matrix".to_string(),
));
}
let matrix_params = ¶ms[base_params_len..];
let ncols = anisotropy_matrix.ncols();
for (i, mut row) in anisotropy_matrix.rows_mut().into_iter().enumerate() {
for (j, val) in row.iter_mut().enumerate() {
*val = matrix_params[i * ncols + j];
}
}
}
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct Variogram {
pub distances: Array1<f64>,
pub semivariances: Array1<f64>,
pub n_pairs: Array1<usize>,
pub fitted_model: Option<SpatialKernel>,
}
impl Variogram {
pub fn compute_empirical(
coords: &Array2<f64>,
values: &Array1<f64>,
n_bins: usize,
) -> SklResult<Self> {
let n_points = coords.nrows();
if coords.nrows() != values.len() {
return Err(SklearsError::DimensionMismatch {
expected: coords.nrows(),
actual: values.len(),
});
}
let mut all_distances = Vec::new();
let mut all_semivariances = Vec::new();
for i in 0..n_points {
for j in i + 1..n_points {
let dist = coords
.row(i)
.iter()
.zip(coords.row(j).iter())
.map(|(a, b)| (a - b).powi(2))
.sum::<f64>()
.sqrt();
let semivar = 0.5 * (values[i] - values[j]).powi(2);
all_distances.push(dist);
all_semivariances.push(semivar);
}
}
if all_distances.is_empty() {
return Err(SklearsError::InvalidInput(
"Not enough data points for variogram".to_string(),
));
}
let max_dist = all_distances.iter().fold(0.0f64, |a, &b| a.max(b));
let min_dist = all_distances.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let bin_width = (max_dist - min_dist) / n_bins as f64;
let mut bin_distances: Array1<f64> = Array1::zeros(n_bins);
let mut bin_semivariances: Array1<f64> = Array1::zeros(n_bins);
let mut bin_counts: Array1<f64> = Array1::zeros(n_bins);
for (dist, semivar) in all_distances.iter().zip(all_semivariances.iter()) {
let bin_idx = ((*dist - min_dist) / bin_width).floor() as usize;
let bin_idx = bin_idx.min(n_bins - 1);
bin_distances[bin_idx] += dist;
bin_semivariances[bin_idx] += semivar;
bin_counts[bin_idx] += 1.0;
}
let mut distances = Vec::new();
let mut semivariances = Vec::new();
let mut n_pairs = Vec::new();
for i in 0..n_bins {
if bin_counts[i] > 0.0 {
distances.push(bin_distances[i] / bin_counts[i] as f64);
semivariances.push(bin_semivariances[i] / bin_counts[i] as f64);
n_pairs.push(bin_counts[i] as usize);
}
}
Ok(Self {
distances: Array1::from_vec(distances),
semivariances: Array1::from_vec(semivariances),
n_pairs: Array1::from_vec(n_pairs),
fitted_model: None,
})
}
pub fn fit_model(&mut self, model_type: &str) -> SklResult<()> {
if self.distances.is_empty() {
return Err(SklearsError::InvalidInput(
"No variogram data to fit".to_string(),
));
}
let max_semivar = self.semivariances.iter().fold(0.0f64, |a, &b| a.max(b));
let max_dist = self.distances.iter().fold(0.0f64, |a, &b| a.max(b));
let nugget = if self.distances[0] > 0.0 {
0.0
} else {
self.semivariances[0]
};
let sill = max_semivar - nugget;
let target_semivar = nugget + 0.95 * sill;
let mut range = max_dist;
for i in 0..self.distances.len() {
if self.semivariances[i] >= target_semivar {
range = self.distances[i];
break;
}
}
let fitted_model = match model_type {
"spherical" => SpatialKernel::Spherical {
sill,
range,
nugget,
},
"exponential" => SpatialKernel::Exponential {
sill,
range,
nugget,
},
"gaussian" => SpatialKernel::Gaussian {
sill,
range,
nugget,
},
"matern15" => SpatialKernel::Matern {
sill,
range,
nugget,
nu: 1.5,
},
_ => {
return Err(SklearsError::InvalidInput(format!(
"Unknown model type: {}",
model_type
)))
}
};
self.fitted_model = Some(fitted_model);
Ok(())
}
pub fn goodness_of_fit(&self) -> SklResult<f64> {
let model = self
.fitted_model
.as_ref()
.ok_or_else(|| SklearsError::InvalidInput("No model fitted".to_string()))?;
let mut ss_res = 0.0;
let mut ss_tot = 0.0;
let mean_semivar = self.semivariances.mean().unwrap_or(0.0);
for i in 0..self.distances.len() {
let predicted = model.compute_variogram(self.distances[i]);
let observed = self.semivariances[i];
ss_res += (observed - predicted).powi(2);
ss_tot += (observed - mean_semivar).powi(2);
}
let r_squared = 1.0 - (ss_res / ss_tot.max(1e-12));
Ok(r_squared)
}
}
#[derive(Debug, Clone)]
pub struct SpatialGaussianProcessRegressor<S = Untrained> {
spatial_kernel: Option<SpatialKernel>,
kriging_type: KrigingType,
estimate_variogram: bool,
variogram_bins: usize,
anisotropy_matrix: Option<Array2<f64>>,
alpha: f64,
_state: S,
}
#[derive(Debug, Clone)]
pub struct SpatialGPConfig {
pub kriging_type: KrigingType,
pub estimate_variogram: bool,
pub variogram_bins: usize,
pub regularization: f64,
}
impl Default for SpatialGPConfig {
fn default() -> Self {
Self {
kriging_type: KrigingType::Ordinary,
estimate_variogram: false,
variogram_bins: 20,
regularization: 1e-6,
}
}
}
impl Default for SpatialGaussianProcessRegressor<Untrained> {
fn default() -> Self {
Self::new()
}
}
impl SpatialGaussianProcessRegressor<Untrained> {
pub fn new() -> Self {
Self {
spatial_kernel: None,
kriging_type: KrigingType::Ordinary,
estimate_variogram: false,
variogram_bins: 20,
anisotropy_matrix: None,
alpha: 1e-6,
_state: Untrained,
}
}
pub fn builder() -> SpatialGPBuilder {
SpatialGPBuilder::new()
}
pub fn spatial_kernel(mut self, kernel: SpatialKernel) -> Self {
self.spatial_kernel = Some(kernel);
self
}
pub fn kriging_type(mut self, kriging_type: KrigingType) -> Self {
self.kriging_type = kriging_type;
self
}
pub fn estimate_variogram(mut self, estimate: bool) -> Self {
self.estimate_variogram = estimate;
self
}
pub fn anisotropy_matrix(mut self, matrix: Array2<f64>) -> Self {
self.anisotropy_matrix = Some(matrix);
self
}
pub fn alpha(mut self, alpha: f64) -> Self {
self.alpha = alpha;
self
}
}
#[derive(Debug, Clone)]
pub struct SpatialGPBuilder {
kernel: Option<SpatialKernel>,
kriging_type: KrigingType,
estimate_variogram: bool,
variogram_bins: usize,
anisotropy_matrix: Option<Array2<f64>>,
alpha: f64,
}
impl Default for SpatialGPBuilder {
fn default() -> Self {
Self::new()
}
}
impl SpatialGPBuilder {
pub fn new() -> Self {
Self {
kernel: None,
kriging_type: KrigingType::Ordinary,
estimate_variogram: false,
variogram_bins: 20,
anisotropy_matrix: None,
alpha: 1e-6,
}
}
pub fn spatial_kernel(mut self, kernel: SpatialKernel) -> Self {
self.kernel = Some(kernel);
self
}
pub fn kriging_type(mut self, kriging_type: KrigingType) -> Self {
self.kriging_type = kriging_type;
self
}
pub fn estimate_variogram(mut self, estimate: bool) -> Self {
self.estimate_variogram = estimate;
self
}
pub fn variogram_bins(mut self, bins: usize) -> Self {
self.variogram_bins = bins;
self
}
pub fn anisotropy_matrix(mut self, matrix: Array2<f64>) -> Self {
self.anisotropy_matrix = Some(matrix);
self
}
pub fn alpha(mut self, alpha: f64) -> Self {
self.alpha = alpha;
self
}
pub fn build(self) -> SpatialGaussianProcessRegressor<Untrained> {
SpatialGaussianProcessRegressor {
spatial_kernel: self.kernel,
kriging_type: self.kriging_type,
estimate_variogram: self.estimate_variogram,
variogram_bins: self.variogram_bins,
anisotropy_matrix: self.anisotropy_matrix.clone(),
alpha: self.alpha,
_state: Untrained,
}
}
}
impl Estimator for SpatialGaussianProcessRegressor<Untrained> {
type Config = SpatialGPConfig;
type Error = SklearsError;
type Float = f64;
fn config(&self) -> &Self::Config {
static DEFAULT_CONFIG: SpatialGPConfig = SpatialGPConfig {
kriging_type: KrigingType::Ordinary,
estimate_variogram: false,
variogram_bins: 20,
regularization: 1e-6,
};
&DEFAULT_CONFIG
}
}
impl Estimator for SpatialGaussianProcessRegressor<Trained> {
type Config = SpatialGPConfig;
type Error = SklearsError;
type Float = f64;
fn config(&self) -> &Self::Config {
static DEFAULT_CONFIG: SpatialGPConfig = SpatialGPConfig {
kriging_type: KrigingType::Ordinary,
estimate_variogram: false,
variogram_bins: 20,
regularization: 1e-6,
};
&DEFAULT_CONFIG
}
}
#[allow(non_snake_case)]
impl Fit<Array2<f64>, Array1<f64>> for SpatialGaussianProcessRegressor<Untrained> {
type Fitted = SpatialGaussianProcessRegressor<Trained>;
fn fit(self, X: &Array2<f64>, y: &Array1<f64>) -> SklResult<Self::Fitted> {
if X.nrows() != y.len() {
return Err(SklearsError::DimensionMismatch {
expected: X.nrows(),
actual: y.len(),
});
}
if X.ncols() < 2 {
return Err(SklearsError::InvalidInput(
"Spatial GP requires at least 2D coordinates".to_string(),
));
}
let X_owned = X.to_owned();
let y_owned = y.to_owned();
let variogram = if self.estimate_variogram {
Some(Variogram::compute_empirical(
&X_owned,
&y_owned,
self.variogram_bins,
)?)
} else {
None
};
let mut spatial_kernel = self.spatial_kernel.clone().ok_or_else(|| {
SklearsError::InvalidInput("Spatial kernel must be specified".to_string())
})?;
if let Some(anisotropy_matrix) = &self.anisotropy_matrix {
spatial_kernel = SpatialKernel::anisotropic(spatial_kernel, anisotropy_matrix.clone());
}
let K = spatial_kernel.compute_kernel_matrix(&X_owned, None)?;
let (y_for_gp, augmented_matrix) = match self.kriging_type {
KrigingType::Simple { mean } => {
let y_centered = &y_owned - mean;
(y_centered, K.clone())
}
KrigingType::Ordinary => {
let n = K.nrows();
let mut augmented = Array2::zeros((n + 1, n + 1));
for i in 0..n {
for j in 0..n {
augmented[[i, j]] = K[[i, j]];
}
}
for i in 0..n {
augmented[[i, n]] = 1.0;
augmented[[n, i]] = 1.0;
}
(y_owned.clone(), augmented)
}
KrigingType::Universal => {
let n = K.nrows();
let mut augmented = Array2::zeros((n + 1, n + 1));
for i in 0..n {
for j in 0..n {
augmented[[i, j]] = K[[i, j]];
}
}
for i in 0..n {
augmented[[i, n]] = 1.0;
augmented[[n, i]] = 1.0;
}
(y_owned.clone(), augmented)
}
KrigingType::CoKriging => {
let n = K.nrows();
let mut augmented = Array2::zeros((n + 1, n + 1));
for i in 0..n {
for j in 0..n {
augmented[[i, j]] = K[[i, j]];
}
}
for i in 0..n {
augmented[[i, n]] = 1.0;
augmented[[n, i]] = 1.0;
}
(y_owned.clone(), augmented)
}
};
let n_train = K.nrows();
let mut k_block_reg = K.clone();
for i in 0..n_train {
k_block_reg[[i, i]] += self.alpha;
}
let chol_block = utils::robust_cholesky(&k_block_reg)?;
let alpha = match self.kriging_type {
KrigingType::Simple { .. } => {
utils::triangular_solve(&chol_block, &y_for_gp)?
}
_ => {
let mut a = augmented_matrix.clone();
for i in 0..n_train {
a[[i, i]] += self.alpha;
}
let mut rhs = Array1::zeros(n_train + 1);
for i in 0..n_train {
rhs[i] = y_for_gp[i];
}
scirs2_linalg::solve(&a.view(), &rhs.view(), None).map_err(|e| {
SklearsError::NumericalError(format!(
"Failed to solve ordinary/universal kriging system: {e}"
))
})?
}
};
let weights_block = alpha.slice(s![0..n_train]).to_owned();
let log_det = chol_block.diag().iter().map(|x| x.ln()).sum::<f64>() * 2.0;
let data_fit = y_for_gp.dot(&weights_block);
let log_likelihood = -0.5 * (data_fit + log_det + n_train as f64 * (2.0 * PI).ln());
let chol_decomp = chol_block;
Ok(SpatialGaussianProcessRegressor {
spatial_kernel: self.spatial_kernel,
kriging_type: self.kriging_type,
estimate_variogram: self.estimate_variogram,
variogram_bins: self.variogram_bins,
anisotropy_matrix: self.anisotropy_matrix.clone(),
alpha: self.alpha,
_state: Trained {
spatial_kernel,
kriging_type: self.kriging_type,
training_data: (X_owned, y_owned),
alpha,
cholesky: chol_decomp,
log_likelihood,
variogram,
anisotropy_matrix: self.anisotropy_matrix.clone(),
},
})
}
}
#[allow(non_snake_case)]
impl SpatialGaussianProcessRegressor<Trained> {
pub fn trained_state(&self) -> &Trained {
&self._state
}
pub fn variogram(&self) -> Option<&Variogram> {
self._state.variogram.as_ref()
}
pub fn predict_with_variance(&self, X: &Array2<f64>) -> SklResult<(Array1<f64>, Array1<f64>)> {
let n_test = X.nrows();
let n_train = self._state.training_data.0.nrows();
let K_star = self
._state
.spatial_kernel
.compute_kernel_matrix(&self._state.training_data.0, Some(X))?;
let (predictions, variances) = match self._state.kriging_type {
KrigingType::Simple { mean } => {
let pred = K_star.t().dot(&self._state.alpha.slice(s![0..n_train])) + mean;
let K_star_star = self._state.spatial_kernel.compute_kernel_matrix(X, None)?;
let cholesky_slice = self
._state
.cholesky
.slice(s![0..n_train, 0..n_train])
.to_owned();
let v = utils::triangular_solve_matrix(&cholesky_slice, &K_star)?;
let pred_var =
K_star_star.diag().to_owned() - &v.map(|x| x.powi(2)).sum_axis(Axis(0));
(pred, pred_var)
}
_ => {
let mut extended_k_star = Array2::zeros((n_train + 1, n_test));
for i in 0..n_train {
for j in 0..n_test {
extended_k_star[[i, j]] = K_star[[i, j]];
}
}
for j in 0..n_test {
extended_k_star[[n_train, j]] = 1.0;
}
let pred = extended_k_star.t().dot(&self._state.alpha);
let K_star_star = self._state.spatial_kernel.compute_kernel_matrix(X, None)?;
let cholesky_slice = self
._state
.cholesky
.slice(s![0..n_train, 0..n_train])
.to_owned();
let v = utils::triangular_solve_matrix(&cholesky_slice, &K_star)?;
let pred_var =
K_star_star.diag().to_owned() - &v.map(|x| x.powi(2)).sum_axis(Axis(0));
(pred, pred_var.map(|x| x.max(0.0)))
}
};
Ok((predictions, variances.map(|x| x.sqrt())))
}
pub fn correlation_structure(
&self,
max_distance: f64,
n_points: usize,
) -> (Array1<f64>, Array1<f64>) {
let distances = Array1::linspace(0.0, max_distance, n_points);
let correlations = distances.map(|&d| self._state.spatial_kernel.compute_covariance(d));
(distances, correlations)
}
pub fn detect_spatial_outliers(&self, threshold: f64) -> SklResult<Vec<usize>> {
let coords = &self._state.training_data.0;
let targets = &self._state.training_data.1;
let n = coords.nrows();
let n_features = coords.ncols();
let mut outliers = Vec::new();
for i in 0..n {
let mut train_coords = Vec::with_capacity((n - 1) * n_features);
let mut train_values = Vec::with_capacity(n - 1);
for j in 0..n {
if j != i {
train_coords.extend(coords.row(j).iter().copied());
train_values.push(targets[j]);
}
}
let train_coords_array = Array2::from_shape_vec((n - 1, n_features), train_coords)
.map_err(|_| {
SklearsError::InvalidInput(
"Failed to build leave-one-out coordinates".to_string(),
)
})?;
let train_values_array = Array1::from_vec(train_values);
let reduced_gp = SpatialGaussianProcessRegressor::builder()
.spatial_kernel(self._state.spatial_kernel.clone())
.kriging_type(self._state.kriging_type)
.alpha(self.alpha)
.build();
let fitted = reduced_gp.fit(&train_coords_array, &train_values_array)?;
let test_coords = coords.slice(s![i..i + 1, ..]).to_owned();
let (prediction, std_dev) = fitted.predict_with_variance(&test_coords)?;
let residual = targets[i] - prediction[0];
let sigma = std_dev[0];
let sigma = if sigma.is_finite() && sigma > 1e-12 {
sigma
} else {
let scale = targets.std(0.0);
(scale * 1e-6).max(1e-12)
};
let z_score = residual / sigma;
if z_score.is_finite() && z_score.abs() > threshold {
outliers.push(i);
}
}
Ok(outliers)
}
pub fn spatial_cross_validation(&self) -> SklResult<f64> {
let n = self._state.training_data.0.nrows();
let mut errors = Vec::new();
for i in 0..n {
let mut train_coords = Vec::new();
let mut train_values = Vec::new();
for j in 0..n {
if j != i {
train_coords.push(self._state.training_data.0.row(j).to_vec());
train_values.push(self._state.training_data.1[j]);
}
}
let train_coords_array = Array2::from_shape_vec(
(n - 1, self._state.training_data.0.ncols()),
train_coords.into_iter().flatten().collect(),
)
.map_err(|_| {
SklearsError::InvalidInput("Failed to create training coordinates".to_string())
})?;
let train_values_array = Array1::from_vec(train_values);
let reduced_gp = SpatialGaussianProcessRegressor::builder()
.spatial_kernel(self._state.spatial_kernel.clone())
.kriging_type(self._state.kriging_type)
.alpha(self.alpha)
.build();
if let Ok(fitted) = reduced_gp.fit(&train_coords_array, &train_values_array) {
let test_coords = self._state.training_data.0.slice(s![i..i + 1, ..]);
if let Ok(pred) = fitted.predict(&test_coords.to_owned()) {
let error = (pred[0] - self._state.training_data.1[i]).powi(2);
errors.push(error);
}
}
}
if errors.is_empty() {
return Err(SklearsError::InvalidInput(
"Cross-validation failed".to_string(),
));
}
let mse = errors.iter().sum::<f64>() / errors.len() as f64;
Ok(mse.sqrt()) }
}
#[allow(non_snake_case)]
impl Predict<Array2<f64>, Array1<f64>> for SpatialGaussianProcessRegressor<Trained> {
fn predict(&self, X: &Array2<f64>) -> SklResult<Array1<f64>> {
let (predictions, _) = self.predict_with_variance(X)?;
Ok(predictions)
}
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
use scirs2_core::ndarray::array;
#[test]
fn test_spatial_kernel_spherical() {
let kernel = SpatialKernel::spherical(1.0, 10.0);
assert_abs_diff_eq!(kernel.compute_covariance(0.0), 1.0, epsilon = 1e-10);
let cov_5 = kernel.compute_covariance(5.0);
assert!(cov_5 > 0.0 && cov_5 < 1.0);
let cov_15 = kernel.compute_covariance(15.0);
assert_abs_diff_eq!(cov_15, 0.0, epsilon = 1e-10);
}
#[test]
fn test_spatial_kernel_exponential() {
let kernel = SpatialKernel::exponential(1.0, 5.0);
let cov_0 = kernel.compute_covariance(0.0);
let cov_5 = kernel.compute_covariance(5.0);
let cov_10 = kernel.compute_covariance(10.0);
assert_abs_diff_eq!(cov_0, 1.0, epsilon = 1e-10);
assert!(cov_5 > cov_10);
assert!(cov_10 > 0.0);
}
#[test]
fn test_variogram_computation() {
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 variogram =
Variogram::compute_empirical(&coords, &values, 5).expect("operation should succeed");
assert!(!variogram.distances.is_empty());
assert_eq!(variogram.distances.len(), variogram.semivariances.len());
assert_eq!(variogram.distances.len(), variogram.n_pairs.len());
}
#[test]
fn test_spatial_gp_ordinary_kriging() {
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 spatial_gp = SpatialGaussianProcessRegressor::builder()
.spatial_kernel(SpatialKernel::spherical(1.0, 2.0))
.kriging_type(KrigingType::Ordinary)
.build();
let trained = spatial_gp
.fit(&coords, &values)
.expect("model fitting should succeed");
let predictions = trained.predict(&coords).expect("prediction should succeed");
assert_eq!(predictions.len(), coords.nrows());
}
#[test]
fn test_spatial_gp_simple_kriging() {
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 spatial_gp = SpatialGaussianProcessRegressor::builder()
.spatial_kernel(SpatialKernel::exponential(1.0, 2.0))
.kriging_type(KrigingType::Simple { mean: 1.75 })
.build();
let trained = spatial_gp
.fit(&coords, &values)
.expect("model fitting should succeed");
let predictions = trained.predict(&coords).expect("prediction should succeed");
assert_eq!(predictions.len(), coords.nrows());
}
#[test]
fn test_spatial_gp_with_variance() {
let coords = array![[0.0, 0.0], [2.0, 0.0], [0.0, 2.0], [2.0, 2.0]];
let values = array![1.0, 3.0, 2.0, 4.0];
let spatial_gp = SpatialGaussianProcessRegressor::builder()
.spatial_kernel(SpatialKernel::gaussian(2.0, 1.0))
.kriging_type(KrigingType::Ordinary)
.build();
let trained = spatial_gp
.fit(&coords, &values)
.expect("model fitting should succeed");
let test_coords = array![[1.0, 1.0]]; let (predictions, variances) = trained
.predict_with_variance(&test_coords)
.expect("operation should succeed");
assert_eq!(predictions.len(), 1);
assert_eq!(variances.len(), 1);
assert!(variances[0] >= 0.0);
}
#[test]
fn test_spatial_kernel_with_nugget() {
let kernel = SpatialKernel::spherical_with_nugget(1.0, 5.0, 0.1);
assert_abs_diff_eq!(kernel.compute_covariance(0.0), 1.1, epsilon = 1e-10);
let cov_1 = kernel.compute_covariance(1.0);
assert!(cov_1 > 0.0 && cov_1 < 1.0);
}
#[test]
fn test_anisotropic_kernel() {
let base_kernel = SpatialKernel::exponential(1.0, 2.0);
let anisotropy_matrix = array![[2.0, 0.0], [0.0, 1.0]];
let aniso_kernel = SpatialKernel::anisotropic(base_kernel, anisotropy_matrix);
let coords = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]];
let K = aniso_kernel
.compute_kernel_matrix(&coords, None)
.expect("operation should succeed");
assert_eq!(K.shape(), &[3, 3]);
assert_abs_diff_eq!(
K[[0, 0]],
aniso_kernel.compute_covariance(0.0),
epsilon = 1e-10
);
}
#[test]
fn test_variogram_model_fitting() {
let coords = array![[0.0, 0.0], [1.0, 0.0], [2.0, 0.0], [3.0, 0.0], [4.0, 0.0]];
let values = array![1.0, 1.2, 1.8, 2.1, 2.5];
let mut variogram =
Variogram::compute_empirical(&coords, &values, 4).expect("operation should succeed");
variogram
.fit_model("spherical")
.expect("operation should succeed");
assert!(variogram.fitted_model.is_some());
let goodness = variogram
.goodness_of_fit()
.expect("operation should succeed");
assert!((0.0..=1.0).contains(&goodness));
}
#[test]
fn test_spatial_outlier_detection() {
let coords = array![[0.0, 0.0], [1.0, 0.0], [2.0, 0.0], [3.0, 0.0]];
let values = array![1.0, 2.0, 3.0, 10.0];
let spatial_gp = SpatialGaussianProcessRegressor::builder()
.spatial_kernel(SpatialKernel::exponential(1.0, 2.0))
.kriging_type(KrigingType::Ordinary)
.build();
let trained = spatial_gp
.fit(&coords, &values)
.expect("model fitting should succeed");
let outliers = trained
.detect_spatial_outliers(2.0)
.expect("operation should succeed");
assert!(!outliers.is_empty());
}
#[test]
fn test_spatial_cross_validation() {
let coords = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0], [0.5, 0.5]];
let values = array![1.0, 2.0, 1.5, 2.5, 1.8];
let spatial_gp = SpatialGaussianProcessRegressor::builder()
.spatial_kernel(SpatialKernel::spherical(1.0, 2.0))
.kriging_type(KrigingType::Ordinary)
.build();
let trained = spatial_gp
.fit(&coords, &values)
.expect("model fitting should succeed");
let cv_error = trained
.spatial_cross_validation()
.expect("operation should succeed");
assert!(cv_error >= 0.0);
}
#[test]
fn test_matern_kernel() {
let kernel = SpatialKernel::matern(1.0, 2.0, 1.5);
assert_abs_diff_eq!(kernel.compute_covariance(0.0), 1.0, epsilon = 1e-10);
let cov_1 = kernel.compute_covariance(1.0);
let cov_2 = kernel.compute_covariance(2.0);
assert!(cov_1 > cov_2);
assert!(cov_2 > 0.0);
}
#[test]
fn test_correlation_structure() {
let coords = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]];
let values = array![1.0, 2.0, 1.5];
let spatial_gp = SpatialGaussianProcessRegressor::builder()
.spatial_kernel(SpatialKernel::exponential(1.0, 2.0))
.kriging_type(KrigingType::Ordinary)
.build();
let trained = spatial_gp
.fit(&coords, &values)
.expect("model fitting should succeed");
let (distances, correlations) = trained.correlation_structure(5.0, 10);
assert_eq!(distances.len(), 10);
assert_eq!(correlations.len(), 10);
assert_abs_diff_eq!(correlations[0], 1.0, epsilon = 1e-10); }
}