1use crate::error::{InterpolateError, InterpolateResult};
53use scirs2_core::ndarray::{Array1, Array2, ArrayView1};
54use scirs2_core::numeric::{Float, FromPrimitive};
55use std::fmt::Debug;
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub struct Triangle {
64 pub vertices: [usize; 3],
66}
67
68#[derive(Debug, Clone)]
72pub struct DelaunayTriangulation<F: Float + FromPrimitive + Debug> {
73 points: Vec<[F; 2]>,
75 triangles: Vec<Triangle>,
77 n_original: usize,
79}
80
81impl<F: Float + FromPrimitive + Debug> DelaunayTriangulation<F> {
82 pub fn new(input_points: &[[F; 2]]) -> InterpolateResult<Self> {
93 let n = input_points.len();
94 if n < 3 {
95 return Err(InterpolateError::insufficient_points(
96 3,
97 n,
98 "Delaunay triangulation",
99 ));
100 }
101
102 let mut min_x = input_points[0][0];
104 let mut max_x = input_points[0][0];
105 let mut min_y = input_points[0][1];
106 let mut max_y = input_points[0][1];
107
108 for p in input_points.iter().skip(1) {
109 if p[0] < min_x {
110 min_x = p[0];
111 }
112 if p[0] > max_x {
113 max_x = p[0];
114 }
115 if p[1] < min_y {
116 min_y = p[1];
117 }
118 if p[1] > max_y {
119 max_y = p[1];
120 }
121 }
122
123 let dx = max_x - min_x;
124 let dy = max_y - min_y;
125 let delta_max = if dx > dy { dx } else { dy };
126
127 let eps = F::from_f64(1e-10).unwrap_or_else(|| F::epsilon());
129 if delta_max < eps {
130 return Err(InterpolateError::invalid_input(
131 "All points are coincident; cannot form triangulation",
132 ));
133 }
134
135 let mid_x = (min_x + max_x) / (F::one() + F::one());
136 let mid_y = (min_y + max_y) / (F::one() + F::one());
137
138 let margin = F::from_f64(20.0).unwrap_or_else(|| {
140 let mut v = F::one();
141 for _ in 0..4 {
142 v = v + v;
143 }
144 v + v + v + v
145 });
146 let super_delta = delta_max * margin;
147
148 let three = F::one() + F::one() + F::one();
149 let p0 = [mid_x - super_delta, mid_y - super_delta];
150 let p1 = [mid_x + super_delta, mid_y - super_delta];
151 let p2 = [mid_x, mid_y + super_delta * three];
152
153 let mut points = Vec::with_capacity(n + 3);
155 points.push(p0);
156 points.push(p1);
157 points.push(p2);
158 for p in input_points {
159 points.push(*p);
160 }
161
162 let mut triangles = vec![Triangle {
163 vertices: [0, 1, 2],
164 }];
165
166 for i in 0..n {
168 let pt_idx = i + 3; let pt = points[pt_idx];
170
171 let mut bad_triangles = Vec::new();
173 for (t_idx, tri) in triangles.iter().enumerate() {
174 if Self::in_circumcircle(&points, tri, pt) {
175 bad_triangles.push(t_idx);
176 }
177 }
178
179 let mut boundary_edges: Vec<[usize; 2]> = Vec::new();
181 for &t_idx in &bad_triangles {
182 let tri = &triangles[t_idx];
183 for edge_i in 0..3 {
184 let edge = [tri.vertices[edge_i], tri.vertices[(edge_i + 1) % 3]];
185
186 let mut shared = false;
188 for &other_idx in &bad_triangles {
189 if other_idx == t_idx {
190 continue;
191 }
192 if Self::triangle_has_edge(&triangles[other_idx], edge) {
193 shared = true;
194 break;
195 }
196 }
197
198 if !shared {
199 boundary_edges.push(edge);
200 }
201 }
202 }
203
204 let mut bad_sorted = bad_triangles;
206 bad_sorted.sort_unstable();
207 for &idx in bad_sorted.iter().rev() {
208 triangles.swap_remove(idx);
209 }
210
211 for edge in &boundary_edges {
213 triangles.push(Triangle {
214 vertices: [edge[0], edge[1], pt_idx],
215 });
216 }
217 }
218
219 triangles
221 .retain(|tri| tri.vertices[0] >= 3 && tri.vertices[1] >= 3 && tri.vertices[2] >= 3);
222
223 for tri in &mut triangles {
225 tri.vertices[0] -= 3;
226 tri.vertices[1] -= 3;
227 tri.vertices[2] -= 3;
228 }
229
230 let final_points: Vec<[F; 2]> = points[3..].to_vec();
232
233 if triangles.is_empty() {
234 return Err(InterpolateError::invalid_input(
235 "No triangles formed; points may be collinear",
236 ));
237 }
238
239 Ok(Self {
240 points: final_points,
241 triangles,
242 n_original: n,
243 })
244 }
245
246 fn in_circumcircle(points: &[[F; 2]], tri: &Triangle, p: [F; 2]) -> bool {
248 let a = points[tri.vertices[0]];
249 let b = points[tri.vertices[1]];
250 let c = points[tri.vertices[2]];
251
252 let ax = a[0] - p[0];
253 let ay = a[1] - p[1];
254 let bx = b[0] - p[0];
255 let by = b[1] - p[1];
256 let cx = c[0] - p[0];
257 let cy = c[1] - p[1];
258
259 let det = ax * (by * (cx * cx + cy * cy) - cy * (bx * bx + by * by))
260 - ay * (bx * (cx * cx + cy * cy) - cx * (bx * bx + by * by))
261 + (ax * ax + ay * ay) * (bx * cy - by * cx);
262
263 let orient = Self::orientation(a, b, c);
266 if orient > F::zero() {
267 det > F::zero()
268 } else {
269 det < F::zero()
270 }
271 }
272
273 fn orientation(a: [F; 2], b: [F; 2], c: [F; 2]) -> F {
276 (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])
277 }
278
279 fn triangle_has_edge(tri: &Triangle, edge: [usize; 2]) -> bool {
281 for i in 0..3 {
282 let e0 = tri.vertices[i];
283 let e1 = tri.vertices[(i + 1) % 3];
284 if (e0 == edge[0] && e1 == edge[1]) || (e0 == edge[1] && e1 == edge[0]) {
285 return true;
286 }
287 }
288 false
289 }
290
291 pub fn triangles(&self) -> &[Triangle] {
293 &self.triangles
294 }
295
296 pub fn points(&self) -> &[[F; 2]] {
298 &self.points
299 }
300
301 pub fn num_triangles(&self) -> usize {
303 self.triangles.len()
304 }
305
306 pub fn find_containing_triangle(&self, x: F, y: F) -> Option<(usize, [F; 3])> {
311 let eps = F::from_f64(-1e-12).unwrap_or_else(|| -F::epsilon());
312
313 for (i, tri) in self.triangles.iter().enumerate() {
314 let bary = self.barycentric_coords(tri, x, y);
315 if let Some(bc) = bary {
316 if bc[0] >= eps && bc[1] >= eps && bc[2] >= eps {
318 return Some((i, bc));
319 }
320 }
321 }
322 None
323 }
324
325 fn barycentric_coords(&self, tri: &Triangle, x: F, y: F) -> Option<[F; 3]> {
327 let p0 = self.points[tri.vertices[0]];
328 let p1 = self.points[tri.vertices[1]];
329 let p2 = self.points[tri.vertices[2]];
330
331 let v0x = p1[0] - p0[0];
332 let v0y = p1[1] - p0[1];
333 let v1x = p2[0] - p0[0];
334 let v1y = p2[1] - p0[1];
335 let v2x = x - p0[0];
336 let v2y = y - p0[1];
337
338 let det = v0x * v1y - v1x * v0y;
339 let eps = F::from_f64(1e-15).unwrap_or_else(|| F::epsilon());
340
341 if det.abs() < eps {
342 return None; }
344
345 let inv_det = F::one() / det;
346 let u = (v2x * v1y - v1x * v2y) * inv_det;
347 let v = (v0x * v2y - v2x * v0y) * inv_det;
348 let w = F::one() - u - v;
349
350 Some([w, u, v])
351 }
352}
353
354#[derive(Debug, Clone, Copy, PartialEq)]
360pub enum TriangulationMethod {
361 Linear,
366
367 CloughTocher,
373
374 NearestVertex,
379}
380
381#[derive(Debug, Clone, Copy, PartialEq)]
383pub enum ExteriorHandling {
384 Nan,
386 NearestNeighbor,
388 Error,
390}
391
392#[derive(Debug, Clone)]
401pub struct TriangulationInterpolator<F: Float + FromPrimitive + Debug> {
402 triangulation: DelaunayTriangulation<F>,
404 values: Array1<F>,
406 method: TriangulationMethod,
408 exterior: ExteriorHandling,
410 gradients: Option<Vec<[F; 2]>>,
412}
413
414impl<F: Float + FromPrimitive + Debug + std::fmt::Display + 'static> TriangulationInterpolator<F> {
415 pub fn new(
428 points: Array2<F>,
429 values: Array1<F>,
430 method: TriangulationMethod,
431 ) -> InterpolateResult<Self> {
432 Self::with_exterior(points, values, method, ExteriorHandling::Nan)
433 }
434
435 pub fn with_exterior(
437 points: Array2<F>,
438 values: Array1<F>,
439 method: TriangulationMethod,
440 exterior: ExteriorHandling,
441 ) -> InterpolateResult<Self> {
442 if points.ncols() != 2 {
443 return Err(InterpolateError::invalid_input(format!(
444 "Triangulation interpolation requires 2D points, got {}D",
445 points.ncols()
446 )));
447 }
448
449 if points.nrows() != values.len() {
450 return Err(InterpolateError::invalid_input(format!(
451 "Number of points ({}) does not match number of values ({})",
452 points.nrows(),
453 values.len()
454 )));
455 }
456
457 let pts: Vec<[F; 2]> = (0..points.nrows())
459 .map(|i| [points[[i, 0]], points[[i, 1]]])
460 .collect();
461
462 let triangulation = DelaunayTriangulation::new(&pts)?;
463
464 let gradients = if method == TriangulationMethod::CloughTocher {
466 Some(Self::estimate_gradients(&triangulation, &values)?)
467 } else {
468 None
469 };
470
471 Ok(Self {
472 triangulation,
473 values,
474 method,
475 exterior,
476 gradients,
477 })
478 }
479
480 pub fn evaluate_point(&self, x: F, y: F) -> InterpolateResult<F> {
492 match self.triangulation.find_containing_triangle(x, y) {
494 Some((tri_idx, bary)) => match self.method {
495 TriangulationMethod::Linear => self.linear_interpolate(tri_idx, &bary),
496 TriangulationMethod::CloughTocher => {
497 self.clough_tocher_interpolate(tri_idx, x, y, &bary)
498 }
499 TriangulationMethod::NearestVertex => {
500 self.nearest_vertex_interpolate(tri_idx, &bary)
501 }
502 },
503 None => {
504 self.handle_exterior(x, y)
506 }
507 }
508 }
509
510 pub fn evaluate_point_array(&self, point: &ArrayView1<F>) -> InterpolateResult<F> {
512 if point.len() != 2 {
513 return Err(InterpolateError::dimension_mismatch(
514 2,
515 point.len(),
516 "TriangulationInterpolator::evaluate_point_array",
517 ));
518 }
519 self.evaluate_point(point[0], point[1])
520 }
521
522 pub fn evaluate(&self, queries: &Array2<F>) -> InterpolateResult<Array1<F>> {
528 if queries.ncols() != 2 {
529 return Err(InterpolateError::dimension_mismatch(
530 2,
531 queries.ncols(),
532 "TriangulationInterpolator::evaluate",
533 ));
534 }
535
536 let n = queries.nrows();
537 let mut result = Array1::zeros(n);
538 for i in 0..n {
539 result[i] = self.evaluate_point(queries[[i, 0]], queries[[i, 1]])?;
540 }
541 Ok(result)
542 }
543
544 pub fn triangulation(&self) -> &DelaunayTriangulation<F> {
546 &self.triangulation
547 }
548
549 pub fn values(&self) -> &Array1<F> {
551 &self.values
552 }
553
554 pub fn num_points(&self) -> usize {
556 self.values.len()
557 }
558
559 pub fn num_triangles(&self) -> usize {
561 self.triangulation.num_triangles()
562 }
563
564 fn linear_interpolate(&self, tri_idx: usize, bary: &[F; 3]) -> InterpolateResult<F> {
569 let tri = &self.triangulation.triangles()[tri_idx];
570 let v0 = self.values[tri.vertices[0]];
571 let v1 = self.values[tri.vertices[1]];
572 let v2 = self.values[tri.vertices[2]];
573
574 Ok(bary[0] * v0 + bary[1] * v1 + bary[2] * v2)
575 }
576
577 fn nearest_vertex_interpolate(&self, tri_idx: usize, bary: &[F; 3]) -> InterpolateResult<F> {
582 let tri = &self.triangulation.triangles()[tri_idx];
583
584 let mut max_bary = bary[0];
586 let mut max_idx = 0;
587 for i in 1..3 {
588 if bary[i] > max_bary {
589 max_bary = bary[i];
590 max_idx = i;
591 }
592 }
593
594 Ok(self.values[tri.vertices[max_idx]])
595 }
596
597 fn estimate_gradients(
603 triangulation: &DelaunayTriangulation<F>,
604 values: &Array1<F>,
605 ) -> InterpolateResult<Vec<[F; 2]>> {
606 let n = triangulation.n_original;
607 let points = triangulation.points();
608 let triangles = triangulation.triangles();
609
610 let mut gradients = vec![[F::zero(), F::zero()]; n];
612
613 for i in 0..n {
614 let mut neighbor_offsets: Vec<(F, F, F)> = Vec::new();
615 let xi = points[i][0];
616 let yi = points[i][1];
617 let fi = values[i];
618
619 for tri in triangles {
621 let mut contains = false;
622 for &v in &tri.vertices {
623 if v == i {
624 contains = true;
625 break;
626 }
627 }
628 if !contains {
629 continue;
630 }
631
632 for &v in &tri.vertices {
633 if v == i || v >= n {
634 continue;
635 }
636 let dx = points[v][0] - xi;
637 let dy = points[v][1] - yi;
638 let df = values[v] - fi;
639 neighbor_offsets.push((dx, dy, df));
640 }
641 }
642
643 neighbor_offsets.sort_by(|a, b| {
645 a.0.partial_cmp(&b.0)
646 .unwrap_or(std::cmp::Ordering::Equal)
647 .then(a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
648 });
649 neighbor_offsets.dedup_by(|a, b| {
650 let eps = F::from_f64(1e-12).unwrap_or_else(|| F::epsilon());
651 (a.0 - b.0).abs() < eps && (a.1 - b.1).abs() < eps
652 });
653
654 if neighbor_offsets.is_empty() {
655 continue;
657 }
658
659 let mut ata00 = F::zero();
663 let mut ata01 = F::zero();
664 let mut ata11 = F::zero();
665 let mut atb0 = F::zero();
666 let mut atb1 = F::zero();
667
668 for &(dx, dy, df) in &neighbor_offsets {
669 ata00 = ata00 + dx * dx;
670 ata01 = ata01 + dx * dy;
671 ata11 = ata11 + dy * dy;
672 atb0 = atb0 + dx * df;
673 atb1 = atb1 + dy * df;
674 }
675
676 let det = ata00 * ata11 - ata01 * ata01;
677 let eps = F::from_f64(1e-14).unwrap_or_else(|| F::epsilon());
678
679 if det.abs() > eps {
680 gradients[i][0] = (ata11 * atb0 - ata01 * atb1) / det;
681 gradients[i][1] = (ata00 * atb1 - ata01 * atb0) / det;
682 }
683 }
685
686 Ok(gradients)
687 }
688
689 fn clough_tocher_interpolate(
694 &self,
695 tri_idx: usize,
696 x: F,
697 y: F,
698 bary: &[F; 3],
699 ) -> InterpolateResult<F> {
700 let gradients = self.gradients.as_ref().ok_or_else(|| {
701 InterpolateError::InvalidState("Gradients not computed for Clough-Tocher".to_string())
702 })?;
703
704 let tri = &self.triangulation.triangles()[tri_idx];
705 let points = self.triangulation.points();
706
707 let v0 = tri.vertices[0];
708 let v1 = tri.vertices[1];
709 let v2 = tri.vertices[2];
710
711 let f0 = self.values[v0];
712 let f1 = self.values[v1];
713 let f2 = self.values[v2];
714
715 let g0 = gradients[v0];
716 let g1 = gradients[v1];
717 let g2 = gradients[v2];
718
719 let p0 = points[v0];
720 let p1 = points[v1];
721 let p2 = points[v2];
722
723 let lambda0 = bary[0];
735 let lambda1 = bary[1];
736 let lambda2 = bary[2];
737
738 let f_linear = lambda0 * f0 + lambda1 * f1 + lambda2 * f2;
740
741 let e10 = [p1[0] - p0[0], p1[1] - p0[1]];
744 let e20 = [p2[0] - p0[0], p2[1] - p0[1]];
745
746 let area2 = e10[0] * e20[1] - e10[1] * e20[0];
747 let eps = F::from_f64(1e-15).unwrap_or_else(|| F::epsilon());
748
749 if area2.abs() < eps {
750 return Ok(f_linear);
752 }
753
754 let inv_area2 = F::one() / area2;
757 let grad_lin = [
758 ((f1 - f0) * e20[1] - (f2 - f0) * e10[1]) * inv_area2,
759 ((f2 - f0) * e10[0] - (f1 - f0) * e20[0]) * inv_area2,
760 ];
761
762 let dx = x - p0[0];
768 let dy = x - p0[1];
769 let _ = (dx, dy);
770
771 let mut correction = F::zero();
772
773 let dg0 = [g0[0] - grad_lin[0], g0[1] - grad_lin[1]];
775 let dp0 = [x - p0[0], y - p0[1]];
776 let c0 = dg0[0] * dp0[0] + dg0[1] * dp0[1];
777 correction = correction + lambda0 * lambda0 * c0;
778
779 let dg1 = [g1[0] - grad_lin[0], g1[1] - grad_lin[1]];
781 let dp1 = [x - p1[0], y - p1[1]];
782 let c1 = dg1[0] * dp1[0] + dg1[1] * dp1[1];
783 correction = correction + lambda1 * lambda1 * c1;
784
785 let dg2 = [g2[0] - grad_lin[0], g2[1] - grad_lin[1]];
787 let dp2 = [x - p2[0], y - p2[1]];
788 let c2 = dg2[0] * dp2[0] + dg2[1] * dp2[1];
789 correction = correction + lambda2 * lambda2 * c2;
790
791 Ok(f_linear + correction)
792 }
793
794 fn handle_exterior(&self, x: F, y: F) -> InterpolateResult<F> {
799 match self.exterior {
800 ExteriorHandling::Nan => Ok(F::nan()),
801 ExteriorHandling::Error => Err(InterpolateError::OutOfBounds(format!(
802 "Point ({}, {}) is outside the convex hull of the triangulation",
803 x, y
804 ))),
805 ExteriorHandling::NearestNeighbor => {
806 let points = self.triangulation.points();
808 let mut min_dist_sq = F::infinity();
809 let mut nearest_idx = 0;
810
811 for (i, p) in points.iter().enumerate() {
812 let dx = p[0] - x;
813 let dy = p[1] - y;
814 let dist_sq = dx * dx + dy * dy;
815 if dist_sq < min_dist_sq {
816 min_dist_sq = dist_sq;
817 nearest_idx = i;
818 }
819 }
820
821 Ok(self.values[nearest_idx])
822 }
823 }
824 }
825}
826
827pub fn make_linear_triangulation<F: Float + FromPrimitive + Debug + std::fmt::Display + 'static>(
852 points: Array2<F>,
853 values: Array1<F>,
854) -> InterpolateResult<TriangulationInterpolator<F>> {
855 TriangulationInterpolator::new(points, values, TriangulationMethod::Linear)
856}
857
858pub fn make_clough_tocher_interpolator<
865 F: Float + FromPrimitive + Debug + std::fmt::Display + 'static,
866>(
867 points: Array2<F>,
868 values: Array1<F>,
869) -> InterpolateResult<TriangulationInterpolator<F>> {
870 TriangulationInterpolator::new(points, values, TriangulationMethod::CloughTocher)
871}
872
873pub fn make_nearest_vertex_interpolator<
880 F: Float + FromPrimitive + Debug + std::fmt::Display + 'static,
881>(
882 points: Array2<F>,
883 values: Array1<F>,
884) -> InterpolateResult<TriangulationInterpolator<F>> {
885 TriangulationInterpolator::new(points, values, TriangulationMethod::NearestVertex)
886}
887
888#[cfg(test)]
893mod tests {
894 use super::*;
895 use scirs2_core::ndarray::{Array1, Array2};
896
897 fn make_square_data() -> (Array2<f64>, Array1<f64>) {
898 let points = Array2::from_shape_vec((4, 2), vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0])
900 .expect("valid shape");
901 let values = Array1::from_vec(vec![0.0, 1.0, 1.0, 2.0]);
902 (points, values)
903 }
904
905 fn make_larger_data() -> (Array2<f64>, Array1<f64>) {
906 let mut pts = Vec::new();
908 let mut vals = Vec::new();
909
910 for i in 0..4 {
911 for j in 0..4 {
912 let x = i as f64 / 3.0;
913 let y = j as f64 / 3.0;
914 pts.push(x);
915 pts.push(y);
916 vals.push(x + y);
917 }
918 }
919
920 let points = Array2::from_shape_vec((16, 2), pts).expect("valid shape");
921 let values = Array1::from_vec(vals);
922 (points, values)
923 }
924
925 fn make_quadratic_data() -> (Array2<f64>, Array1<f64>) {
926 let mut pts = Vec::new();
928 let mut vals = Vec::new();
929
930 for i in 0..5 {
932 for j in 0..5 {
933 let x = i as f64 / 4.0;
934 let y = j as f64 / 4.0;
935 pts.push(x);
936 pts.push(y);
937 vals.push(x * x + y * y);
938 }
939 }
940
941 let points = Array2::from_shape_vec((25, 2), pts).expect("valid shape");
942 let values = Array1::from_vec(vals);
943 (points, values)
944 }
945
946 #[test]
949 fn test_delaunay_basic() {
950 let points = vec![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]];
951 let tri = DelaunayTriangulation::new(&points).expect("valid triangulation");
952
953 assert_eq!(
954 tri.num_triangles(),
955 2,
956 "Unit square should have 2 triangles"
957 );
958 assert_eq!(tri.points().len(), 4);
959 }
960
961 #[test]
962 fn test_delaunay_larger() {
963 let mut points = Vec::new();
964 for i in 0..5 {
965 for j in 0..5 {
966 points.push([i as f64, j as f64]);
967 }
968 }
969 let tri = DelaunayTriangulation::new(&points).expect("valid triangulation");
970
971 assert!(
973 tri.num_triangles() >= 20,
974 "25 points should give at least 20 triangles, got {}",
975 tri.num_triangles()
976 );
977 }
978
979 #[test]
980 fn test_delaunay_too_few_points() {
981 let points = vec![[0.0, 0.0], [1.0, 0.0]];
982 let result = DelaunayTriangulation::new(&points);
983 assert!(result.is_err(), "2 points should fail");
984 }
985
986 #[test]
987 fn test_delaunay_find_containing_triangle() {
988 let points = vec![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]];
989 let tri = DelaunayTriangulation::new(&points).expect("valid triangulation");
990
991 let result = tri.find_containing_triangle(0.25, 0.25);
993 assert!(result.is_some(), "Interior point should be found");
994
995 let (_, bary) = result.expect("has result");
996 assert!(
997 bary[0] >= -1e-10 && bary[1] >= -1e-10 && bary[2] >= -1e-10,
998 "Barycentric coords should be non-negative"
999 );
1000
1001 let result = tri.find_containing_triangle(-1.0, -1.0);
1003 assert!(result.is_none(), "Exterior point should not be found");
1004 }
1005
1006 #[test]
1009 fn test_linear_at_data_points() {
1010 let (points, values) = make_larger_data();
1011 let interp = TriangulationInterpolator::new(
1012 points.clone(),
1013 values.clone(),
1014 TriangulationMethod::Linear,
1015 )
1016 .expect("valid");
1017
1018 for i in 0..points.nrows() {
1020 let result = interp
1021 .evaluate_point(points[[i, 0]], points[[i, 1]])
1022 .expect("valid");
1023 if result.is_nan() {
1024 continue; }
1026 assert!(
1027 (result - values[i]).abs() < 1e-8,
1028 "Linear at data point {}: expected {}, got {}",
1029 i,
1030 values[i],
1031 result
1032 );
1033 }
1034 }
1035
1036 #[test]
1037 fn test_linear_reproduces_linear_function() {
1038 let (points, values) = make_larger_data();
1039 let interp = TriangulationInterpolator::new(points, values, TriangulationMethod::Linear)
1040 .expect("valid");
1041
1042 let test_points = vec![(0.25, 0.25), (0.5, 0.5), (0.75, 0.25), (0.4, 0.6)];
1044 for (x, y) in test_points {
1045 let result = interp.evaluate_point(x, y).expect("valid");
1046 if result.is_nan() {
1047 continue;
1048 }
1049 let expected = x + y;
1050 assert!(
1051 (result - expected).abs() < 1e-8,
1052 "Linear at ({}, {}): expected {}, got {}",
1053 x,
1054 y,
1055 expected,
1056 result
1057 );
1058 }
1059 }
1060
1061 #[test]
1062 fn test_linear_interior_point() {
1063 let (points, values) = make_square_data();
1064 let interp = TriangulationInterpolator::new(points, values, TriangulationMethod::Linear)
1065 .expect("valid");
1066
1067 let result = interp.evaluate_point(0.5, 0.5).expect("valid");
1068 assert!(
1070 (result - 1.0).abs() < 1e-8,
1071 "Linear at (0.5, 0.5): expected 1.0, got {}",
1072 result
1073 );
1074 }
1075
1076 #[test]
1079 fn test_nearest_vertex_at_data_points() {
1080 let (points, values) = make_larger_data();
1081 let interp = TriangulationInterpolator::new(
1082 points.clone(),
1083 values.clone(),
1084 TriangulationMethod::NearestVertex,
1085 )
1086 .expect("valid");
1087
1088 for i in 0..points.nrows() {
1089 let result = interp
1090 .evaluate_point(points[[i, 0]], points[[i, 1]])
1091 .expect("valid");
1092 if result.is_nan() {
1093 continue;
1094 }
1095 assert!(
1096 (result - values[i]).abs() < 1e-8,
1097 "NearestVertex at data point {}: expected {}, got {}",
1098 i,
1099 values[i],
1100 result
1101 );
1102 }
1103 }
1104
1105 #[test]
1106 fn test_nearest_vertex_is_piecewise_constant() {
1107 let (points, values) = make_square_data();
1108 let interp =
1109 TriangulationInterpolator::new(points, values, TriangulationMethod::NearestVertex)
1110 .expect("valid");
1111
1112 let result = interp.evaluate_point(0.01, 0.01).expect("valid");
1114 if !result.is_nan() {
1115 assert!(
1116 (result - 0.0).abs() < 1e-8,
1117 "NearestVertex near (0,0): expected 0.0, got {}",
1118 result
1119 );
1120 }
1121 }
1122
1123 #[test]
1126 fn test_clough_tocher_at_data_points() {
1127 let (points, values) = make_larger_data();
1128 let interp = TriangulationInterpolator::new(
1129 points.clone(),
1130 values.clone(),
1131 TriangulationMethod::CloughTocher,
1132 )
1133 .expect("valid");
1134
1135 for i in 0..points.nrows() {
1137 let result = interp
1138 .evaluate_point(points[[i, 0]], points[[i, 1]])
1139 .expect("valid");
1140 if result.is_nan() {
1141 continue;
1142 }
1143 assert!(
1144 (result - values[i]).abs() < 1e-6,
1145 "CloughTocher at data point {}: expected {}, got {}",
1146 i,
1147 values[i],
1148 result
1149 );
1150 }
1151 }
1152
1153 #[test]
1154 fn test_clough_tocher_smoother_than_linear() {
1155 let (points, values) = make_quadratic_data();
1156
1157 let linear = TriangulationInterpolator::new(
1158 points.clone(),
1159 values.clone(),
1160 TriangulationMethod::Linear,
1161 )
1162 .expect("valid");
1163
1164 let ct = TriangulationInterpolator::new(points, values, TriangulationMethod::CloughTocher)
1165 .expect("valid");
1166
1167 let test_points = vec![(0.3, 0.3), (0.5, 0.5), (0.7, 0.3)];
1169 let mut ct_error_sum = 0.0;
1170 let mut count = 0;
1171
1172 for (x, y) in test_points {
1173 let exact = x * x + y * y;
1174
1175 let r_linear = linear.evaluate_point(x, y).expect("valid");
1176 let r_ct = ct.evaluate_point(x, y).expect("valid");
1177
1178 if r_linear.is_nan() || r_ct.is_nan() {
1179 continue;
1180 }
1181
1182 let _linear_err = (r_linear - exact).abs();
1184 ct_error_sum += (r_ct - exact).abs();
1185 count += 1;
1186 }
1187
1188 if count > 0 {
1189 let ct_avg = ct_error_sum / count as f64;
1192 assert!(
1193 ct_avg < 1.0,
1194 "Clough-Tocher average error should be reasonable: {}",
1195 ct_avg
1196 );
1197 }
1198 }
1199
1200 #[test]
1203 fn test_batch_evaluation() {
1204 let (points, values) = make_larger_data();
1205 let interp = TriangulationInterpolator::new(points, values, TriangulationMethod::Linear)
1206 .expect("valid");
1207
1208 let queries =
1209 Array2::from_shape_vec((3, 2), vec![0.25, 0.25, 0.5, 0.5, 0.4, 0.6]).expect("valid");
1210
1211 let results = interp.evaluate(&queries).expect("valid");
1212 assert_eq!(results.len(), 3);
1213
1214 for i in 0..3 {
1215 if results[i].is_nan() {
1216 continue;
1217 }
1218 let expected = queries[[i, 0]] + queries[[i, 1]];
1219 assert!(
1220 (results[i] - expected).abs() < 1e-8,
1221 "Batch result {}: expected {}, got {}",
1222 i,
1223 expected,
1224 results[i]
1225 );
1226 }
1227 }
1228
1229 #[test]
1232 fn test_exterior_nan() {
1233 let (points, values) = make_square_data();
1234 let interp = TriangulationInterpolator::with_exterior(
1235 points,
1236 values,
1237 TriangulationMethod::Linear,
1238 ExteriorHandling::Nan,
1239 )
1240 .expect("valid");
1241
1242 let result = interp.evaluate_point(-1.0, -1.0).expect("valid");
1243 assert!(result.is_nan(), "Exterior point should return NaN");
1244 }
1245
1246 #[test]
1247 fn test_exterior_error() {
1248 let (points, values) = make_square_data();
1249 let interp = TriangulationInterpolator::with_exterior(
1250 points,
1251 values,
1252 TriangulationMethod::Linear,
1253 ExteriorHandling::Error,
1254 )
1255 .expect("valid");
1256
1257 let result = interp.evaluate_point(-1.0, -1.0);
1258 assert!(result.is_err(), "Exterior point should return error");
1259 }
1260
1261 #[test]
1262 fn test_exterior_nearest_neighbor() {
1263 let (points, values) = make_square_data();
1264 let interp = TriangulationInterpolator::with_exterior(
1265 points,
1266 values,
1267 TriangulationMethod::Linear,
1268 ExteriorHandling::NearestNeighbor,
1269 )
1270 .expect("valid");
1271
1272 let result = interp.evaluate_point(-0.1, -0.1).expect("valid");
1274 assert!(
1275 (result - 0.0).abs() < 1e-8,
1276 "Exterior NN at (-0.1, -0.1): expected 0.0, got {}",
1277 result
1278 );
1279 }
1280
1281 #[test]
1284 fn test_non_2d_rejected() {
1285 let points = Array2::from_shape_vec((3, 3), vec![0.0; 9]).expect("valid shape");
1286 let values = Array1::from_vec(vec![0.0, 1.0, 2.0]);
1287 let result = TriangulationInterpolator::new(points, values, TriangulationMethod::Linear);
1288 assert!(result.is_err(), "3D points should be rejected");
1289 }
1290
1291 #[test]
1292 fn test_mismatched_sizes_rejected() {
1293 let points = Array2::from_shape_vec((4, 2), vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0])
1294 .expect("valid shape");
1295 let values = Array1::from_vec(vec![0.0, 1.0]); let result = TriangulationInterpolator::new(points, values, TriangulationMethod::Linear);
1297 assert!(result.is_err(), "Mismatched sizes should be rejected");
1298 }
1299
1300 #[test]
1301 fn test_too_few_points_rejected() {
1302 let points = Array2::from_shape_vec((2, 2), vec![0.0, 0.0, 1.0, 1.0]).expect("valid shape");
1303 let values = Array1::from_vec(vec![0.0, 1.0]);
1304 let result = TriangulationInterpolator::new(points, values, TriangulationMethod::Linear);
1305 assert!(result.is_err(), "2 points should be rejected");
1306 }
1307
1308 #[test]
1311 fn test_make_linear_triangulation() {
1312 let (points, values) = make_larger_data();
1313 let interp = make_linear_triangulation(points, values).expect("valid");
1314 let result = interp.evaluate_point(0.5, 0.5).expect("valid");
1315 if !result.is_nan() {
1316 assert!(
1317 (result - 1.0).abs() < 1e-8,
1318 "make_linear_triangulation at (0.5,0.5): expected 1.0, got {}",
1319 result
1320 );
1321 }
1322 }
1323
1324 #[test]
1325 fn test_make_clough_tocher_interpolator() {
1326 let (points, values) = make_larger_data();
1327 let interp = make_clough_tocher_interpolator(points, values).expect("valid");
1328 let result = interp.evaluate_point(0.5, 0.5).expect("valid");
1329 assert!(result.is_finite() || result.is_nan());
1330 }
1331
1332 #[test]
1333 fn test_make_nearest_vertex_interpolator() {
1334 let (points, values) = make_larger_data();
1335 let interp = make_nearest_vertex_interpolator(points, values).expect("valid");
1336 let result = interp.evaluate_point(0.5, 0.5).expect("valid");
1337 assert!(result.is_finite() || result.is_nan());
1338 }
1339
1340 #[test]
1343 fn test_accessors() {
1344 let (points, values) = make_larger_data();
1345 let interp = TriangulationInterpolator::new(points, values, TriangulationMethod::Linear)
1346 .expect("valid");
1347
1348 assert_eq!(interp.num_points(), 16);
1349 assert!(interp.num_triangles() >= 10);
1350 assert_eq!(interp.values().len(), 16);
1351 assert!(interp.triangulation().num_triangles() >= 10);
1352 }
1353
1354 #[test]
1357 fn test_linear_convergence_quadratic() {
1358 let test_point = (0.37_f64, 0.63_f64);
1362 let exact_value = 0.37 * 0.37 + 0.63 * 0.63;
1363
1364 let mut errors = Vec::new();
1365 for &n in &[5, 10, 20] {
1366 let mut pts = Vec::new();
1367 let mut vals = Vec::new();
1368
1369 for i in 0..n {
1370 for j in 0..n {
1371 let x = i as f64 / (n - 1) as f64;
1372 let y = j as f64 / (n - 1) as f64;
1373 pts.push(x);
1374 pts.push(y);
1375 vals.push(x * x + y * y);
1376 }
1377 }
1378
1379 let points = Array2::from_shape_vec((n * n, 2), pts).expect("valid");
1380 let values = Array1::from_vec(vals);
1381
1382 let interp =
1383 TriangulationInterpolator::new(points, values, TriangulationMethod::Linear)
1384 .expect("valid");
1385
1386 let result = interp
1387 .evaluate_point(test_point.0, test_point.1)
1388 .expect("valid");
1389
1390 if result.is_nan() {
1391 continue;
1392 }
1393
1394 let error = (result - exact_value).abs();
1395 errors.push(error);
1396 }
1397
1398 if errors.len() >= 2 {
1400 assert!(
1401 errors[errors.len() - 1] < errors[0] || errors[errors.len() - 1] < 1e-10,
1402 "Error should decrease: first={}, last={}",
1403 errors[0],
1404 errors[errors.len() - 1]
1405 );
1406 }
1407
1408 if let Some(&last) = errors.last() {
1409 assert!(last < 0.01, "Should converge: final error = {}", last);
1410 }
1411 }
1412}