1use crate::error::{AnalyticsError, Result};
7use scirs2_core::ndarray::{Array1, Array2, ArrayView1};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum KrigingType {
12 Ordinary,
14 Universal,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum VariogramModel {
21 Spherical,
23 Exponential,
25 Gaussian,
27 Linear,
29}
30
31#[derive(Debug, Clone, Copy)]
33pub struct Variogram {
34 pub nugget: f64,
36 pub sill: f64,
38 pub range: f64,
40 pub model: VariogramModel,
42}
43
44impl Variogram {
45 pub fn new(model: VariogramModel, nugget: f64, sill: f64, range: f64) -> Self {
47 Self {
48 nugget,
49 sill,
50 range,
51 model,
52 }
53 }
54
55 pub fn evaluate(&self, h: f64) -> f64 {
57 if h < f64::EPSILON {
58 return 0.0;
59 }
60
61 let partial_sill = self.sill - self.nugget;
62
63 match self.model {
64 VariogramModel::Spherical => {
65 if h >= self.range {
66 self.sill
67 } else {
68 let h_r = h / self.range;
69 self.nugget + partial_sill * (1.5 * h_r - 0.5 * h_r.powi(3))
70 }
71 }
72 VariogramModel::Exponential => {
73 self.nugget + partial_sill * (1.0 - (-h / self.range).exp())
74 }
75 VariogramModel::Gaussian => {
76 self.nugget + partial_sill * (1.0 - (-(h * h) / (self.range * self.range)).exp())
77 }
78 VariogramModel::Linear => {
79 let slope = self.sill / self.range;
80 self.nugget + slope * h.min(self.range)
81 }
82 }
83 }
84}
85
86#[derive(Debug, Clone)]
88pub struct KrigingResult {
89 pub values: Array1<f64>,
91 pub variances: Array1<f64>,
93 pub coordinates: Array2<f64>,
95}
96
97pub struct KrigingInterpolator {
99 kriging_type: KrigingType,
100 variogram: Variogram,
101}
102
103impl KrigingInterpolator {
104 pub fn new(kriging_type: KrigingType, variogram: Variogram) -> Self {
110 Self {
111 kriging_type,
112 variogram,
113 }
114 }
115
116 pub fn interpolate(
126 &self,
127 points: &Array2<f64>,
128 values: &ArrayView1<f64>,
129 targets: &Array2<f64>,
130 ) -> Result<KrigingResult> {
131 let n_points = points.nrows();
132 let n_targets = targets.nrows();
133
134 if values.len() != n_points {
135 return Err(AnalyticsError::dimension_mismatch(
136 format!("{}", n_points),
137 format!("{}", values.len()),
138 ));
139 }
140
141 if targets.ncols() != points.ncols() {
142 return Err(AnalyticsError::dimension_mismatch(
143 format!("{}", points.ncols()),
144 format!("{}", targets.ncols()),
145 ));
146 }
147
148 let cov_matrix = self.build_covariance_matrix(points)?;
150
151 let weights_matrix = self.solve_kriging_system(&cov_matrix)?;
153
154 let mut interpolated = Array1::zeros(n_targets);
155 let mut variances = Array1::zeros(n_targets);
156
157 for i in 0..n_targets {
158 let target = targets.row(i);
159 let (value, variance) =
160 self.interpolate_point(&target, points, values, &weights_matrix)?;
161 interpolated[i] = value;
162 variances[i] = variance;
163 }
164
165 Ok(KrigingResult {
166 values: interpolated,
167 variances,
168 coordinates: targets.clone(),
169 })
170 }
171
172 fn build_covariance_matrix(&self, points: &Array2<f64>) -> Result<Array2<f64>> {
174 let n = points.nrows();
175 let size = match self.kriging_type {
176 KrigingType::Ordinary => n + 1, KrigingType::Universal => n + 4, };
179
180 let mut matrix = Array2::zeros((size, size));
181
182 for i in 0..n {
184 for j in 0..n {
185 let dist = self.calculate_distance(&points.row(i), &points.row(j))?;
186 let covariance = self.variogram.sill - self.variogram.evaluate(dist);
187 matrix[[i, j]] = covariance;
188 }
189 }
190
191 match self.kriging_type {
193 KrigingType::Ordinary => {
194 for i in 0..n {
196 matrix[[i, n]] = 1.0;
197 matrix[[n, i]] = 1.0;
198 }
199 }
200 KrigingType::Universal => {
201 for i in 0..n {
203 let x = points[[i, 0]];
204 let y = points[[i, 1]];
205 matrix[[i, n]] = 1.0;
206 matrix[[n, i]] = 1.0;
207 matrix[[i, n + 1]] = x;
208 matrix[[n + 1, i]] = x;
209 matrix[[i, n + 2]] = y;
210 matrix[[n + 2, i]] = y;
211 matrix[[i, n + 3]] = x * y;
212 matrix[[n + 3, i]] = x * y;
213 }
214 }
215 }
216
217 Ok(matrix)
218 }
219
220 fn solve_kriging_system(&self, cov_matrix: &Array2<f64>) -> Result<Array2<f64>> {
222 self.matrix_inverse(cov_matrix)
225 }
226
227 fn matrix_inverse(&self, matrix: &Array2<f64>) -> Result<Array2<f64>> {
229 let n = matrix.nrows();
230 if n != matrix.ncols() {
231 return Err(AnalyticsError::matrix_error("Matrix must be square"));
232 }
233
234 let mut aug = Array2::zeros((n, 2 * n));
236 for i in 0..n {
237 for j in 0..n {
238 aug[[i, j]] = matrix[[i, j]];
239 }
240 aug[[i, n + i]] = 1.0;
241 }
242
243 for i in 0..n {
245 let mut max_row = i;
247 let mut max_val = aug[[i, i]].abs();
248 for k in (i + 1)..n {
249 if aug[[k, i]].abs() > max_val {
250 max_val = aug[[k, i]].abs();
251 max_row = k;
252 }
253 }
254
255 if max_val < f64::EPSILON {
256 return Err(AnalyticsError::matrix_error("Matrix is singular"));
257 }
258
259 if max_row != i {
261 for j in 0..(2 * n) {
262 let tmp = aug[[i, j]];
263 aug[[i, j]] = aug[[max_row, j]];
264 aug[[max_row, j]] = tmp;
265 }
266 }
267
268 let pivot = aug[[i, i]];
270 for j in 0..(2 * n) {
271 aug[[i, j]] /= pivot;
272 }
273
274 for k in 0..n {
275 if k != i {
276 let factor = aug[[k, i]];
277 for j in 0..(2 * n) {
278 aug[[k, j]] -= factor * aug[[i, j]];
279 }
280 }
281 }
282 }
283
284 let mut inverse = Array2::zeros((n, n));
286 for i in 0..n {
287 for j in 0..n {
288 inverse[[i, j]] = aug[[i, n + j]];
289 }
290 }
291
292 Ok(inverse)
293 }
294
295 fn interpolate_point(
297 &self,
298 target: &scirs2_core::ndarray::ArrayView1<f64>,
299 points: &Array2<f64>,
300 values: &ArrayView1<f64>,
301 weights_matrix: &Array2<f64>,
302 ) -> Result<(f64, f64)> {
303 let n = points.nrows();
304
305 let rhs_size = match self.kriging_type {
307 KrigingType::Ordinary => n + 1,
308 KrigingType::Universal => n + 4,
309 };
310
311 let mut rhs = Array1::zeros(rhs_size);
312
313 for i in 0..n {
315 let dist = self.calculate_distance(&points.row(i), target)?;
316 rhs[i] = self.variogram.sill - self.variogram.evaluate(dist);
317 }
318
319 match self.kriging_type {
321 KrigingType::Ordinary => {
322 rhs[n] = 1.0;
323 }
324 KrigingType::Universal => {
325 rhs[n] = 1.0;
326 rhs[n + 1] = target[0];
327 rhs[n + 2] = target[1];
328 rhs[n + 3] = target[0] * target[1];
329 }
330 }
331
332 let mut weights: Array1<f64> = Array1::zeros(rhs_size);
334 for i in 0..rhs_size {
335 for j in 0..rhs_size {
336 weights[i] += weights_matrix[[i, j]] * rhs[j];
337 }
338 }
339
340 let mut value: f64 = 0.0;
342 for i in 0..n {
343 value += weights[i] * values[i];
344 }
345
346 let mut variance = self.variogram.sill;
348 for i in 0..rhs_size {
349 variance -= weights[i] * rhs[i];
350 }
351
352 Ok((value, variance.max(0.0)))
353 }
354
355 fn calculate_distance(
357 &self,
358 p1: &scirs2_core::ndarray::ArrayView1<f64>,
359 p2: &scirs2_core::ndarray::ArrayView1<f64>,
360 ) -> Result<f64> {
361 if p1.len() != p2.len() {
362 return Err(AnalyticsError::dimension_mismatch(
363 format!("{}", p1.len()),
364 format!("{}", p2.len()),
365 ));
366 }
367
368 let dist_sq: f64 = p1.iter().zip(p2.iter()).map(|(a, b)| (a - b).powi(2)).sum();
369 Ok(dist_sq.sqrt())
370 }
371}
372
373pub struct SemivariogramCalculator;
375
376impl SemivariogramCalculator {
377 pub fn calculate(
387 points: &Array2<f64>,
388 values: &ArrayView1<f64>,
389 n_bins: usize,
390 ) -> Result<(Array1<f64>, Array1<f64>)> {
391 let n = points.nrows();
392 if values.len() != n {
393 return Err(AnalyticsError::dimension_mismatch(
394 format!("{}", n),
395 format!("{}", values.len()),
396 ));
397 }
398
399 let mut pairs = Vec::new();
401 for i in 0..n {
402 for j in (i + 1)..n {
403 let mut dist_sq = 0.0;
404 for k in 0..points.ncols() {
405 let diff = points[[i, k]] - points[[j, k]];
406 dist_sq += diff * diff;
407 }
408 let dist = dist_sq.sqrt();
409 let semivar = 0.5 * (values[i] - values[j]).powi(2);
410 pairs.push((dist, semivar));
411 }
412 }
413
414 if pairs.is_empty() {
415 return Err(AnalyticsError::insufficient_data("Need at least 2 points"));
416 }
417
418 let max_dist = pairs
420 .iter()
421 .map(|(d, _)| *d)
422 .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
423 .ok_or_else(|| AnalyticsError::insufficient_data("No valid distances"))?;
424
425 let bin_width = max_dist / (n_bins as f64);
426
427 let mut bin_sums = vec![0.0; n_bins];
429 let mut bin_counts = vec![0usize; n_bins];
430
431 for (dist, semivar) in pairs {
432 let bin = ((dist / bin_width).floor() as usize).min(n_bins - 1);
433 bin_sums[bin] += semivar;
434 bin_counts[bin] += 1;
435 }
436
437 let mut distances = Vec::new();
439 let mut semivariances = Vec::new();
440
441 for i in 0..n_bins {
442 if bin_counts[i] > 0 {
443 distances.push((i as f64 + 0.5) * bin_width);
444 semivariances.push(bin_sums[i] / (bin_counts[i] as f64));
445 }
446 }
447
448 Ok((Array1::from_vec(distances), Array1::from_vec(semivariances)))
449 }
450}
451
452use scirs2_core::ndarray::ArrayView2;
459
460#[derive(Debug, Clone)]
466pub enum DriftBasis {
467 Constant,
469 Linear,
471 Quadratic,
473 External(Vec<f64>),
476}
477
478#[derive(Debug, Clone)]
483pub struct UniversalKrigingOptions {
484 pub drift_bases: Vec<DriftBasis>,
486 pub variogram: Variogram,
488 pub regularization: f64,
490}
491
492impl Default for UniversalKrigingOptions {
493 fn default() -> Self {
494 Self {
495 drift_bases: vec![DriftBasis::Constant],
496 variogram: Variogram::new(VariogramModel::Spherical, 0.0, 1.0, 1.0),
497 regularization: 1e-10,
498 }
499 }
500}
501
502#[derive(Debug, Clone)]
504pub struct UniversalKrigingResult {
505 pub predicted: Vec<f64>,
507 pub variance: Vec<f64>,
509 pub drift_coefficients: Vec<f64>,
512}
513
514fn drift_basis_columns(basis: &DriftBasis) -> usize {
516 match basis {
517 DriftBasis::Constant => 1,
518 DriftBasis::Linear => 3,
519 DriftBasis::Quadratic => 6,
520 DriftBasis::External(_) => 1,
521 }
522}
523
524fn fill_drift_row(bases: &[DriftBasis], sample_index: usize, x: f64, y: f64, row: &mut [f64]) {
526 let mut col = 0usize;
527 for basis in bases {
528 match basis {
529 DriftBasis::Constant => {
530 row[col] = 1.0;
531 col += 1;
532 }
533 DriftBasis::Linear => {
534 row[col] = 1.0;
535 row[col + 1] = x;
536 row[col + 2] = y;
537 col += 3;
538 }
539 DriftBasis::Quadratic => {
540 row[col] = 1.0;
541 row[col + 1] = x;
542 row[col + 2] = y;
543 row[col + 3] = x * x;
544 row[col + 4] = x * y;
545 row[col + 5] = y * y;
546 col += 6;
547 }
548 DriftBasis::External(values) => {
549 row[col] = values[sample_index];
550 col += 1;
551 }
552 }
553 }
554}
555
556fn fill_query_drift_row(
559 bases: &[DriftBasis],
560 x: f64,
561 y: f64,
562 external_row: Option<&[f64]>,
563 f0: &mut [f64],
564) -> Result<()> {
565 let mut col = 0usize;
566 let mut external_used = 0usize;
567 for basis in bases {
568 match basis {
569 DriftBasis::Constant => {
570 f0[col] = 1.0;
571 col += 1;
572 }
573 DriftBasis::Linear => {
574 f0[col] = 1.0;
575 f0[col + 1] = x;
576 f0[col + 2] = y;
577 col += 3;
578 }
579 DriftBasis::Quadratic => {
580 f0[col] = 1.0;
581 f0[col + 1] = x;
582 f0[col + 2] = y;
583 f0[col + 3] = x * x;
584 f0[col + 4] = x * y;
585 f0[col + 5] = y * y;
586 col += 6;
587 }
588 DriftBasis::External(_) => {
589 let row = external_row.ok_or_else(|| {
590 AnalyticsError::invalid_input(
591 "query_external_drift required for DriftBasis::External",
592 )
593 })?;
594 if external_used >= row.len() {
595 return Err(AnalyticsError::dimension_mismatch(
596 format!(">= {}", external_used + 1),
597 format!("{}", row.len()),
598 ));
599 }
600 f0[col] = row[external_used];
601 external_used += 1;
602 col += 1;
603 }
604 }
605 }
606 Ok(())
607}
608
609fn squared_distance_2d(ax: f64, ay: f64, bx: f64, by: f64) -> f64 {
611 let dx = ax - bx;
612 let dy = ay - by;
613 dx * dx + dy * dy
614}
615
616fn gauss_jordan_invert_uked(matrix: &Array2<f64>) -> Option<Array2<f64>> {
619 let n = matrix.nrows();
620 if n != matrix.ncols() || n == 0 {
621 return None;
622 }
623
624 let mut aug = Array2::<f64>::zeros((n, 2 * n));
625 for i in 0..n {
626 for j in 0..n {
627 aug[[i, j]] = matrix[[i, j]];
628 }
629 aug[[i, n + i]] = 1.0;
630 }
631
632 for i in 0..n {
633 let mut max_row = i;
634 let mut max_val = aug[[i, i]].abs();
635 for k in (i + 1)..n {
636 let v = aug[[k, i]].abs();
637 if v > max_val {
638 max_val = v;
639 max_row = k;
640 }
641 }
642
643 if max_val < f64::EPSILON {
644 return None;
645 }
646
647 if max_row != i {
648 for j in 0..(2 * n) {
649 let tmp = aug[[i, j]];
650 aug[[i, j]] = aug[[max_row, j]];
651 aug[[max_row, j]] = tmp;
652 }
653 }
654
655 let pivot = aug[[i, i]];
656 for j in 0..(2 * n) {
657 aug[[i, j]] /= pivot;
658 }
659
660 for k in 0..n {
661 if k != i {
662 let factor = aug[[k, i]];
663 if factor != 0.0 {
664 for j in 0..(2 * n) {
665 aug[[k, j]] -= factor * aug[[i, j]];
666 }
667 }
668 }
669 }
670 }
671
672 let mut inverse = Array2::<f64>::zeros((n, n));
673 for i in 0..n {
674 for j in 0..n {
675 inverse[[i, j]] = aug[[i, n + j]];
676 }
677 }
678 Some(inverse)
679}
680
681pub fn universal_kriging_fit(
707 coords: ArrayView2<f64>,
708 values: ArrayView1<f64>,
709 options: &UniversalKrigingOptions,
710 query_points: ArrayView2<f64>,
711 query_external_drift: Option<ArrayView2<f64>>,
712) -> Result<UniversalKrigingResult> {
713 let n = values.len();
714 if coords.nrows() != n {
715 return Err(AnalyticsError::dimension_mismatch(
716 format!("{}", n),
717 format!("{}", coords.nrows()),
718 ));
719 }
720 if coords.ncols() != 2 {
721 return Err(AnalyticsError::dimension_mismatch(
722 "2",
723 format!("{}", coords.ncols()),
724 ));
725 }
726 if query_points.ncols() != 2 {
727 return Err(AnalyticsError::dimension_mismatch(
728 "2",
729 format!("{}", query_points.ncols()),
730 ));
731 }
732 if options.drift_bases.is_empty() {
733 return Err(AnalyticsError::invalid_input(
734 "UniversalKrigingOptions.drift_bases must not be empty",
735 ));
736 }
737 if !options.regularization.is_finite() || options.regularization < 0.0 {
738 return Err(AnalyticsError::invalid_parameter(
739 "regularization",
740 "must be a non-negative finite number",
741 ));
742 }
743
744 let mut external_basis_count = 0usize;
746 for basis in &options.drift_bases {
747 if let DriftBasis::External(v) = basis {
748 if v.len() != n {
749 return Err(AnalyticsError::dimension_mismatch(
750 format!("{}", n),
751 format!("{}", v.len()),
752 ));
753 }
754 external_basis_count += 1;
755 }
756 }
757
758 let p: usize = options.drift_bases.iter().map(drift_basis_columns).sum();
760 if p == 0 {
761 return Err(AnalyticsError::invalid_input(
762 "drift bases must contribute at least one column",
763 ));
764 }
765 if n < p {
766 return Err(AnalyticsError::insufficient_data(format!(
767 "universal kriging needs at least p={p} samples, got n={n}"
768 )));
769 }
770
771 let m = query_points.nrows();
773 if external_basis_count > 0 {
774 let qed = query_external_drift.ok_or_else(|| {
775 AnalyticsError::invalid_input(
776 "query_external_drift required when DriftBasis::External is used",
777 )
778 })?;
779 if qed.nrows() != m {
780 return Err(AnalyticsError::dimension_mismatch(
781 format!("{}", m),
782 format!("{}", qed.nrows()),
783 ));
784 }
785 if qed.ncols() != external_basis_count {
786 return Err(AnalyticsError::dimension_mismatch(
787 format!("{}", external_basis_count),
788 format!("{}", qed.ncols()),
789 ));
790 }
791 }
792
793 let mut gamma = Array2::<f64>::zeros((n, n));
795 for i in 0..n {
796 let xi = coords[[i, 0]];
797 let yi = coords[[i, 1]];
798 for j in 0..n {
799 if i == j {
800 gamma[[i, j]] = 0.0;
801 } else {
802 let xj = coords[[j, 0]];
803 let yj = coords[[j, 1]];
804 let h = squared_distance_2d(xi, yi, xj, yj).sqrt();
805 gamma[[i, j]] = options.variogram.evaluate(h);
806 }
807 }
808 }
809
810 let mut f_matrix = Array2::<f64>::zeros((n, p));
811 for i in 0..n {
812 let xi = coords[[i, 0]];
813 let yi = coords[[i, 1]];
814 let mut row_buf = vec![0.0f64; p];
815 fill_drift_row(&options.drift_bases, i, xi, yi, &mut row_buf);
816 for col in 0..p {
817 f_matrix[[i, col]] = row_buf[col];
818 }
819 }
820
821 let size = n + p;
823 let mut m_design = Array2::<f64>::zeros((size, size));
824 for i in 0..n {
825 for j in 0..n {
826 m_design[[i, j]] = gamma[[i, j]];
827 }
828 m_design[[i, i]] += options.regularization;
829 }
830 for i in 0..n {
831 for j in 0..p {
832 m_design[[i, n + j]] = f_matrix[[i, j]];
833 m_design[[n + j, i]] = f_matrix[[i, j]];
834 }
835 }
836 let m_inv = gauss_jordan_invert_uked(&m_design).ok_or_else(|| {
839 AnalyticsError::matrix_error("UKED design matrix singular (rank-deficient solve)")
840 })?;
841
842 let mut predicted = Vec::with_capacity(m);
843 let mut variance = Vec::with_capacity(m);
844 let mut drift_coefficients = vec![0.0f64; p];
845
846 for q in 0..m {
847 let qx = query_points[[q, 0]];
848 let qy = query_points[[q, 1]];
849
850 let mut b = vec![0.0f64; size];
852 for i in 0..n {
853 let xi = coords[[i, 0]];
854 let yi = coords[[i, 1]];
855 let h = squared_distance_2d(xi, yi, qx, qy).sqrt();
856 b[i] = options.variogram.evaluate(h);
857 }
858 let ext_row_owned: Option<Vec<f64>> = if external_basis_count > 0 {
859 let qed = query_external_drift.ok_or_else(|| {
860 AnalyticsError::invalid_input(
861 "query_external_drift required when DriftBasis::External is used",
862 )
863 })?;
864 let mut row = Vec::with_capacity(external_basis_count);
865 for k in 0..external_basis_count {
866 row.push(qed[[q, k]]);
867 }
868 Some(row)
869 } else {
870 None
871 };
872 let mut f0 = vec![0.0f64; p];
873 fill_query_drift_row(
874 &options.drift_bases,
875 qx,
876 qy,
877 ext_row_owned.as_deref(),
878 &mut f0,
879 )?;
880 b[n..n + p].copy_from_slice(&f0[..p]);
881
882 let mut z = vec![0.0f64; size];
884 for i in 0..size {
885 let mut acc = 0.0f64;
886 for j in 0..size {
887 acc += m_inv[[i, j]] * b[j];
888 }
889 z[i] = acc;
890 }
891
892 let mut value = 0.0f64;
894 for i in 0..n {
895 value += z[i] * values[i];
896 }
897 predicted.push(value);
898
899 let mut var_acc = 0.0f64;
901 for i in 0..n {
902 var_acc += z[i] * b[i];
903 }
904 for j in 0..p {
905 var_acc += z[n + j] * f0[j];
906 }
907 variance.push(var_acc.max(0.0));
908
909 drift_coefficients[..p].copy_from_slice(&z[n..n + p]);
911 }
912
913 Ok(UniversalKrigingResult {
914 predicted,
915 variance,
916 drift_coefficients,
917 })
918}
919
920#[cfg(test)]
921mod tests {
922 use super::*;
923 use approx::assert_abs_diff_eq;
924 use scirs2_core::ndarray::array;
925
926 #[test]
927 fn test_variogram_spherical() {
928 let var = Variogram::new(VariogramModel::Spherical, 0.1, 1.0, 10.0);
929
930 assert_abs_diff_eq!(var.evaluate(0.0), 0.0, epsilon = 1e-10);
931 assert_abs_diff_eq!(var.evaluate(10.0), 1.0, epsilon = 1e-10);
932 assert_abs_diff_eq!(var.evaluate(20.0), 1.0, epsilon = 1e-10);
933 }
934
935 #[test]
936 fn test_kriging_simple() {
937 let points = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]];
938 let values = array![1.0, 2.0, 3.0, 4.0];
939 let targets = array![[0.5, 0.5]];
940
941 let var = Variogram::new(VariogramModel::Spherical, 0.0, 1.0, 2.0);
942 let interpolator = KrigingInterpolator::new(KrigingType::Ordinary, var);
943
944 let result = interpolator
945 .interpolate(&points, &values.view(), &targets)
946 .expect("Kriging interpolation should succeed for valid data");
947
948 assert_eq!(result.values.len(), 1);
949 assert_eq!(result.variances.len(), 1);
950 assert!(result.values[0] > 2.0 && result.values[0] < 3.0);
951 }
952
953 #[test]
954 fn test_semivariogram_calculation() {
955 let points = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]];
956 let values = array![1.0, 2.0, 3.0];
957
958 let (distances, semivariances) =
959 SemivariogramCalculator::calculate(&points, &values.view(), 3)
960 .expect("Semivariogram calculation should succeed");
961
962 assert!(!distances.is_empty());
963 assert_eq!(distances.len(), semivariances.len());
964 }
965}