1use crate::error::{SpatialError, SpatialResult};
41use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2};
42use scirs2_core::numeric::Float;
43use std::f64::consts::PI;
44
45#[derive(Debug, Clone, Copy, PartialEq)]
47pub enum VariogramModel {
48 Spherical,
50 Exponential,
52 Gaussian,
54 Linear,
56 Power,
58 Matern,
60}
61
62#[derive(Debug, Clone)]
64pub struct FittedVariogram<T: Float> {
65 pub model: VariogramModel,
67 pub range: T,
69 pub sill: T,
71 pub nugget: T,
73 pub extra_params: Vec<T>,
75 pub r_squared: T,
77}
78
79impl<T: Float> FittedVariogram<T> {
80 pub fn evaluate(&self, distance: T) -> T {
82 match self.model {
83 VariogramModel::Spherical => {
84 if distance >= self.range {
85 self.nugget + self.sill
86 } else {
87 let h_over_r = distance / self.range;
88 let three_halves = T::from(1.5).expect("conversion failed");
89 let half = T::from(0.5).expect("conversion failed");
90 self.nugget
91 + self.sill
92 * (three_halves * h_over_r - half * h_over_r * h_over_r * h_over_r)
93 }
94 }
95 VariogramModel::Exponential => {
96 self.nugget + self.sill * (T::one() - (-distance / self.range).exp())
97 }
98 VariogramModel::Gaussian => {
99 let h_over_r = distance / self.range;
100 self.nugget + self.sill * (T::one() - (-(h_over_r * h_over_r)).exp())
101 }
102 VariogramModel::Linear => {
103 let slope = if !self.extra_params.is_empty() {
104 self.extra_params[0]
105 } else {
106 self.sill / self.range
107 };
108 self.nugget + slope * distance
109 }
110 VariogramModel::Power => {
111 let power = if !self.extra_params.is_empty() {
112 self.extra_params[0]
113 } else {
114 T::from(0.5).expect("conversion failed")
115 };
116 self.nugget + self.sill * distance.powf(power)
117 }
118 VariogramModel::Matern => {
119 let nu = if !self.extra_params.is_empty() {
120 self.extra_params[0]
121 } else {
122 T::from(1.5).expect("conversion failed") };
124 if distance.is_zero() {
126 self.nugget
127 } else {
128 let scaled_dist = distance / self.range
129 * T::from(2.0).expect("conversion failed")
130 * nu.sqrt();
131 let term = (T::one() + scaled_dist) * (-scaled_dist).exp();
133 self.nugget + self.sill * (T::one() - term)
134 }
135 }
136 }
137 }
138}
139
140pub fn experimental_variogram<T: Float>(
166 coordinates: &ArrayView2<T>,
167 values: &ArrayView1<T>,
168 n_lags: usize,
169 lag_tolerance: Option<T>,
170) -> SpatialResult<(Array1<T>, Array1<T>)> {
171 let n = coordinates.shape()[0];
172
173 if n != values.len() {
174 return Err(SpatialError::DimensionError(
175 "Number of coordinates must match number of values".to_string(),
176 ));
177 }
178
179 if n < 2 {
180 return Err(SpatialError::ValueError(
181 "Need at least 2 points for variogram".to_string(),
182 ));
183 }
184
185 let mut pairs = Vec::new();
187 for i in 0..n {
188 for j in (i + 1)..n {
189 let mut dist_sq = T::zero();
190 for k in 0..coordinates.shape()[1] {
191 let diff = coordinates[[i, k]] - coordinates[[j, k]];
192 dist_sq = dist_sq + diff * diff;
193 }
194 let distance = dist_sq.sqrt();
195
196 let value_diff = values[i] - values[j];
197 let gamma = value_diff * value_diff / (T::one() + T::one()); pairs.push((distance, gamma));
200 }
201 }
202
203 if pairs.is_empty() {
204 return Err(SpatialError::ValueError("No valid pairs found".to_string()));
205 }
206
207 let max_distance = pairs
209 .iter()
210 .map(|(d, _)| *d)
211 .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
212 .ok_or_else(|| SpatialError::ValueError("Failed to find max distance".to_string()))?;
213
214 let lag_size = max_distance / T::from(n_lags).expect("conversion failed");
216 let tolerance = lag_tolerance.unwrap_or(lag_size / (T::one() + T::one()));
217
218 let mut lag_bins: Vec<Vec<T>> = vec![Vec::new(); n_lags];
220 let mut lag_centers = Array1::zeros(n_lags);
221
222 for i in 0..n_lags {
223 let lag_center = lag_size
224 * (T::from(i).expect("conversion failed") + T::from(0.5).expect("conversion failed"));
225 lag_centers[i] = lag_center;
226
227 for &(distance, gamma) in &pairs {
228 if (distance - lag_center).abs() <= tolerance {
229 lag_bins[i].push(gamma);
230 }
231 }
232 }
233
234 let mut gamma_values = Array1::zeros(n_lags);
236 let mut valid_lags = Vec::new();
237 let mut valid_gammas = Vec::new();
238
239 for i in 0..n_lags {
240 if !lag_bins[i].is_empty() {
241 let sum: T = lag_bins[i]
242 .iter()
243 .copied()
244 .fold(T::zero(), |acc, x| acc + x);
245 let mean = sum / T::from(lag_bins[i].len()).expect("conversion failed");
246 gamma_values[i] = mean;
247 valid_lags.push(lag_centers[i]);
248 valid_gammas.push(mean);
249 }
250 }
251
252 if valid_lags.is_empty() {
253 return Err(SpatialError::ValueError(
254 "No valid lags computed".to_string(),
255 ));
256 }
257
258 let lags_array = Array1::from_vec(valid_lags);
260 let gamma_array = Array1::from_vec(valid_gammas);
261
262 Ok((lags_array, gamma_array))
263}
264
265pub fn fit_variogram<T: Float>(
297 lags: &Array1<T>,
298 gamma: &Array1<T>,
299 model: VariogramModel,
300) -> SpatialResult<FittedVariogram<T>> {
301 if lags.len() != gamma.len() {
302 return Err(SpatialError::DimensionError(
303 "Lags and gamma must have same length".to_string(),
304 ));
305 }
306
307 if lags.is_empty() {
308 return Err(SpatialError::ValueError(
309 "Need at least one lag-gamma pair".to_string(),
310 ));
311 }
312
313 let zero = T::zero();
314 let one = T::one();
315
316 let max_lag = lags
319 .iter()
320 .copied()
321 .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
322 .ok_or_else(|| SpatialError::ValueError("Failed to find max lag".to_string()))?;
323
324 let max_gamma = gamma
325 .iter()
326 .copied()
327 .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
328 .ok_or_else(|| SpatialError::ValueError("Failed to find max gamma".to_string()))?;
329
330 let safe_max_lag = if max_lag > zero { max_lag } else { one };
335 let safe_max_gamma = if max_gamma > zero { max_gamma } else { one };
336 let positive_floor =
337 (safe_max_lag.min(safe_max_gamma) * float_lit(1e-6, zero)).max(float_lit(1e-30, zero));
338
339 let initial_range = (max_lag * float_lit(0.7, zero)).max(positive_floor);
340 let initial_sill = (max_gamma * float_lit(0.9, zero)).max(positive_floor);
341 let initial_nugget = (gamma[0] * float_lit(0.1, zero)).max(zero);
342
343 let (mut params, lower_bounds) = initial_params(
344 model,
345 initial_nugget,
346 initial_sill,
347 initial_range,
348 positive_floor,
349 );
350
351 levenberg_marquardt_fit(model, lags, gamma, &mut params, &lower_bounds);
352
353 let mut fitted = params_to_fitted(model, ¶ms);
354
355 let mean_gamma = gamma.sum() / T::from(gamma.len()).unwrap_or(one);
357 let mut ss_res = zero;
358 let mut ss_tot = zero;
359
360 for i in 0..lags.len() {
361 let predicted = fitted.evaluate(lags[i]);
362 let residual = gamma[i] - predicted;
363 ss_res = ss_res + residual * residual;
364
365 let deviation = gamma[i] - mean_gamma;
366 ss_tot = ss_tot + deviation * deviation;
367 }
368
369 fitted.r_squared = if ss_tot > zero {
370 one - ss_res / ss_tot
371 } else {
372 zero
373 };
374
375 Ok(fitted)
376}
377
378fn float_lit<T: Float>(x: f64, fallback: T) -> T {
382 T::from(x).unwrap_or(fallback)
383}
384
385fn initial_params<T: Float>(
394 model: VariogramModel,
395 nugget0: T,
396 sill0: T,
397 range0: T,
398 positive_floor: T,
399) -> (Vec<T>, Vec<T>) {
400 let zero = T::zero();
401 match model {
402 VariogramModel::Linear => {
403 let slope0 = if range0 > zero { sill0 / range0 } else { sill0 };
404 (
405 vec![nugget0, slope0.max(positive_floor)],
406 vec![zero, positive_floor],
407 )
408 }
409 VariogramModel::Power => {
410 let power0 = float_lit(0.5, T::one());
411 (
412 vec![nugget0, sill0.max(positive_floor), power0],
413 vec![zero, positive_floor, positive_floor],
414 )
415 }
416 _ => (
417 vec![
418 nugget0,
419 sill0.max(positive_floor),
420 range0.max(positive_floor),
421 ],
422 vec![zero, positive_floor, positive_floor],
423 ),
424 }
425}
426
427fn params_to_fitted<T: Float>(model: VariogramModel, params: &[T]) -> FittedVariogram<T> {
432 match model {
433 VariogramModel::Linear => FittedVariogram {
434 model,
435 range: T::one(),
436 sill: params[1],
437 nugget: params[0],
438 extra_params: vec![params[1]],
439 r_squared: T::zero(),
440 },
441 VariogramModel::Power => FittedVariogram {
442 model,
443 range: T::one(),
444 sill: params[1],
445 nugget: params[0],
446 extra_params: vec![params[2]],
447 r_squared: T::zero(),
448 },
449 _ => FittedVariogram {
450 model,
451 range: params[2],
452 sill: params[1],
453 nugget: params[0],
454 extra_params: vec![],
455 r_squared: T::zero(),
456 },
457 }
458}
459
460fn levenberg_marquardt_fit<T: Float>(
470 model: VariogramModel,
471 lags: &Array1<T>,
472 gamma: &Array1<T>,
473 params: &mut Vec<T>,
474 lower_bounds: &[T],
475) {
476 let n = params.len();
477 let m = lags.len();
478 if m == 0 || n == 0 {
479 return;
480 }
481
482 let zero = T::zero();
483 let one = T::one();
484
485 let evaluate_cost = |p: &[T]| -> (T, Vec<T>) {
486 let candidate = params_to_fitted(model, p);
487 let mut residuals = Vec::with_capacity(m);
488 let mut sse = zero;
489 for i in 0..m {
490 let r = gamma[i] - candidate.evaluate(lags[i]);
491 sse = sse + r * r;
492 residuals.push(r);
493 }
494 (sse, residuals)
495 };
496
497 let relative_step = float_lit(1e-6, zero);
498 let mut lambda = float_lit(1e-3, zero);
499 let lambda_up = float_lit(10.0, one);
500 let lambda_down = float_lit(0.1, one);
501 let min_lambda = float_lit(1e-12, zero);
502 let max_iters = 200;
503 let max_lambda_attempts = 30;
504
505 let (mut current_cost, mut residuals) = evaluate_cost(params);
506
507 for _ in 0..max_iters {
508 let mut jac = vec![zero; m * n];
510 for j in 0..n {
511 let base = params[j].abs();
512 let step = if base > zero {
513 base * relative_step
514 } else {
515 relative_step
516 };
517 let mut p_plus = params.clone();
518 let mut p_minus = params.clone();
519 p_plus[j] = p_plus[j] + step;
520 p_minus[j] = p_minus[j] - step;
521
522 let cand_plus = params_to_fitted(model, &p_plus);
523 let cand_minus = params_to_fitted(model, &p_minus);
524 let two_step = step + step;
525 for i in 0..m {
526 let f_plus = cand_plus.evaluate(lags[i]);
527 let f_minus = cand_minus.evaluate(lags[i]);
528 jac[i * n + j] = (f_plus - f_minus) / two_step;
529 }
530 }
531
532 let mut jtj = vec![zero; n * n];
534 let mut jtr = vec![zero; n];
535 for i in 0..m {
536 for a in 0..n {
537 jtr[a] = jtr[a] + jac[i * n + a] * residuals[i];
538 for b in 0..n {
539 jtj[a * n + b] = jtj[a * n + b] + jac[i * n + a] * jac[i * n + b];
540 }
541 }
542 }
543
544 let mut attempt_lambda = lambda;
545 let mut improved = false;
546
547 for _ in 0..max_lambda_attempts {
548 let mut damped = jtj.clone();
549 for d in 0..n {
550 let diag = jtj[d * n + d];
551 damped[d * n + d] = if diag > zero {
552 diag * (one + attempt_lambda)
553 } else {
554 attempt_lambda.max(min_lambda)
555 };
556 }
557
558 if let Some(delta) = solve_linear_system(&damped, &jtr, n) {
559 let mut candidate_params = params.clone();
560 for (k, slot) in candidate_params.iter_mut().enumerate() {
561 let updated = *slot + delta[k];
562 *slot = if updated < lower_bounds[k] {
563 lower_bounds[k]
564 } else {
565 updated
566 };
567 }
568
569 let (candidate_cost, candidate_residuals) = evaluate_cost(&candidate_params);
570 if candidate_cost < current_cost {
571 *params = candidate_params;
572 residuals = candidate_residuals;
573 current_cost = candidate_cost;
574 lambda = (attempt_lambda * lambda_down).max(min_lambda);
575 improved = true;
576 break;
577 }
578 }
579
580 attempt_lambda = attempt_lambda * lambda_up;
581 }
582
583 if !improved {
584 break;
587 }
588 }
589}
590
591fn solve_linear_system<T: Float>(a: &[T], b: &[T], n: usize) -> Option<Vec<T>> {
595 let zero = T::zero();
596 let pivot_floor = float_lit(1e-300, zero);
597 let mut aug = vec![zero; n * (n + 1)];
598 for i in 0..n {
599 aug[i * (n + 1)..i * (n + 1) + n].copy_from_slice(&a[i * n..i * n + n]);
600 aug[i * (n + 1) + n] = b[i];
601 }
602
603 for col in 0..n {
604 let mut pivot_row = col;
605 let mut pivot_val = aug[col * (n + 1) + col].abs();
606 for row in (col + 1)..n {
607 let val = aug[row * (n + 1) + col].abs();
608 if val > pivot_val {
609 pivot_val = val;
610 pivot_row = row;
611 }
612 }
613 if pivot_val <= pivot_floor {
614 return None;
615 }
616 if pivot_row != col {
617 for k in 0..(n + 1) {
618 aug.swap(col * (n + 1) + k, pivot_row * (n + 1) + k);
619 }
620 }
621
622 let pivot = aug[col * (n + 1) + col];
623 for row in (col + 1)..n {
624 let factor = aug[row * (n + 1) + col] / pivot;
625 if factor.is_zero() {
626 continue;
627 }
628 for k in col..(n + 1) {
629 let sub = factor * aug[col * (n + 1) + k];
630 aug[row * (n + 1) + k] = aug[row * (n + 1) + k] - sub;
631 }
632 }
633 }
634
635 let mut x = vec![zero; n];
636 for row in (0..n).rev() {
637 let mut sum = aug[row * (n + 1) + n];
638 for (col, &xc) in x.iter().enumerate().take(n).skip(row + 1) {
639 sum = sum - aug[row * (n + 1) + col] * xc;
640 }
641 let diag = aug[row * (n + 1) + row];
642 if diag.abs() <= pivot_floor {
643 return None;
644 }
645 x[row] = sum / diag;
646 }
647
648 Some(x)
649}
650
651pub fn directional_variogram<T: Float>(
668 coordinates: &ArrayView2<T>,
669 values: &ArrayView1<T>,
670 direction: T,
671 tolerance: T,
672 n_lags: usize,
673) -> SpatialResult<(Array1<T>, Array1<T>)> {
674 let n = coordinates.shape()[0];
675
676 if coordinates.shape()[1] != 2 {
677 return Err(SpatialError::DimensionError(
678 "Directional variogram requires 2D coordinates".to_string(),
679 ));
680 }
681
682 if n != values.len() {
683 return Err(SpatialError::DimensionError(
684 "Number of coordinates must match number of values".to_string(),
685 ));
686 }
687
688 let mut pairs = Vec::new();
690 for i in 0..n {
691 for j in (i + 1)..n {
692 let dx = coordinates[[j, 0]] - coordinates[[i, 0]];
693 let dy = coordinates[[j, 1]] - coordinates[[i, 1]];
694
695 let distance = (dx * dx + dy * dy).sqrt();
696 let angle = dy.atan2(dx); let angle_diff = (angle - direction).abs();
700 let pi_t = T::from(PI).expect("conversion failed");
701 let angle_diff_wrapped = if angle_diff > pi_t {
702 (T::one() + T::one()) * pi_t - angle_diff
703 } else {
704 angle_diff
705 };
706
707 if angle_diff_wrapped <= tolerance {
708 let value_diff = values[i] - values[j];
709 let gamma = value_diff * value_diff / (T::one() + T::one());
710 pairs.push((distance, gamma));
711 }
712 }
713 }
714
715 if pairs.is_empty() {
716 return Err(SpatialError::ValueError(
717 "No pairs found in specified direction".to_string(),
718 ));
719 }
720
721 let max_distance = pairs
724 .iter()
725 .map(|(d, _)| *d)
726 .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
727 .ok_or_else(|| SpatialError::ValueError("Failed to find max distance".to_string()))?;
728
729 let lag_size = max_distance / T::from(n_lags).expect("conversion failed");
730 let mut lag_bins: Vec<Vec<T>> = vec![Vec::new(); n_lags];
731 let mut lag_centers = Array1::zeros(n_lags);
732
733 for i in 0..n_lags {
734 let lag_center = lag_size
735 * (T::from(i).expect("conversion failed") + T::from(0.5).expect("conversion failed"));
736 lag_centers[i] = lag_center;
737
738 for &(distance, gamma) in &pairs {
739 let lag_tolerance = lag_size / (T::one() + T::one());
740 if (distance - lag_center).abs() <= lag_tolerance {
741 lag_bins[i].push(gamma);
742 }
743 }
744 }
745
746 let mut valid_lags = Vec::new();
747 let mut valid_gammas = Vec::new();
748
749 for i in 0..n_lags {
750 if !lag_bins[i].is_empty() {
751 let sum: T = lag_bins[i]
752 .iter()
753 .copied()
754 .fold(T::zero(), |acc, x| acc + x);
755 let mean = sum / T::from(lag_bins[i].len()).expect("conversion failed");
756 valid_lags.push(lag_centers[i]);
757 valid_gammas.push(mean);
758 }
759 }
760
761 Ok((Array1::from_vec(valid_lags), Array1::from_vec(valid_gammas)))
762}
763
764#[cfg(test)]
765mod tests {
766 use super::*;
767 use approx::assert_relative_eq;
768 use scirs2_core::ndarray::array;
769
770 #[test]
771 fn test_experimental_variogram() {
772 let coords = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]];
773 let values = array![1.0, 2.0, 1.5, 2.5];
774
775 let result = experimental_variogram(&coords.view(), &values.view(), 5, None);
776 assert!(result.is_ok());
777
778 let (lags, gamma) = result.expect("computation failed");
779 assert!(!lags.is_empty());
780 assert_eq!(lags.len(), gamma.len());
781
782 for &g in gamma.iter() {
784 assert!(g >= 0.0);
785 }
786 }
787
788 #[test]
789 fn test_fit_spherical_variogram() {
790 let lags = array![0.5, 1.0, 1.5, 2.0, 2.5];
791 let gamma = array![0.1, 0.3, 0.6, 0.85, 0.95];
792
793 let fitted = fit_variogram(&lags, &gamma, VariogramModel::Spherical);
794 assert!(fitted.is_ok());
795
796 let model = fitted.expect("fitting failed");
797 assert!(model.range > 0.0);
798 assert!(model.sill > 0.0);
799 assert!(model.nugget >= 0.0);
800 }
801
802 #[test]
803 fn test_fit_exponential_variogram() {
804 let lags = array![0.5, 1.0, 1.5, 2.0, 2.5];
805 let gamma = array![0.2, 0.4, 0.6, 0.75, 0.85];
806
807 let fitted = fit_variogram(&lags, &gamma, VariogramModel::Exponential);
808 assert!(fitted.is_ok());
809
810 let model = fitted.expect("fitting failed");
811 assert!(model.range > 0.0);
812 assert!(model.sill > 0.0);
813 }
814
815 #[test]
824 fn test_fit_variogram_recovers_known_parameters() {
825 let true_model = FittedVariogram {
826 model: VariogramModel::Spherical,
827 range: 12.0,
828 sill: 4.0,
829 nugget: 0.5,
830 extra_params: vec![],
831 r_squared: 1.0,
832 };
833
834 let lags = array![
835 0.5, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 12.0, 15.0, 18.0, 22.0, 30.0
836 ];
837 let gamma = lags.mapv(|h| true_model.evaluate(h));
838
839 let fitted = fit_variogram(&lags, &gamma, VariogramModel::Spherical)
840 .expect("fit should succeed on noiseless synthetic data");
841
842 assert_relative_eq!(fitted.range, 12.0, epsilon = 0.3);
846 assert_relative_eq!(fitted.sill, 4.0, epsilon = 0.1);
847 assert_relative_eq!(fitted.nugget, 0.5, epsilon = 0.1);
848 assert!(
849 fitted.r_squared > 0.999,
850 "expected near-perfect fit on noiseless data, got r_squared = {}",
851 fitted.r_squared
852 );
853 }
854
855 #[test]
856 fn test_variogram_evaluate() {
857 let fitted = FittedVariogram {
858 model: VariogramModel::Spherical,
859 range: 2.0,
860 sill: 1.0,
861 nugget: 0.1,
862 extra_params: vec![],
863 r_squared: 0.95,
864 };
865
866 let gamma_0 = fitted.evaluate(0.0);
868 assert_relative_eq!(gamma_0, 0.1, epsilon = 0.01);
869
870 let gamma_range = fitted.evaluate(2.0);
872 assert!(gamma_range >= 1.0);
873 assert!(gamma_range <= 1.2);
874
875 let gamma_beyond = fitted.evaluate(5.0);
877 assert_relative_eq!(gamma_beyond, 1.1, epsilon = 0.01);
878 }
879
880 #[test]
881 fn test_directional_variogram() {
882 let coords = array![
883 [0.0, 0.0],
884 [1.0, 0.0],
885 [2.0, 0.0],
886 [0.0, 1.0],
887 [1.0, 1.0],
888 [2.0, 1.0]
889 ];
890 let values = array![1.0, 1.5, 2.0, 1.2, 1.7, 2.2];
891
892 let result = directional_variogram(
894 &coords.view(),
895 &values.view(),
896 0.0,
897 std::f64::consts::PI / 4.0, 5,
899 );
900
901 assert!(result.is_ok());
902 let (lags, gamma) = result.expect("computation failed");
903 assert!(!lags.is_empty());
904 }
905
906 #[test]
907 fn test_variogram_models() {
908 let models = vec![
909 VariogramModel::Spherical,
910 VariogramModel::Exponential,
911 VariogramModel::Gaussian,
912 VariogramModel::Linear,
913 ];
914
915 for model in models {
916 let fitted = FittedVariogram {
917 model,
918 range: 1.0,
919 sill: 1.0,
920 nugget: 0.0,
921 extra_params: vec![],
922 r_squared: 0.0,
923 };
924
925 let gamma_1 = fitted.evaluate(0.5);
927 let gamma_2 = fitted.evaluate(1.0);
928 assert!(gamma_2 >= gamma_1);
929 }
930 }
931}