1use crate::kernels::Kernel;
38use crate::utils;
39use scirs2_core::ndarray::{s, Array1, Array2, ArrayView1, Axis};
40use sklears_core::error::{Result as SklResult, SklearsError};
42use sklears_core::traits::{Estimator, Fit, Predict};
43use std::f64::consts::PI;
44
45#[derive(Debug, Clone)]
47pub struct Untrained;
48
49#[derive(Debug, Clone)]
51pub struct Trained {
52 pub spatial_kernel: SpatialKernel,
53 pub kriging_type: KrigingType,
54 pub training_data: (Array2<f64>, Array1<f64>),
55 pub alpha: Array1<f64>,
56 pub cholesky: Array2<f64>,
57 pub log_likelihood: f64,
58 pub variogram: Option<Variogram>,
59 pub anisotropy_matrix: Option<Array2<f64>>,
60}
61
62#[derive(Debug, Clone, Copy)]
64pub enum KrigingType {
65 Simple { mean: f64 },
67 Ordinary,
69 Universal,
71 CoKriging,
73}
74
75#[derive(Debug, Clone)]
77pub enum SpatialKernel {
78 Spherical {
80 sill: f64, range: f64, nugget: f64, },
84 Exponential { sill: f64, range: f64, nugget: f64 },
86 Gaussian { sill: f64, range: f64, nugget: f64 },
88 Matern {
90 sill: f64,
91 range: f64,
92 nugget: f64,
93 nu: f64, },
95 Power {
97 scale: f64,
98 exponent: f64, },
100 Linear { slope: f64, nugget: f64 },
102 HoleEffect {
104 sill: f64,
105 range: f64,
106 nugget: f64,
107 damping: f64,
108 },
109 Anisotropic {
111 base_kernel: Box<SpatialKernel>,
112 anisotropy_matrix: Array2<f64>,
113 },
114}
115
116impl SpatialKernel {
117 pub fn spherical(sill: f64, range: f64) -> Self {
119 Self::Spherical {
120 sill,
121 range,
122 nugget: 0.0,
123 }
124 }
125
126 pub fn spherical_with_nugget(sill: f64, range: f64, nugget: f64) -> Self {
128 Self::Spherical {
129 sill,
130 range,
131 nugget,
132 }
133 }
134
135 pub fn exponential(sill: f64, range: f64) -> Self {
137 Self::Exponential {
138 sill,
139 range,
140 nugget: 0.0,
141 }
142 }
143
144 pub fn gaussian(sill: f64, range: f64) -> Self {
146 Self::Gaussian {
147 sill,
148 range,
149 nugget: 0.0,
150 }
151 }
152
153 pub fn matern(sill: f64, range: f64, nu: f64) -> Self {
155 Self::Matern {
156 sill,
157 range,
158 nugget: 0.0,
159 nu,
160 }
161 }
162
163 pub fn anisotropic(base_kernel: SpatialKernel, anisotropy_matrix: Array2<f64>) -> Self {
165 Self::Anisotropic {
166 base_kernel: Box::new(base_kernel),
167 anisotropy_matrix,
168 }
169 }
170
171 fn compute_distance(&self, x1: &[f64], x2: &[f64]) -> f64 {
173 match self {
174 Self::Anisotropic {
175 anisotropy_matrix, ..
176 } => {
177 let diff: Array1<f64> =
179 Array1::from_vec(x1.iter().zip(x2.iter()).map(|(a, b)| a - b).collect());
180 let transformed = anisotropy_matrix.dot(&diff);
181 transformed.iter().map(|x| x.powi(2)).sum::<f64>().sqrt()
182 }
183 _ => {
184 x1.iter()
186 .zip(x2.iter())
187 .map(|(a, b)| (a - b).powi(2))
188 .sum::<f64>()
189 .sqrt()
190 }
191 }
192 }
193
194 pub fn compute_covariance(&self, distance: f64) -> f64 {
196 match self {
197 Self::Spherical {
198 sill,
199 range,
200 nugget,
201 } => {
202 if distance == 0.0 {
203 sill + nugget
204 } else if distance <= *range {
205 let h_r = distance / range;
206 sill * (1.0 - 1.5 * h_r + 0.5 * h_r.powi(3))
207 + if distance == 0.0 { *nugget } else { 0.0 }
208 } else {
209 if distance == 0.0 {
210 *nugget
211 } else {
212 0.0
213 }
214 }
215 }
216 Self::Exponential {
217 sill,
218 range,
219 nugget,
220 } => {
221 if distance == 0.0 {
222 sill + nugget
223 } else {
224 sill * (-3.0 * distance / range).exp()
225 }
226 }
227 Self::Gaussian {
228 sill,
229 range,
230 nugget,
231 } => {
232 if distance == 0.0 {
233 sill + nugget
234 } else {
235 sill * (-3.0 * (distance / range).powi(2)).exp()
236 }
237 }
238 Self::Matern {
239 sill,
240 range,
241 nugget,
242 nu,
243 } => {
244 if distance == 0.0 {
245 return sill + nugget;
246 }
247
248 let h = distance / range;
249 match nu {
250 0.5 => sill * (-h).exp(),
251 1.5 => sill * (1.0 + h * 3.0_f64.sqrt()) * (-h * 3.0_f64.sqrt()).exp(),
252 2.5 => {
253 sill * (1.0 + h * 5.0_f64.sqrt() + 5.0 * h.powi(2) / 3.0)
254 * (-h * 5.0_f64.sqrt()).exp()
255 }
256 _ => {
257 sill * (1.0 + h).exp() * (-h).exp()
259 }
260 }
261 }
262 Self::Power { scale, exponent } => {
263 if distance == 0.0 {
264 0.0
265 } else {
266 scale * distance.powf(*exponent)
267 }
268 }
269 Self::Linear { slope, nugget } => {
270 if distance == 0.0 {
271 *nugget
272 } else {
273 slope * distance
274 }
275 }
276 Self::HoleEffect {
277 sill,
278 range,
279 nugget,
280 damping,
281 } => {
282 if distance == 0.0 {
283 sill + nugget
284 } else {
285 let h_r = distance / range;
286 sill * (1.0 - h_r) * (-damping * h_r).exp().max(0.0)
287 }
288 }
289 Self::Anisotropic { base_kernel, .. } => {
290 base_kernel.compute_covariance(distance)
292 }
293 }
294 }
295
296 pub fn compute_variogram(&self, distance: f64) -> f64 {
298 match self {
299 Self::Spherical {
300 sill,
301 range,
302 nugget,
303 } => {
304 if distance == 0.0 {
305 0.0
306 } else if distance <= *range {
307 let h_r = distance / range;
308 nugget + sill * (1.5 * h_r - 0.5 * h_r.powi(3))
309 } else {
310 nugget + sill
311 }
312 }
313 Self::Exponential {
314 sill,
315 range,
316 nugget,
317 } => {
318 if distance == 0.0 {
319 0.0
320 } else {
321 nugget + sill * (1.0 - (-3.0 * distance / range).exp())
322 }
323 }
324 Self::Gaussian {
325 sill,
326 range,
327 nugget,
328 } => {
329 if distance == 0.0 {
330 0.0
331 } else {
332 nugget + sill * (1.0 - (-3.0 * (distance / range).powi(2)).exp())
333 }
334 }
335 _ => {
336 let c0 = self.compute_covariance(0.0);
338 let ch = self.compute_covariance(distance);
339 (c0 - ch).max(0.0)
340 }
341 }
342 }
343
344 pub fn effective_range(&self) -> f64 {
346 match self {
347 Self::Spherical { range, .. }
348 | Self::Exponential { range, .. }
349 | Self::Gaussian { range, .. }
350 | Self::Matern { range, .. }
351 | Self::HoleEffect { range, .. } => *range,
352 Self::Power { .. } => f64::INFINITY,
353 Self::Linear { .. } => f64::INFINITY,
354 Self::Anisotropic { base_kernel, .. } => base_kernel.effective_range(),
355 }
356 }
357
358 pub fn sill(&self) -> f64 {
360 match self {
361 Self::Spherical { sill, .. }
362 | Self::Exponential { sill, .. }
363 | Self::Gaussian { sill, .. }
364 | Self::Matern { sill, .. }
365 | Self::HoleEffect { sill, .. } => *sill,
366 Self::Power { scale, .. } => *scale,
367 Self::Linear { slope, .. } => *slope,
368 Self::Anisotropic { base_kernel, .. } => base_kernel.sill(),
369 }
370 }
371
372 pub fn nugget(&self) -> f64 {
374 match self {
375 Self::Spherical { nugget, .. }
376 | Self::Exponential { nugget, .. }
377 | Self::Gaussian { nugget, .. }
378 | Self::Matern { nugget, .. }
379 | Self::HoleEffect { nugget, .. }
380 | Self::Linear { nugget, .. } => *nugget,
381 Self::Power { .. } => 0.0,
382 Self::Anisotropic { base_kernel, .. } => base_kernel.nugget(),
383 }
384 }
385}
386
387impl Kernel for SpatialKernel {
388 fn compute_kernel_matrix(
389 &self,
390 x1: &Array2<f64>,
391 x2: Option<&Array2<f64>>,
392 ) -> SklResult<Array2<f64>> {
393 let x2 = x2.unwrap_or(x1);
394 let n1 = x1.nrows();
395 let n2 = x2.nrows();
396
397 if x1.ncols() < 2 || x2.ncols() < 2 {
398 return Err(SklearsError::InvalidInput(
399 "Spatial kernels require at least 2D coordinates".to_string(),
400 ));
401 }
402
403 let mut K = Array2::zeros((n1, n2));
404
405 for i in 0..n1 {
406 for j in 0..n2 {
407 let x1_coords = x1.row(i).to_vec();
408 let x2_coords = x2.row(j).to_vec();
409 let distance = self.compute_distance(&x1_coords, &x2_coords);
410 K[[i, j]] = self.compute_covariance(distance);
411 }
412 }
413
414 Ok(K)
415 }
416
417 fn clone_box(&self) -> Box<dyn Kernel> {
418 Box::new(self.clone())
419 }
420
421 fn kernel(&self, x1: &ArrayView1<f64>, x2: &ArrayView1<f64>) -> f64 {
422 if x1.len() < 2 || x2.len() < 2 {
423 return 0.0; }
425 let distance = self.compute_distance(&x1.to_vec(), &x2.to_vec());
426 self.compute_covariance(distance)
427 }
428
429 fn get_params(&self) -> Vec<f64> {
430 match self {
431 Self::Spherical {
432 range,
433 sill,
434 nugget,
435 } => vec![*range, *sill, *nugget],
436 Self::Exponential {
437 range,
438 sill,
439 nugget,
440 } => vec![*range, *sill, *nugget],
441 Self::Gaussian {
442 range,
443 sill,
444 nugget,
445 } => vec![*range, *sill, *nugget],
446 Self::Matern {
447 range,
448 sill,
449 nugget,
450 nu,
451 } => vec![*range, *sill, *nugget, *nu],
452 Self::Linear { slope, nugget } => vec![*slope, *nugget],
453 Self::Power { scale, exponent } => vec![*scale, *exponent],
454 Self::HoleEffect {
455 range,
456 sill,
457 nugget,
458 damping,
459 } => vec![*range, *sill, *nugget, *damping],
460 Self::Anisotropic {
461 base_kernel,
462 anisotropy_matrix,
463 } => {
464 let mut params = base_kernel.get_params();
465 for row in anisotropy_matrix.rows() {
467 for &val in row {
468 params.push(val);
469 }
470 }
471 params
472 }
473 }
474 }
475
476 fn set_params(&mut self, params: &[f64]) -> SklResult<()> {
477 match self {
478 Self::Spherical {
479 range,
480 sill,
481 nugget,
482 } => {
483 if params.len() != 3 {
484 return Err(SklearsError::InvalidInput(
485 "Spherical kernel requires 3 parameters".to_string(),
486 ));
487 }
488 *range = params[0];
489 *sill = params[1];
490 *nugget = params[2];
491 }
492 Self::Exponential {
493 range,
494 sill,
495 nugget,
496 } => {
497 if params.len() != 3 {
498 return Err(SklearsError::InvalidInput(
499 "Exponential kernel requires 3 parameters".to_string(),
500 ));
501 }
502 *range = params[0];
503 *sill = params[1];
504 *nugget = params[2];
505 }
506 Self::Gaussian {
507 range,
508 sill,
509 nugget,
510 } => {
511 if params.len() != 3 {
512 return Err(SklearsError::InvalidInput(
513 "Gaussian kernel requires 3 parameters".to_string(),
514 ));
515 }
516 *range = params[0];
517 *sill = params[1];
518 *nugget = params[2];
519 }
520 Self::Matern {
521 range,
522 sill,
523 nugget,
524 nu,
525 } => {
526 if params.len() != 4 {
527 return Err(SklearsError::InvalidInput(
528 "Matern kernel requires 4 parameters".to_string(),
529 ));
530 }
531 *range = params[0];
532 *sill = params[1];
533 *nugget = params[2];
534 *nu = params[3];
535 }
536 Self::Linear { slope, nugget } => {
537 if params.len() != 2 {
538 return Err(SklearsError::InvalidInput(
539 "Linear kernel requires 2 parameters".to_string(),
540 ));
541 }
542 *slope = params[0];
543 *nugget = params[1];
544 }
545 Self::Power { scale, exponent } => {
546 if params.len() != 2 {
547 return Err(SklearsError::InvalidInput(
548 "Power kernel requires 2 parameters".to_string(),
549 ));
550 }
551 *scale = params[0];
552 *exponent = params[1];
553 }
554 Self::HoleEffect {
555 range,
556 sill,
557 nugget,
558 damping,
559 } => {
560 if params.len() != 4 {
561 return Err(SklearsError::InvalidInput(
562 "HoleEffect kernel requires 4 parameters".to_string(),
563 ));
564 }
565 *range = params[0];
566 *sill = params[1];
567 *nugget = params[2];
568 *damping = params[3];
569 }
570 Self::Anisotropic {
571 base_kernel,
572 anisotropy_matrix,
573 } => {
574 let base_params_len = base_kernel.get_params().len();
575 if params.len() < base_params_len {
576 return Err(SklearsError::InvalidInput(
577 "Not enough parameters for anisotropic kernel".to_string(),
578 ));
579 }
580
581 base_kernel.set_params(¶ms[..base_params_len])?;
583
584 let matrix_size = anisotropy_matrix.nrows() * anisotropy_matrix.ncols();
586 if params.len() != base_params_len + matrix_size {
587 return Err(SklearsError::InvalidInput(
588 "Incorrect number of parameters for anisotropy matrix".to_string(),
589 ));
590 }
591
592 let matrix_params = ¶ms[base_params_len..];
593 let ncols = anisotropy_matrix.ncols();
594 for (i, mut row) in anisotropy_matrix.rows_mut().into_iter().enumerate() {
595 for (j, val) in row.iter_mut().enumerate() {
596 *val = matrix_params[i * ncols + j];
597 }
598 }
599 }
600 }
601 Ok(())
602 }
603}
604
605#[derive(Debug, Clone)]
607pub struct Variogram {
608 pub distances: Array1<f64>,
609 pub semivariances: Array1<f64>,
610 pub n_pairs: Array1<usize>,
611 pub fitted_model: Option<SpatialKernel>,
612}
613
614impl Variogram {
615 pub fn compute_empirical(
617 coords: &Array2<f64>,
618 values: &Array1<f64>,
619 n_bins: usize,
620 ) -> SklResult<Self> {
621 let n_points = coords.nrows();
622 if coords.nrows() != values.len() {
623 return Err(SklearsError::DimensionMismatch {
624 expected: coords.nrows(),
625 actual: values.len(),
626 });
627 }
628
629 let mut all_distances = Vec::new();
631 let mut all_semivariances = Vec::new();
632
633 for i in 0..n_points {
634 for j in i + 1..n_points {
635 let dist = coords
636 .row(i)
637 .iter()
638 .zip(coords.row(j).iter())
639 .map(|(a, b)| (a - b).powi(2))
640 .sum::<f64>()
641 .sqrt();
642
643 let semivar = 0.5 * (values[i] - values[j]).powi(2);
644
645 all_distances.push(dist);
646 all_semivariances.push(semivar);
647 }
648 }
649
650 if all_distances.is_empty() {
651 return Err(SklearsError::InvalidInput(
652 "Not enough data points for variogram".to_string(),
653 ));
654 }
655
656 let max_dist = all_distances.iter().fold(0.0f64, |a, &b| a.max(b));
658 let min_dist = all_distances.iter().fold(f64::INFINITY, |a, &b| a.min(b));
659 let bin_width = (max_dist - min_dist) / n_bins as f64;
660
661 let mut bin_distances: Array1<f64> = Array1::zeros(n_bins);
662 let mut bin_semivariances: Array1<f64> = Array1::zeros(n_bins);
663 let mut bin_counts: Array1<f64> = Array1::zeros(n_bins);
664
665 for (dist, semivar) in all_distances.iter().zip(all_semivariances.iter()) {
667 let bin_idx = ((*dist - min_dist) / bin_width).floor() as usize;
668 let bin_idx = bin_idx.min(n_bins - 1);
669
670 bin_distances[bin_idx] += dist;
671 bin_semivariances[bin_idx] += semivar;
672 bin_counts[bin_idx] += 1.0;
673 }
674
675 let mut distances = Vec::new();
677 let mut semivariances = Vec::new();
678 let mut n_pairs = Vec::new();
679
680 for i in 0..n_bins {
681 if bin_counts[i] > 0.0 {
682 distances.push(bin_distances[i] / bin_counts[i] as f64);
683 semivariances.push(bin_semivariances[i] / bin_counts[i] as f64);
684 n_pairs.push(bin_counts[i] as usize);
685 }
686 }
687
688 Ok(Self {
689 distances: Array1::from_vec(distances),
690 semivariances: Array1::from_vec(semivariances),
691 n_pairs: Array1::from_vec(n_pairs),
692 fitted_model: None,
693 })
694 }
695
696 pub fn fit_model(&mut self, model_type: &str) -> SklResult<()> {
698 if self.distances.is_empty() {
699 return Err(SklearsError::InvalidInput(
700 "No variogram data to fit".to_string(),
701 ));
702 }
703
704 let max_semivar = self.semivariances.iter().fold(0.0f64, |a, &b| a.max(b));
706 let max_dist = self.distances.iter().fold(0.0f64, |a, &b| a.max(b));
707
708 let nugget = if self.distances[0] > 0.0 {
710 0.0
711 } else {
712 self.semivariances[0]
713 };
714
715 let sill = max_semivar - nugget;
717
718 let target_semivar = nugget + 0.95 * sill;
720 let mut range = max_dist;
721 for i in 0..self.distances.len() {
722 if self.semivariances[i] >= target_semivar {
723 range = self.distances[i];
724 break;
725 }
726 }
727
728 let fitted_model = match model_type {
729 "spherical" => SpatialKernel::Spherical {
730 sill,
731 range,
732 nugget,
733 },
734 "exponential" => SpatialKernel::Exponential {
735 sill,
736 range,
737 nugget,
738 },
739 "gaussian" => SpatialKernel::Gaussian {
740 sill,
741 range,
742 nugget,
743 },
744 "matern15" => SpatialKernel::Matern {
745 sill,
746 range,
747 nugget,
748 nu: 1.5,
749 },
750 _ => {
751 return Err(SklearsError::InvalidInput(format!(
752 "Unknown model type: {}",
753 model_type
754 )))
755 }
756 };
757
758 self.fitted_model = Some(fitted_model);
759 Ok(())
760 }
761
762 pub fn goodness_of_fit(&self) -> SklResult<f64> {
764 let model = self
765 .fitted_model
766 .as_ref()
767 .ok_or_else(|| SklearsError::InvalidInput("No model fitted".to_string()))?;
768
769 let mut ss_res = 0.0;
770 let mut ss_tot = 0.0;
771 let mean_semivar = self.semivariances.mean().unwrap_or(0.0);
772
773 for i in 0..self.distances.len() {
774 let predicted = model.compute_variogram(self.distances[i]);
775 let observed = self.semivariances[i];
776
777 ss_res += (observed - predicted).powi(2);
778 ss_tot += (observed - mean_semivar).powi(2);
779 }
780
781 let r_squared = 1.0 - (ss_res / ss_tot.max(1e-12));
782 Ok(r_squared)
783 }
784}
785
786#[derive(Debug, Clone)]
788pub struct SpatialGaussianProcessRegressor<S = Untrained> {
789 spatial_kernel: Option<SpatialKernel>,
790 kriging_type: KrigingType,
791 estimate_variogram: bool,
792 variogram_bins: usize,
793 anisotropy_matrix: Option<Array2<f64>>,
794 alpha: f64,
795 _state: S,
796}
797
798#[derive(Debug, Clone)]
800pub struct SpatialGPConfig {
801 pub kriging_type: KrigingType,
802 pub estimate_variogram: bool,
803 pub variogram_bins: usize,
804 pub regularization: f64,
805}
806
807impl Default for SpatialGPConfig {
808 fn default() -> Self {
809 Self {
810 kriging_type: KrigingType::Ordinary,
811 estimate_variogram: false,
812 variogram_bins: 20,
813 regularization: 1e-6,
814 }
815 }
816}
817
818impl SpatialGaussianProcessRegressor<Untrained> {
819 pub fn new() -> Self {
821 Self {
822 spatial_kernel: None,
823 kriging_type: KrigingType::Ordinary,
824 estimate_variogram: false,
825 variogram_bins: 20,
826 anisotropy_matrix: None,
827 alpha: 1e-6,
828 _state: Untrained,
829 }
830 }
831
832 pub fn builder() -> SpatialGPBuilder {
834 SpatialGPBuilder::new()
835 }
836
837 pub fn spatial_kernel(mut self, kernel: SpatialKernel) -> Self {
839 self.spatial_kernel = Some(kernel);
840 self
841 }
842
843 pub fn kriging_type(mut self, kriging_type: KrigingType) -> Self {
845 self.kriging_type = kriging_type;
846 self
847 }
848
849 pub fn estimate_variogram(mut self, estimate: bool) -> Self {
851 self.estimate_variogram = estimate;
852 self
853 }
854
855 pub fn anisotropy_matrix(mut self, matrix: Array2<f64>) -> Self {
857 self.anisotropy_matrix = Some(matrix);
858 self
859 }
860
861 pub fn alpha(mut self, alpha: f64) -> Self {
863 self.alpha = alpha;
864 self
865 }
866}
867
868#[derive(Debug, Clone)]
870pub struct SpatialGPBuilder {
871 kernel: Option<SpatialKernel>,
872 kriging_type: KrigingType,
873 estimate_variogram: bool,
874 variogram_bins: usize,
875 anisotropy_matrix: Option<Array2<f64>>,
876 alpha: f64,
877}
878
879impl SpatialGPBuilder {
880 pub fn new() -> Self {
881 Self {
882 kernel: None,
883 kriging_type: KrigingType::Ordinary,
884 estimate_variogram: false,
885 variogram_bins: 20,
886 anisotropy_matrix: None,
887 alpha: 1e-6,
888 }
889 }
890
891 pub fn spatial_kernel(mut self, kernel: SpatialKernel) -> Self {
892 self.kernel = Some(kernel);
893 self
894 }
895
896 pub fn kriging_type(mut self, kriging_type: KrigingType) -> Self {
897 self.kriging_type = kriging_type;
898 self
899 }
900
901 pub fn estimate_variogram(mut self, estimate: bool) -> Self {
902 self.estimate_variogram = estimate;
903 self
904 }
905
906 pub fn variogram_bins(mut self, bins: usize) -> Self {
907 self.variogram_bins = bins;
908 self
909 }
910
911 pub fn anisotropy_matrix(mut self, matrix: Array2<f64>) -> Self {
912 self.anisotropy_matrix = Some(matrix);
913 self
914 }
915
916 pub fn alpha(mut self, alpha: f64) -> Self {
917 self.alpha = alpha;
918 self
919 }
920
921 pub fn build(self) -> SpatialGaussianProcessRegressor<Untrained> {
922 SpatialGaussianProcessRegressor {
923 spatial_kernel: self.kernel,
924 kriging_type: self.kriging_type,
925 estimate_variogram: self.estimate_variogram,
926 variogram_bins: self.variogram_bins,
927 anisotropy_matrix: self.anisotropy_matrix.clone(),
928 alpha: self.alpha,
929 _state: Untrained,
930 }
931 }
932}
933
934impl Estimator for SpatialGaussianProcessRegressor<Untrained> {
935 type Config = SpatialGPConfig;
936 type Error = SklearsError;
937 type Float = f64;
938
939 fn config(&self) -> &Self::Config {
940 static DEFAULT_CONFIG: SpatialGPConfig = SpatialGPConfig {
941 kriging_type: KrigingType::Ordinary,
942 estimate_variogram: false,
943 variogram_bins: 20,
944 regularization: 1e-6,
945 };
946 &DEFAULT_CONFIG
947 }
948}
949
950impl Estimator for SpatialGaussianProcessRegressor<Trained> {
951 type Config = SpatialGPConfig;
952 type Error = SklearsError;
953 type Float = f64;
954
955 fn config(&self) -> &Self::Config {
956 static DEFAULT_CONFIG: SpatialGPConfig = SpatialGPConfig {
957 kriging_type: KrigingType::Ordinary,
958 estimate_variogram: false,
959 variogram_bins: 20,
960 regularization: 1e-6,
961 };
962 &DEFAULT_CONFIG
963 }
964}
965
966impl Fit<Array2<f64>, Array1<f64>> for SpatialGaussianProcessRegressor<Untrained> {
967 type Fitted = SpatialGaussianProcessRegressor<Trained>;
968
969 fn fit(self, X: &Array2<f64>, y: &Array1<f64>) -> SklResult<Self::Fitted> {
970 if X.nrows() != y.len() {
971 return Err(SklearsError::DimensionMismatch {
972 expected: X.nrows(),
973 actual: y.len(),
974 });
975 }
976
977 if X.ncols() < 2 {
978 return Err(SklearsError::InvalidInput(
979 "Spatial GP requires at least 2D coordinates".to_string(),
980 ));
981 }
982
983 let X_owned = X.to_owned();
984 let y_owned = y.to_owned();
985
986 let variogram = if self.estimate_variogram {
988 Some(Variogram::compute_empirical(
989 &X_owned,
990 &y_owned,
991 self.variogram_bins,
992 )?)
993 } else {
994 None
995 };
996
997 let mut spatial_kernel = self.spatial_kernel.clone().ok_or_else(|| {
999 SklearsError::InvalidInput("Spatial kernel must be specified".to_string())
1000 })?;
1001
1002 if let Some(anisotropy_matrix) = &self.anisotropy_matrix {
1004 spatial_kernel = SpatialKernel::anisotropic(spatial_kernel, anisotropy_matrix.clone());
1005 }
1006
1007 let K = spatial_kernel.compute_kernel_matrix(&X_owned, None)?;
1009
1010 let (y_for_gp, augmented_matrix) = match self.kriging_type {
1012 KrigingType::Simple { mean } => {
1013 let y_centered = &y_owned - mean;
1015 (y_centered, K.clone())
1016 }
1017 KrigingType::Ordinary => {
1018 let n = K.nrows();
1020 let mut augmented = Array2::zeros((n + 1, n + 1));
1021
1022 for i in 0..n {
1024 for j in 0..n {
1025 augmented[[i, j]] = K[[i, j]];
1026 }
1027 }
1028
1029 for i in 0..n {
1031 augmented[[i, n]] = 1.0;
1032 augmented[[n, i]] = 1.0;
1033 }
1034
1035 (y_owned.clone(), augmented)
1036 }
1037 KrigingType::Universal => {
1038 let n = K.nrows();
1041 let mut augmented = Array2::zeros((n + 1, n + 1));
1042
1043 for i in 0..n {
1044 for j in 0..n {
1045 augmented[[i, j]] = K[[i, j]];
1046 }
1047 }
1048
1049 for i in 0..n {
1050 augmented[[i, n]] = 1.0;
1051 augmented[[n, i]] = 1.0;
1052 }
1053
1054 (y_owned.clone(), augmented)
1055 }
1056 KrigingType::CoKriging => {
1057 let n = K.nrows();
1060 let mut augmented = Array2::zeros((n + 1, n + 1));
1061
1062 for i in 0..n {
1063 for j in 0..n {
1064 augmented[[i, j]] = K[[i, j]];
1065 }
1066 }
1067
1068 for i in 0..n {
1069 augmented[[i, n]] = 1.0;
1070 augmented[[n, i]] = 1.0;
1071 }
1072
1073 (y_owned.clone(), augmented)
1074 }
1075 };
1076
1077 let mut K_reg = augmented_matrix.clone();
1079 let matrix_size = K_reg.nrows();
1080
1081 for i in 0..K.nrows() {
1083 K_reg[[i, i]] += self.alpha;
1084 }
1085
1086 match self.kriging_type {
1090 KrigingType::Simple { .. } => {}
1091 _ => {
1092 if matrix_size > K.nrows() {
1096 K_reg[[K.nrows(), K.nrows()]] = 0.01;
1097 }
1098 }
1099 }
1100
1101 let rhs = match self.kriging_type {
1103 KrigingType::Simple { .. } => y_for_gp,
1104 _ => {
1105 let mut extended_y = Array1::zeros(y_for_gp.len() + 1);
1106 for i in 0..y_for_gp.len() {
1107 extended_y[i] = y_for_gp[i];
1108 }
1109 extended_y
1111 }
1112 };
1113
1114 let chol_decomp = utils::robust_cholesky(&K_reg)?;
1116 let alpha = utils::triangular_solve(&chol_decomp, &rhs)?;
1117
1118 let log_det = chol_decomp.diag().iter().map(|x| x.ln()).sum::<f64>() * 2.0;
1120 let data_fit = rhs.dot(&alpha);
1121 let n = rhs.len();
1122 let log_likelihood = -0.5 * (data_fit + log_det + n as f64 * (2.0 * PI).ln());
1123
1124 Ok(SpatialGaussianProcessRegressor {
1125 spatial_kernel: self.spatial_kernel,
1126 kriging_type: self.kriging_type,
1127 estimate_variogram: self.estimate_variogram,
1128 variogram_bins: self.variogram_bins,
1129 anisotropy_matrix: self.anisotropy_matrix.clone(),
1130 alpha: self.alpha,
1131 _state: Trained {
1132 spatial_kernel,
1133 kriging_type: self.kriging_type,
1134 training_data: (X_owned, y_owned),
1135 alpha,
1136 cholesky: chol_decomp,
1137 log_likelihood,
1138 variogram,
1139 anisotropy_matrix: self.anisotropy_matrix.clone(),
1140 },
1141 })
1142 }
1143}
1144
1145impl SpatialGaussianProcessRegressor<Trained> {
1146 pub fn trained_state(&self) -> &Trained {
1148 &self._state
1149 }
1150
1151 pub fn variogram(&self) -> Option<&Variogram> {
1153 self._state.variogram.as_ref()
1154 }
1155
1156 pub fn predict_with_variance(&self, X: &Array2<f64>) -> SklResult<(Array1<f64>, Array1<f64>)> {
1158 let n_test = X.nrows();
1159 let n_train = self._state.training_data.0.nrows();
1160
1161 let K_star = self
1163 ._state
1164 .spatial_kernel
1165 .compute_kernel_matrix(&self._state.training_data.0, Some(X))?;
1166
1167 let (predictions, variances) = match self._state.kriging_type {
1169 KrigingType::Simple { mean } => {
1170 let pred = K_star.t().dot(&self._state.alpha.slice(s![0..n_train])) + mean;
1172
1173 let K_star_star = self._state.spatial_kernel.compute_kernel_matrix(X, None)?;
1175 let cholesky_slice = self
1176 ._state
1177 .cholesky
1178 .slice(s![0..n_train, 0..n_train])
1179 .to_owned();
1180 let v = utils::triangular_solve_matrix(&cholesky_slice, &K_star)?;
1181 let pred_var =
1182 K_star_star.diag().to_owned() - &v.map(|x| x.powi(2)).sum_axis(Axis(0));
1183
1184 (pred, pred_var)
1185 }
1186 _ => {
1187 let mut extended_k_star = Array2::zeros((n_train + 1, n_test));
1189
1190 for i in 0..n_train {
1192 for j in 0..n_test {
1193 extended_k_star[[i, j]] = K_star[[i, j]];
1194 }
1195 }
1196
1197 for j in 0..n_test {
1199 extended_k_star[[n_train, j]] = 1.0;
1200 }
1201
1202 let pred = extended_k_star.t().dot(&self._state.alpha);
1203
1204 let K_star_star = self._state.spatial_kernel.compute_kernel_matrix(X, None)?;
1206 let cholesky_slice = self
1207 ._state
1208 .cholesky
1209 .slice(s![0..n_train, 0..n_train])
1210 .to_owned();
1211 let v = utils::triangular_solve_matrix(&cholesky_slice, &K_star)?;
1212 let pred_var =
1213 K_star_star.diag().to_owned() - &v.map(|x| x.powi(2)).sum_axis(Axis(0));
1214
1215 (pred, pred_var.map(|x| x.max(0.0)))
1216 }
1217 };
1218
1219 Ok((predictions, variances.map(|x| x.sqrt())))
1220 }
1221
1222 pub fn correlation_structure(
1224 &self,
1225 max_distance: f64,
1226 n_points: usize,
1227 ) -> (Array1<f64>, Array1<f64>) {
1228 let distances = Array1::linspace(0.0, max_distance, n_points);
1229 let correlations = distances.map(|&d| self._state.spatial_kernel.compute_covariance(d));
1230 (distances, correlations)
1231 }
1232
1233 pub fn detect_spatial_outliers(&self, threshold: f64) -> SklResult<Vec<usize>> {
1235 let (predictions, _) = self.predict_with_variance(&self._state.training_data.0)?;
1236 let residuals = &self._state.training_data.1 - &predictions;
1237
1238 let residual_std = residuals.std(0.0);
1240 let standardized_residuals = residuals / residual_std;
1241
1242 let mut outliers = Vec::new();
1243 for (i, &residual) in standardized_residuals.iter().enumerate() {
1244 if residual.abs() > threshold {
1245 outliers.push(i);
1246 }
1247 }
1248
1249 Ok(outliers)
1250 }
1251
1252 pub fn spatial_cross_validation(&self) -> SklResult<f64> {
1254 let n = self._state.training_data.0.nrows();
1255 let mut errors = Vec::new();
1256
1257 for i in 0..n {
1258 let mut train_coords = Vec::new();
1260 let mut train_values = Vec::new();
1261
1262 for j in 0..n {
1263 if j != i {
1264 train_coords.push(self._state.training_data.0.row(j).to_vec());
1265 train_values.push(self._state.training_data.1[j]);
1266 }
1267 }
1268
1269 let train_coords_array = Array2::from_shape_vec(
1271 (n - 1, self._state.training_data.0.ncols()),
1272 train_coords.into_iter().flatten().collect(),
1273 )
1274 .map_err(|_| {
1275 SklearsError::InvalidInput("Failed to create training coordinates".to_string())
1276 })?;
1277
1278 let train_values_array = Array1::from_vec(train_values);
1279
1280 let reduced_gp = SpatialGaussianProcessRegressor::builder()
1282 .spatial_kernel(self._state.spatial_kernel.clone())
1283 .kriging_type(self._state.kriging_type)
1284 .alpha(self.alpha)
1285 .build();
1286
1287 if let Ok(fitted) = reduced_gp.fit(&train_coords_array, &train_values_array) {
1288 let test_coords = self._state.training_data.0.slice(s![i..i + 1, ..]);
1290 if let Ok(pred) = fitted.predict(&test_coords.to_owned()) {
1291 let error = (pred[0] - self._state.training_data.1[i]).powi(2);
1292 errors.push(error);
1293 }
1294 }
1295 }
1296
1297 if errors.is_empty() {
1298 return Err(SklearsError::InvalidInput(
1299 "Cross-validation failed".to_string(),
1300 ));
1301 }
1302
1303 let mse = errors.iter().sum::<f64>() / errors.len() as f64;
1304 Ok(mse.sqrt()) }
1306}
1307
1308impl Predict<Array2<f64>, Array1<f64>> for SpatialGaussianProcessRegressor<Trained> {
1309 fn predict(&self, X: &Array2<f64>) -> SklResult<Array1<f64>> {
1310 let (predictions, _) = self.predict_with_variance(X)?;
1311 Ok(predictions)
1312 }
1313}
1314
1315#[allow(non_snake_case)]
1316#[cfg(test)]
1317mod tests {
1318 use super::*;
1319 use approx::assert_abs_diff_eq;
1320 use scirs2_core::ndarray::array;
1321
1322 #[test]
1323 fn test_spatial_kernel_spherical() {
1324 let kernel = SpatialKernel::spherical(1.0, 10.0);
1325
1326 assert_abs_diff_eq!(kernel.compute_covariance(0.0), 1.0, epsilon = 1e-10);
1328
1329 let cov_5 = kernel.compute_covariance(5.0);
1331 assert!(cov_5 > 0.0 && cov_5 < 1.0);
1332
1333 let cov_15 = kernel.compute_covariance(15.0);
1335 assert_abs_diff_eq!(cov_15, 0.0, epsilon = 1e-10);
1336 }
1337
1338 #[test]
1339 fn test_spatial_kernel_exponential() {
1340 let kernel = SpatialKernel::exponential(1.0, 5.0);
1341
1342 let cov_0 = kernel.compute_covariance(0.0);
1344 let cov_5 = kernel.compute_covariance(5.0);
1345 let cov_10 = kernel.compute_covariance(10.0);
1346
1347 assert_abs_diff_eq!(cov_0, 1.0, epsilon = 1e-10);
1348 assert!(cov_5 > cov_10);
1349 assert!(cov_10 > 0.0);
1350 }
1351
1352 #[test]
1353 fn test_variogram_computation() {
1354 let coords = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]];
1355 let values = array![1.0, 2.0, 1.5, 2.5];
1356
1357 let variogram = Variogram::compute_empirical(&coords, &values, 5).unwrap();
1358
1359 assert!(variogram.distances.len() > 0);
1360 assert_eq!(variogram.distances.len(), variogram.semivariances.len());
1361 assert_eq!(variogram.distances.len(), variogram.n_pairs.len());
1362 }
1363
1364 #[test]
1365 #[ignore] fn test_spatial_gp_ordinary_kriging() {
1367 let coords = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]];
1368 let values = array![1.0, 2.0, 1.5, 2.5];
1369
1370 let spatial_gp = SpatialGaussianProcessRegressor::builder()
1371 .spatial_kernel(SpatialKernel::spherical(1.0, 2.0))
1372 .kriging_type(KrigingType::Ordinary)
1373 .build();
1374
1375 let trained = spatial_gp.fit(&coords, &values).unwrap();
1376 let predictions = trained.predict(&coords).unwrap();
1377
1378 assert_eq!(predictions.len(), coords.nrows());
1379 }
1380
1381 #[test]
1382 fn test_spatial_gp_simple_kriging() {
1383 let coords = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]];
1384 let values = array![1.0, 2.0, 1.5, 2.5];
1385
1386 let spatial_gp = SpatialGaussianProcessRegressor::builder()
1387 .spatial_kernel(SpatialKernel::exponential(1.0, 2.0))
1388 .kriging_type(KrigingType::Simple { mean: 1.75 })
1389 .build();
1390
1391 let trained = spatial_gp.fit(&coords, &values).unwrap();
1392 let predictions = trained.predict(&coords).unwrap();
1393
1394 assert_eq!(predictions.len(), coords.nrows());
1395 }
1396
1397 #[test]
1398 #[ignore] fn test_spatial_gp_with_variance() {
1400 let coords = array![[0.0, 0.0], [2.0, 0.0], [0.0, 2.0], [2.0, 2.0]];
1401 let values = array![1.0, 3.0, 2.0, 4.0];
1402
1403 let spatial_gp = SpatialGaussianProcessRegressor::builder()
1404 .spatial_kernel(SpatialKernel::gaussian(2.0, 1.0))
1405 .kriging_type(KrigingType::Ordinary)
1406 .build();
1407
1408 let trained = spatial_gp.fit(&coords, &values).unwrap();
1409
1410 let test_coords = array![[1.0, 1.0]]; let (predictions, variances) = trained.predict_with_variance(&test_coords).unwrap();
1412
1413 assert_eq!(predictions.len(), 1);
1414 assert_eq!(variances.len(), 1);
1415 assert!(variances[0] >= 0.0);
1416 }
1417
1418 #[test]
1419 fn test_spatial_kernel_with_nugget() {
1420 let kernel = SpatialKernel::spherical_with_nugget(1.0, 5.0, 0.1);
1421
1422 assert_abs_diff_eq!(kernel.compute_covariance(0.0), 1.1, epsilon = 1e-10);
1424
1425 let cov_1 = kernel.compute_covariance(1.0);
1427 assert!(cov_1 > 0.0 && cov_1 < 1.0);
1428 }
1429
1430 #[test]
1431 fn test_anisotropic_kernel() {
1432 let base_kernel = SpatialKernel::exponential(1.0, 2.0);
1433 let anisotropy_matrix = array![[2.0, 0.0], [0.0, 1.0]]; let aniso_kernel = SpatialKernel::anisotropic(base_kernel, anisotropy_matrix);
1436
1437 let coords = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]];
1438 let K = aniso_kernel.compute_kernel_matrix(&coords, None).unwrap();
1439
1440 assert_eq!(K.shape(), &[3, 3]);
1441 assert_abs_diff_eq!(
1442 K[[0, 0]],
1443 aniso_kernel.compute_covariance(0.0),
1444 epsilon = 1e-10
1445 );
1446 }
1447
1448 #[test]
1449 fn test_variogram_model_fitting() {
1450 let coords = array![[0.0, 0.0], [1.0, 0.0], [2.0, 0.0], [3.0, 0.0], [4.0, 0.0]];
1451 let values = array![1.0, 1.2, 1.8, 2.1, 2.5];
1452
1453 let mut variogram = Variogram::compute_empirical(&coords, &values, 4).unwrap();
1454 variogram.fit_model("spherical").unwrap();
1455
1456 assert!(variogram.fitted_model.is_some());
1457
1458 let goodness = variogram.goodness_of_fit().unwrap();
1459 assert!(goodness >= 0.0 && goodness <= 1.0);
1460 }
1461
1462 #[test]
1463 #[ignore] fn test_spatial_outlier_detection() {
1465 let coords = array![[0.0, 0.0], [1.0, 0.0], [2.0, 0.0], [3.0, 0.0]];
1466 let values = array![1.0, 2.0, 3.0, 10.0]; let spatial_gp = SpatialGaussianProcessRegressor::builder()
1469 .spatial_kernel(SpatialKernel::exponential(1.0, 2.0))
1470 .kriging_type(KrigingType::Ordinary)
1471 .build();
1472
1473 let trained = spatial_gp.fit(&coords, &values).unwrap();
1474 let outliers = trained.detect_spatial_outliers(2.0).unwrap();
1475
1476 assert!(!outliers.is_empty());
1478 }
1479
1480 #[test]
1481 #[ignore] fn test_spatial_cross_validation() {
1483 let coords = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0], [0.5, 0.5]];
1484 let values = array![1.0, 2.0, 1.5, 2.5, 1.8];
1485
1486 let spatial_gp = SpatialGaussianProcessRegressor::builder()
1487 .spatial_kernel(SpatialKernel::spherical(1.0, 2.0))
1488 .kriging_type(KrigingType::Ordinary)
1489 .build();
1490
1491 let trained = spatial_gp.fit(&coords, &values).unwrap();
1492 let cv_error = trained.spatial_cross_validation().unwrap();
1493
1494 assert!(cv_error >= 0.0);
1495 }
1496
1497 #[test]
1498 fn test_matern_kernel() {
1499 let kernel = SpatialKernel::matern(1.0, 2.0, 1.5);
1500
1501 assert_abs_diff_eq!(kernel.compute_covariance(0.0), 1.0, epsilon = 1e-10);
1503
1504 let cov_1 = kernel.compute_covariance(1.0);
1506 let cov_2 = kernel.compute_covariance(2.0);
1507 assert!(cov_1 > cov_2);
1508 assert!(cov_2 > 0.0);
1509 }
1510
1511 #[test]
1512 #[ignore] fn test_correlation_structure() {
1514 let coords = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]];
1515 let values = array![1.0, 2.0, 1.5];
1516
1517 let spatial_gp = SpatialGaussianProcessRegressor::builder()
1518 .spatial_kernel(SpatialKernel::exponential(1.0, 2.0))
1519 .kriging_type(KrigingType::Ordinary)
1520 .build();
1521
1522 let trained = spatial_gp.fit(&coords, &values).unwrap();
1523 let (distances, correlations) = trained.correlation_structure(5.0, 10);
1524
1525 assert_eq!(distances.len(), 10);
1526 assert_eq!(correlations.len(), 10);
1527 assert_abs_diff_eq!(correlations[0], 1.0, epsilon = 1e-10); }
1529}