1use crate::delaunay::Delaunay;
16use crate::error::{SpatialError, SpatialResult};
17use crate::voronoi::Voronoi;
18use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2};
19use std::collections::HashMap;
20use std::fmt;
21
22pub struct NaturalNeighborInterpolator {
54 points: Array2<f64>,
56 values: Array1<f64>,
58 delaunay: Delaunay,
60 #[allow(dead_code)]
62 voronoi: Voronoi,
63 dim: usize,
65 n_points: usize,
67}
68
69impl Clone for NaturalNeighborInterpolator {
70 fn clone(&self) -> Self {
71 let delaunay = Delaunay::new(&self.points).expect("Operation failed");
73 let voronoi = Voronoi::new(&self.points.view(), false).expect("Operation failed");
74
75 Self {
76 points: self.points.clone(),
77 values: self.values.clone(),
78 delaunay,
79 voronoi,
80 dim: self.dim,
81 n_points: self.n_points,
82 }
83 }
84}
85
86impl fmt::Debug for NaturalNeighborInterpolator {
87 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88 f.debug_struct("NaturalNeighborInterpolator")
89 .field("dim", &self.dim)
90 .field("n_points", &self.n_points)
91 .field("pointsshape", &self.points.shape())
92 .field("values_len", &self.values.len())
93 .finish()
94 }
95}
96
97impl NaturalNeighborInterpolator {
98 pub fn new(points: &ArrayView2<'_, f64>, values: &ArrayView1<f64>) -> SpatialResult<Self> {
116 let n_points = points.nrows();
118 let dim = points.ncols();
119
120 if n_points != values.len() {
121 return Err(SpatialError::DimensionError(format!(
122 "Number of n_points ({}) must match number of values ({})",
123 n_points,
124 values.len()
125 )));
126 }
127
128 if dim != 2 {
129 return Err(SpatialError::DimensionError(format!(
130 "Natural neighbor interpolation currently only supports 2D points, got {dim}D"
131 )));
132 }
133
134 if n_points < 3 {
135 return Err(SpatialError::ValueError(
136 "Natural neighbor interpolation requires at least 3 n_points".to_string(),
137 ));
138 }
139
140 let delaunay = Delaunay::new(&points.to_owned())?;
142
143 let voronoi = Voronoi::new(points, false)?;
145
146 Ok(Self {
147 points: points.to_owned(),
148 values: values.to_owned(),
149 delaunay,
150 voronoi,
151 dim,
152 n_points,
153 })
154 }
155
156 pub fn interpolate(&self, point: &ArrayView1<f64>) -> SpatialResult<f64> {
171 if point.len() != self.dim {
173 return Err(SpatialError::DimensionError(format!(
174 "Query point has dimension {}, expected {}",
175 point.len(),
176 self.dim
177 )));
178 }
179
180 for (i, input_point) in self.points.outer_iter().enumerate() {
183 let dist_sq: f64 = point
184 .iter()
185 .zip(input_point.iter())
186 .map(|(a, b)| (a - b).powi(2))
187 .sum();
188 if dist_sq < 1e-20 {
189 return Ok(self.values[i]);
190 }
191 }
192
193 let simplex_idx = self
195 .delaunay
196 .find_simplex(point.as_slice().expect("Operation failed"));
197
198 if simplex_idx.is_none() {
199 return Err(SpatialError::ValueError(
200 "Query point is outside the convex hull of the input points".to_string(),
201 ));
202 }
203
204 let weights = self.natural_neighbor_weights(point)?;
206
207 let mut result = 0.0;
209 for (idx, weight) in weights {
210 result += weight * self.values[idx];
211 }
212
213 Ok(result)
214 }
215
216 pub fn interpolate_many(&self, points: &ArrayView2<'_, f64>) -> SpatialResult<Array1<f64>> {
230 if points.ncols() != self.dim {
232 return Err(SpatialError::DimensionError(format!(
233 "Query n_points have dimension {}, expected {}",
234 points.ncols(),
235 self.dim
236 )));
237 }
238
239 let n_queries = points.nrows();
240 let mut results = Array1::zeros(n_queries);
241
242 for i in 0..n_queries {
244 let point = points.row(i);
245
246 match self.interpolate(&point) {
248 Ok(value) => results[i] = value,
249 Err(_) => results[i] = f64::NAN,
250 }
251 }
252
253 Ok(results)
254 }
255
256 fn natural_neighbor_weights(
271 &self,
272 point: &ArrayView1<f64>,
273 ) -> SpatialResult<HashMap<usize, f64>> {
274 let simplex_idx = self
280 .delaunay
281 .find_simplex(point.as_slice().expect("Operation failed"));
282
283 if simplex_idx.is_none() {
284 return Err(SpatialError::ValueError(
285 "Query point is outside the convex hull of the input points".to_string(),
286 ));
287 }
288
289 let simplex_idx = simplex_idx.expect("Operation failed");
290 let simplex = &self.delaunay.simplices()[simplex_idx];
291
292 if self.dim == 2 {
294 if let Ok(weights) = self.compute_robust_natural_neighbor_weights(point, simplex) {
296 return Ok(weights);
297 }
298
299 self.barycentric_weights_as_map(point, simplex_idx)
301 } else {
302 self.barycentric_weights_as_map(point, simplex_idx)
304 }
305 }
306
307 fn compute_robust_natural_neighbor_weights(
309 &self,
310 point: &ArrayView1<f64>,
311 simplex: &[usize],
312 ) -> SpatialResult<HashMap<usize, f64>> {
313 let natural_neighbors = self.find_natural_neighbors(point, simplex)?;
315
316 if natural_neighbors.len() < 3 {
318 return Err(SpatialError::ComputationError(
319 "Insufficient natural neighbors for interpolation".to_string(),
320 ));
321 }
322
323 let mut weights = HashMap::new();
325 let mut total_weight = 0.0;
326
327 for &neighbor_idx in &natural_neighbors {
329 let stolen_area = self.estimate_stolen_area(point, neighbor_idx, &natural_neighbors)?;
331
332 if stolen_area > 1e-12 {
333 weights.insert(neighbor_idx, stolen_area);
334 total_weight += stolen_area;
335 }
336 }
337
338 if weights.is_empty() || total_weight <= 1e-12 {
340 return Err(SpatialError::ComputationError(
341 "Failed to compute valid natural neighbor weights".to_string(),
342 ));
343 }
344
345 for weight in weights.values_mut() {
347 *weight /= total_weight;
348 }
349
350 let weight_sum: f64 = weights.values().sum();
352 if (weight_sum - 1.0).abs() > 1e-10 {
353 let correction = 1.0 / weight_sum;
355 for weight in weights.values_mut() {
356 *weight *= correction;
357 }
358 }
359
360 Ok(weights)
361 }
362
363 fn estimate_stolen_area(
365 &self,
366 query_point: &ArrayView1<f64>,
367 neighbor_idx: usize,
368 natural_neighbors: &[usize],
369 ) -> SpatialResult<f64> {
370 let neighbor_point = self.points.row(neighbor_idx);
371
372 let distance = Self::euclidean_distance(query_point, &neighbor_point);
374 if distance < 1e-12 {
375 return Ok(1.0); }
377
378 let base_weight = 1.0 / distance;
380
381 let mut adjustment = 1.0;
383
384 let mut angle_sum = 0.0;
386 let mut neighbor_count = 0;
387
388 for &other_neighbor_idx in natural_neighbors {
389 if other_neighbor_idx != neighbor_idx {
390 let other_neighbor_point = self.points.row(other_neighbor_idx);
391
392 let v1_x = neighbor_point[0] - query_point[0];
394 let v1_y = neighbor_point[1] - query_point[1];
395 let v2_x = other_neighbor_point[0] - query_point[0];
396 let v2_y = other_neighbor_point[1] - query_point[1];
397
398 let dot_product = v1_x * v2_x + v1_y * v2_y;
399 let mag1 = (v1_x * v1_x + v1_y * v1_y).sqrt();
400 let mag2 = (v2_x * v2_x + v2_y * v2_y).sqrt();
401
402 if mag1 > 1e-12 && mag2 > 1e-12 {
403 let cos_angle = (dot_product / (mag1 * mag2)).clamp(-1.0, 1.0);
404 let angle = cos_angle.acos();
405 angle_sum += angle;
406 neighbor_count += 1;
407 }
408 }
409 }
410
411 if neighbor_count > 0 {
413 let average_angle = angle_sum / neighbor_count as f64;
414 adjustment = average_angle / std::f64::consts::PI + 0.1; }
416
417 Ok(base_weight * adjustment)
418 }
419
420 fn barycentric_weights(
435 &self,
436 point: &ArrayView1<f64>,
437 simplex_idx: usize,
438 ) -> SpatialResult<Vec<f64>> {
439 let simplex = &self.delaunay.simplices()[simplex_idx];
440 let mut vertices = Vec::new();
441
442 for &_idx in simplex {
443 vertices.push(self.points.row(_idx));
444 }
445
446 if vertices.len() != 3 {
448 return Err(SpatialError::ValueError(format!(
449 "Expected 3 vertices for 2D triangle, got {}",
450 vertices.len()
451 )));
452 }
453
454 let a = vertices[0];
456 let b = vertices[1];
457 let c = vertices[2];
458 let p = point;
459
460 let v0_x = b[0] - a[0];
461 let v0_y = b[1] - a[1];
462 let v1_x = c[0] - a[0];
463 let v1_y = c[1] - a[1];
464 let v2_x = p[0] - a[0];
465 let v2_y = p[1] - a[1];
466
467 let d00 = v0_x * v0_x + v0_y * v0_y;
468 let d01 = v0_x * v1_x + v0_y * v1_y;
469 let d11 = v1_x * v1_x + v1_y * v1_y;
470 let d20 = v2_x * v0_x + v2_y * v0_y;
471 let d21 = v2_x * v1_x + v2_y * v1_y;
472
473 let denom = d00 * d11 - d01 * d01;
474 if denom.abs() < 1e-10 {
475 return Err(SpatialError::ValueError(
476 "Degenerate triangle, cannot compute barycentric coordinates".to_string(),
477 ));
478 }
479
480 let v = (d11 * d20 - d01 * d21) / denom;
481 let w = (d00 * d21 - d01 * d20) / denom;
482 let u = 1.0 - v - w;
483
484 Ok(vec![u, v, w])
485 }
486
487 #[allow(dead_code)]
502 fn get_voronoi_vertices(voronoi: &Voronoi, region: &[i64]) -> SpatialResult<Array2<f64>> {
503 if region.is_empty() {
504 return Err(SpatialError::ValueError("Empty Voronoi region".to_string()));
505 }
506
507 let valid_count = region.iter().filter(|&&idx| idx >= 0).count();
509
510 if valid_count == 0 {
511 return Err(SpatialError::ValueError(
512 "All vertices are at infinity".to_string(),
513 ));
514 }
515
516 let mut vertices = Array2::zeros((valid_count, 2));
517 let mut j = 0;
518
519 for &idx in region.iter() {
520 if idx >= 0 {
521 vertices
522 .row_mut(j)
523 .assign(&voronoi.vertices().row(idx as usize));
524 j += 1;
525 }
526 }
527
528 Ok(vertices)
529 }
530
531 #[allow(dead_code)]
545 fn polygon_area(vertices: &Array2<f64>) -> SpatialResult<f64> {
546 let n = vertices.nrows();
547
548 if n < 3 {
549 return Err(SpatialError::ValueError(format!(
550 "Polygon must have at least 3 vertices, got {n}"
551 )));
552 }
553
554 let mut area = 0.0;
555
556 for i in 0..n {
557 let j = (i + 1) % n;
558 area += vertices[[i, 0]] * vertices[[j, 1]] - vertices[[j, 0]] * vertices[[i, 1]];
559 }
560
561 Ok(area.abs() / 2.0)
562 }
563
564 fn euclidean_distance(p1: &ArrayView1<f64>, p2: &ArrayView1<f64>) -> f64 {
575 let len = p1.len().min(p2.len());
576 let mut sum_sq = 0.0;
577
578 let chunks = len / 4;
580
581 for i in 0..chunks {
582 let base = i * 4;
583 let diff0 = p1[base] - p2[base];
584 let diff1 = p1[base + 1] - p2[base + 1];
585 let diff2 = p1[base + 2] - p2[base + 2];
586 let diff3 = p1[base + 3] - p2[base + 3];
587
588 sum_sq += diff0 * diff0 + diff1 * diff1 + diff2 * diff2 + diff3 * diff3;
589 }
590
591 for i in (chunks * 4)..len {
593 let diff = p1[i] - p2[i];
594 sum_sq += diff * diff;
595 }
596
597 sum_sq.sqrt()
598 }
599
600 fn find_natural_neighbors(
605 &self,
606 point: &ArrayView1<f64>,
607 simplex: &[usize],
608 ) -> SpatialResult<Vec<usize>> {
609 let mut neighbors = Vec::new();
610
611 for &idx in simplex {
613 neighbors.push(idx);
614 }
615
616 let circumradius = self.compute_circumradius(simplex).unwrap_or(1.0);
618
619 let search_radius = self.compute_adaptive_search_radius(point, circumradius)?;
621
622 let mut candidates = Vec::new();
624 for i in 0..self.n_points {
625 if !neighbors.contains(&i) {
626 let dist = Self::euclidean_distance(point, &self.points.row(i));
627 if dist <= search_radius {
628 candidates.push((i, dist));
629 }
630 }
631 }
632
633 candidates.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
635
636 let max_additional = (self.n_points / 4).clamp(10, 20);
638 for (idx_, _) in candidates.into_iter().take(max_additional) {
639 neighbors.push(idx_);
640 }
641
642 if neighbors.len() < 3 {
644 let mut all_distances: Vec<(usize, f64)> = (0..self.n_points)
646 .filter(|&i| !neighbors.contains(&i))
647 .map(|i| {
648 let dist = Self::euclidean_distance(point, &self.points.row(i));
649 (i, dist)
650 })
651 .collect();
652
653 all_distances
654 .sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
655
656 for (idx_, _) in all_distances.into_iter().take(3 - neighbors.len()) {
657 neighbors.push(idx_);
658 }
659 }
660
661 Ok(neighbors)
662 }
663
664 fn compute_adaptive_search_radius(
666 &self,
667 point: &ArrayView1<f64>,
668 base_radius: f64,
669 ) -> SpatialResult<f64> {
670 const K: usize = 5;
672 let mut distances: Vec<f64> = (0..self.n_points)
673 .map(|i| Self::euclidean_distance(point, &self.points.row(i)))
674 .collect();
675
676 distances.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
677
678 let k_nearest_dist = if distances.len() > K {
680 distances[K]
681 } else {
682 distances.last().copied().unwrap_or(base_radius)
683 };
684
685 let adaptive_radius = (base_radius * 2.0).max(k_nearest_dist * 1.5);
687
688 Ok(adaptive_radius)
689 }
690
691 fn compute_circumradius(&self, simplex: &[usize]) -> SpatialResult<f64> {
693 if simplex.len() != 3 || self.dim != 2 {
694 return Err(SpatialError::ValueError(
695 "Circumradius computation only supported for 2D triangles".to_string(),
696 ));
697 }
698
699 let a = self.points.row(simplex[0]);
700 let b = self.points.row(simplex[1]);
701 let c = self.points.row(simplex[2]);
702
703 let ab = Self::euclidean_distance(&a, &b);
705 let bc = Self::euclidean_distance(&b, &c);
706 let ca = Self::euclidean_distance(&c, &a);
707
708 let s = (ab + bc + ca) / 2.0;
710 let area = (s * (s - ab) * (s - bc) * (s - ca)).sqrt();
711
712 if area < 1e-10 {
713 return Err(SpatialError::ValueError("Degenerate triangle".to_string()));
714 }
715
716 Ok((ab * bc * ca) / (4.0 * area))
718 }
719
720 fn barycentric_weights_as_map(
722 &self,
723 point: &ArrayView1<f64>,
724 simplex_idx: usize,
725 ) -> SpatialResult<HashMap<usize, f64>> {
726 let simplex = &self.delaunay.simplices()[simplex_idx];
727 let bary_weights = self.barycentric_weights(point, simplex_idx)?;
728
729 let mut weights = HashMap::new();
730 for (i, &_idx) in simplex.iter().enumerate() {
731 if bary_weights[i] > 1e-10 {
732 weights.insert(_idx, bary_weights[i]);
733 }
734 }
735
736 Ok(weights)
737 }
738}
739
740#[cfg(test)]
741mod tests {
742 use super::*;
743 use scirs2_core::ndarray::array;
744
745 #[test]
746 fn test_natural_neighbor_interpolator() {
747 let points = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0],];
749 let values = array![0.0, 1.0, 2.0, 3.0];
750
751 let interp = NaturalNeighborInterpolator::new(&points.view(), &values.view())
753 .expect("Operation failed");
754
755 let query_point = array![0.5, 0.5];
757 let result = interp
758 .interpolate(&query_point.view())
759 .expect("Operation failed");
760
761 assert!((result - 1.5).abs() < 0.1, "Expected ~1.5, got {result}");
764
765 let corner = array![0.0, 0.0];
767 let corner_result = interp
768 .interpolate(&corner.view())
769 .expect("Operation failed");
770 assert!(
771 (corner_result - 0.0).abs() < 1e-6,
772 "Expected 0.0 at corner, got {corner_result}"
773 );
774 }
775
776 #[test]
777 fn test_outside_convex_hull() {
778 let points = array![[0.0, 0.0], [1.0, 0.0], [0.5, 1.0],];
780 let values = array![0.0, 1.0, 2.0];
781
782 let interp = NaturalNeighborInterpolator::new(&points.view(), &values.view())
783 .expect("Operation failed");
784
785 let outside_point = array![2.0, 2.0];
787 let result = interp.interpolate(&outside_point.view());
788
789 assert!(
790 result.is_err(),
791 "Expected error for point outside convex hull"
792 );
793
794 let query_points = array![
796 [0.5, 0.5], [2.0, 2.0], [0.25, 0.25], ];
800
801 let results = interp
802 .interpolate_many(&query_points.view())
803 .expect("Operation failed");
804 assert!(
805 !results[0].is_nan(),
806 "Inside point should have valid result"
807 );
808 assert!(results[1].is_nan(), "Outside point should return NaN");
809 assert!(
810 !results[2].is_nan(),
811 "Inside point should have valid result"
812 );
813 }
814
815 #[test]
816 fn test_error_handling() {
817 let points = array![[0.0, 0.0], [1.0, 1.0]];
819 let values = array![0.0, 1.0];
820
821 let result = NaturalNeighborInterpolator::new(&points.view(), &values.view());
822 assert!(result.is_err());
823
824 let points_3d = array![[0.0, 0.0, 0.0], [1.0, 1.0, 1.0], [2.0, 2.0, 2.0]];
826 let values = array![0.0, 1.0, 2.0];
827
828 let result = NaturalNeighborInterpolator::new(&points_3d.view(), &values.view());
829 assert!(result.is_err());
830
831 let points = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]];
833 let values = array![0.0, 1.0];
834
835 let result = NaturalNeighborInterpolator::new(&points.view(), &values.view());
836 assert!(result.is_err());
837 }
838}