Skip to main content

scirs2_interpolate/
triangulation_interp.rs

1//! Triangulation-based interpolation for 2D scattered data
2//!
3//! This module provides interpolation methods built on top of Delaunay triangulations
4//! of 2D scattered data. The triangulation serves as the underlying spatial structure
5//! upon which interpolation is performed.
6//!
7//! ## Methods
8//!
9//! - **Linear triangulation interpolation**: Barycentric interpolation within each
10//!   triangle of the Delaunay triangulation. C0 continuous everywhere, exact at data
11//!   points.
12//!
13//! - **Clough-Tocher C1 interpolation**: Smooth (C1) interpolation that subdivides
14//!   each triangle into three sub-triangles and uses cubic polynomials. Requires
15//!   gradient estimation at data points.
16//!
17//! - **Nearest vertex interpolation**: Returns the value of the nearest vertex of
18//!   the enclosing triangle. Piecewise constant.
19//!
20//! ## Delaunay Triangulation
21//!
22//! The module includes a robust incremental Delaunay triangulation algorithm that
23//! handles degenerate cases (collinear points, coincident points, etc.).
24//!
25//! ## Examples
26//!
27//! ```rust
28//! use scirs2_core::ndarray::{Array1, Array2};
29//! use scirs2_interpolate::triangulation_interp::{
30//!     TriangulationInterpolator, TriangulationMethod,
31//! };
32//!
33//! // Scattered 2D data: z = x + y
34//! let points = Array2::from_shape_vec((4, 2), vec![
35//!     0.0_f64, 0.0,
36//!     1.0, 0.0,
37//!     0.0, 1.0,
38//!     1.0, 1.0,
39//! ]).expect("valid shape");
40//! let values = Array1::from_vec(vec![0.0_f64, 1.0, 1.0, 2.0]);
41//!
42//! let interp = TriangulationInterpolator::new(
43//!     points,
44//!     values,
45//!     TriangulationMethod::Linear,
46//! ).expect("valid");
47//!
48//! let result = interp.evaluate_point(0.5_f64, 0.5).expect("valid");
49//! assert!((result - 1.0_f64).abs() < 1e-10);
50//! ```
51
52use crate::error::{InterpolateError, InterpolateResult};
53use scirs2_core::ndarray::{Array1, Array2, ArrayView1};
54use scirs2_core::numeric::{Float, FromPrimitive};
55use std::fmt::Debug;
56
57// ---------------------------------------------------------------------------
58// Delaunay triangulation
59// ---------------------------------------------------------------------------
60
61/// A triangle defined by three vertex indices
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub struct Triangle {
64    /// Vertex indices (into the points array)
65    pub vertices: [usize; 3],
66}
67
68/// Delaunay triangulation of 2D point data
69///
70/// Uses the Bowyer-Watson incremental insertion algorithm.
71#[derive(Debug, Clone)]
72pub struct DelaunayTriangulation<F: Float + FromPrimitive + Debug> {
73    /// The input points (x, y coordinates)
74    points: Vec<[F; 2]>,
75    /// The triangles forming the triangulation
76    triangles: Vec<Triangle>,
77    /// Number of original (non-super-triangle) points
78    n_original: usize,
79}
80
81impl<F: Float + FromPrimitive + Debug> DelaunayTriangulation<F> {
82    /// Build a Delaunay triangulation from a set of 2D points
83    ///
84    /// # Arguments
85    ///
86    /// * `points` - 2D point coordinates as (x, y) pairs
87    ///
88    /// # Errors
89    ///
90    /// Returns an error if fewer than 3 points are provided or if all points
91    /// are collinear.
92    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        // Compute bounding box
103        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        // Guard against degenerate case where all points are the same
128        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        // Create super-triangle that contains all points
139        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        // Start with the super-triangle vertices plus the input points
154        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        // Insert each point
167        for i in 0..n {
168            let pt_idx = i + 3; // Offset by super-triangle vertices
169            let pt = points[pt_idx];
170
171            // Find all triangles whose circumcircle contains the point
172            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            // Find the boundary polygon of the hole
180            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                    // Check if this edge is shared with another bad triangle
187                    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            // Remove bad triangles (in reverse order to preserve indices)
205            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            // Re-triangulate the hole with the new point
212            for edge in &boundary_edges {
213                triangles.push(Triangle {
214                    vertices: [edge[0], edge[1], pt_idx],
215                });
216            }
217        }
218
219        // Remove triangles that reference super-triangle vertices (indices 0, 1, 2)
220        triangles
221            .retain(|tri| tri.vertices[0] >= 3 && tri.vertices[1] >= 3 && tri.vertices[2] >= 3);
222
223        // Remap vertex indices (subtract 3 to remove super-triangle offset)
224        for tri in &mut triangles {
225            tri.vertices[0] -= 3;
226            tri.vertices[1] -= 3;
227            tri.vertices[2] -= 3;
228        }
229
230        // Remove super-triangle vertices from points
231        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    /// Check if a point lies inside the circumcircle of a triangle
247    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        // For counter-clockwise triangles, det > 0 means inside circumcircle
264        // We need to handle both orientations
265        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    /// Compute the orientation of triangle (a, b, c)
274    /// Positive = counter-clockwise, Negative = clockwise, Zero = collinear
275    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    /// Check if a triangle has a specific edge
280    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    /// Get the triangles
292    pub fn triangles(&self) -> &[Triangle] {
293        &self.triangles
294    }
295
296    /// Get the points
297    pub fn points(&self) -> &[[F; 2]] {
298        &self.points
299    }
300
301    /// Get the number of triangles
302    pub fn num_triangles(&self) -> usize {
303        self.triangles.len()
304    }
305
306    /// Find the triangle containing a query point
307    ///
308    /// Returns the triangle index and barycentric coordinates, or None if
309    /// the point is outside the convex hull.
310    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                // Check all barycentric coordinates are >= 0 (with tolerance)
317                if bc[0] >= eps && bc[1] >= eps && bc[2] >= eps {
318                    return Some((i, bc));
319                }
320            }
321        }
322        None
323    }
324
325    /// Compute barycentric coordinates of point (x, y) with respect to a triangle
326    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; // Degenerate triangle
343        }
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// ---------------------------------------------------------------------------
355// Interpolation method
356// ---------------------------------------------------------------------------
357
358/// Interpolation method for triangulation-based interpolation
359#[derive(Debug, Clone, Copy, PartialEq)]
360pub enum TriangulationMethod {
361    /// Linear (barycentric) interpolation within each triangle
362    ///
363    /// C0 continuous. The interpolated surface is a piecewise planar surface
364    /// over the triangulation.
365    Linear,
366
367    /// Clough-Tocher C1 smooth interpolation
368    ///
369    /// Subdivides each triangle into 3 sub-triangles and uses cubic Bernstein
370    /// polynomials for C1 continuity. Gradient estimation at vertices uses
371    /// least-squares fitting.
372    CloughTocher,
373
374    /// Nearest vertex interpolation
375    ///
376    /// Returns the value of the nearest vertex of the enclosing triangle.
377    /// Piecewise constant within each Voronoi cell.
378    NearestVertex,
379}
380
381/// How to handle query points outside the convex hull
382#[derive(Debug, Clone, Copy, PartialEq)]
383pub enum ExteriorHandling {
384    /// Return NaN for exterior points
385    Nan,
386    /// Return the value of the nearest data point
387    NearestNeighbor,
388    /// Return an error
389    Error,
390}
391
392// ---------------------------------------------------------------------------
393// Main interpolator
394// ---------------------------------------------------------------------------
395
396/// Triangulation-based 2D scattered data interpolator
397///
398/// Builds a Delaunay triangulation of the input points and performs
399/// interpolation within each triangle using the chosen method.
400#[derive(Debug, Clone)]
401pub struct TriangulationInterpolator<F: Float + FromPrimitive + Debug> {
402    /// The Delaunay triangulation
403    triangulation: DelaunayTriangulation<F>,
404    /// Values at data points
405    values: Array1<F>,
406    /// Interpolation method
407    method: TriangulationMethod,
408    /// How to handle exterior points
409    exterior: ExteriorHandling,
410    /// Estimated gradients at vertices (for Clough-Tocher)
411    gradients: Option<Vec<[F; 2]>>,
412}
413
414impl<F: Float + FromPrimitive + Debug + std::fmt::Display + 'static> TriangulationInterpolator<F> {
415    /// Create a new triangulation-based interpolator
416    ///
417    /// # Arguments
418    ///
419    /// * `points` - 2D scattered data points, shape (n_points, 2)
420    /// * `values` - Values at data points, shape (n_points,)
421    /// * `method` - Interpolation method
422    ///
423    /// # Errors
424    ///
425    /// Returns an error if the data has fewer than 3 points, if the points are
426    /// not 2D, or if the triangulation fails.
427    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    /// Create a new interpolator with custom exterior handling
436    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        // Convert to internal format
458        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        // Estimate gradients for Clough-Tocher
465        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    /// Evaluate the interpolator at a single point (x, y)
481    ///
482    /// # Arguments
483    ///
484    /// * `x` - x-coordinate of the query point
485    /// * `y` - y-coordinate of the query point
486    ///
487    /// # Errors
488    ///
489    /// Returns an error if exterior handling is set to Error and the point
490    /// is outside the convex hull.
491    pub fn evaluate_point(&self, x: F, y: F) -> InterpolateResult<F> {
492        // Find the containing triangle
493        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                // Point is outside the convex hull
505                self.handle_exterior(x, y)
506            }
507        }
508    }
509
510    /// Evaluate the interpolator at a single point given as an array
511    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    /// Evaluate the interpolator at multiple points
523    ///
524    /// # Arguments
525    ///
526    /// * `queries` - Query points, shape (n_queries, 2)
527    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    /// Get a reference to the Delaunay triangulation
545    pub fn triangulation(&self) -> &DelaunayTriangulation<F> {
546        &self.triangulation
547    }
548
549    /// Get a reference to the values
550    pub fn values(&self) -> &Array1<F> {
551        &self.values
552    }
553
554    /// Get the number of data points
555    pub fn num_points(&self) -> usize {
556        self.values.len()
557    }
558
559    /// Get the number of triangles
560    pub fn num_triangles(&self) -> usize {
561        self.triangulation.num_triangles()
562    }
563
564    // -----------------------------------------------------------------------
565    // Private: linear interpolation
566    // -----------------------------------------------------------------------
567
568    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    // -----------------------------------------------------------------------
578    // Private: nearest vertex interpolation
579    // -----------------------------------------------------------------------
580
581    fn nearest_vertex_interpolate(&self, tri_idx: usize, bary: &[F; 3]) -> InterpolateResult<F> {
582        let tri = &self.triangulation.triangles()[tri_idx];
583
584        // Find the vertex with the largest barycentric coordinate
585        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    // -----------------------------------------------------------------------
598    // Private: Clough-Tocher C1 interpolation
599    // -----------------------------------------------------------------------
600
601    /// Estimate gradients at each vertex using least-squares over adjacent triangles
602    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        // For each vertex, find adjacent vertices through shared triangles
611        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            // Collect all neighbors from triangles containing vertex i
620            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            // Deduplicate (same neighbor can appear from multiple triangles)
644            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                // No neighbors, gradient is zero
656                continue;
657            }
658
659            // Solve the least-squares problem:
660            // minimize sum_j (dx_j * gx + dy_j * gy - df_j)^2
661            // Normal equations: [A^T A] [gx; gy] = A^T b
662            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            // If det is too small, keep gradient as zero
684        }
685
686        Ok(gradients)
687    }
688
689    /// Clough-Tocher C1 interpolation within a triangle
690    ///
691    /// This is a simplified Clough-Tocher scheme that blends the linear
692    /// interpolant with gradient-corrected cubic terms.
693    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        // Clough-Tocher interpolation using the cubic Hermite approach:
724        //
725        // At each vertex i, we have: value f_i and gradient (gx_i, gy_i).
726        // The Clough-Tocher element uses these to construct a C1 surface.
727        //
728        // Simplified approach: blend the linear interpolant with gradient corrections.
729        // f(x,y) = f_linear(x,y) + sum_i phi_i(bary) * correction_i
730        //
731        // where correction_i accounts for the difference between the gradient
732        // at vertex i and the gradient of the linear interpolant.
733
734        let lambda0 = bary[0];
735        let lambda1 = bary[1];
736        let lambda2 = bary[2];
737
738        // Linear interpolant value
739        let f_linear = lambda0 * f0 + lambda1 * f1 + lambda2 * f2;
740
741        // Gradient of the linear interpolant (constant over the triangle)
742        // Using the relation: grad f_linear = sum_i f_i * grad(lambda_i)
743        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            // Degenerate triangle, fall back to linear
751            return Ok(f_linear);
752        }
753
754        // Gradient corrections at each vertex
755        // diff_i = gradient at vertex i - linear gradient
756        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        // For each vertex, compute the correction term
763        // The correction at point (x,y) near vertex i is:
764        // (g_i - grad_lin) . (p - p_i) * lambda_i^2
765        // This ensures C1 continuity at the vertices
766
767        let dx = x - p0[0];
768        let dy = x - p0[1];
769        let _ = (dx, dy);
770
771        let mut correction = F::zero();
772
773        // Vertex 0 correction
774        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        // Vertex 1 correction
780        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        // Vertex 2 correction
786        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    // -----------------------------------------------------------------------
795    // Private: exterior handling
796    // -----------------------------------------------------------------------
797
798    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                // Find the nearest data point
807                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
827// ---------------------------------------------------------------------------
828// Convenience constructors
829// ---------------------------------------------------------------------------
830
831/// Create a linear triangulation interpolator
832///
833/// # Arguments
834///
835/// * `points` - 2D scattered data points, shape (n_points, 2)
836/// * `values` - Values at data points, shape (n_points,)
837///
838/// # Examples
839///
840/// ```rust
841/// use scirs2_core::ndarray::{Array1, Array2};
842/// use scirs2_interpolate::triangulation_interp::make_linear_triangulation;
843///
844/// let points = Array2::from_shape_vec((4, 2), vec![
845///     0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0,
846/// ]).expect("valid shape");
847/// let values = Array1::from_vec(vec![0.0, 1.0, 1.0, 2.0]);
848///
849/// let interp = make_linear_triangulation(points, values).expect("valid");
850/// ```
851pub 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
858/// Create a Clough-Tocher C1 smooth triangulation interpolator
859///
860/// # Arguments
861///
862/// * `points` - 2D scattered data points, shape (n_points, 2)
863/// * `values` - Values at data points, shape (n_points,)
864pub 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
873/// Create a nearest-vertex triangulation interpolator
874///
875/// # Arguments
876///
877/// * `points` - 2D scattered data points, shape (n_points, 2)
878/// * `values` - Values at data points, shape (n_points,)
879pub 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// ---------------------------------------------------------------------------
889// Tests
890// ---------------------------------------------------------------------------
891
892#[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        // 4 corners of unit square, z = x + y
899        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        // More points for better triangulation, z = x + y
907        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        // z = x^2 + y^2 on a scattered set of points
927        let mut pts = Vec::new();
928        let mut vals = Vec::new();
929
930        // Grid points
931        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    // === Delaunay triangulation tests ===
947
948    #[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        // For n points in general position, Delaunay should have roughly 2n - 5 triangles
972        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        // Point inside the convex hull
992        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        // Point outside the convex hull
1002        let result = tri.find_containing_triangle(-1.0, -1.0);
1003        assert!(result.is_none(), "Exterior point should not be found");
1004    }
1005
1006    // === Linear interpolation tests ===
1007
1008    #[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        // At data points, should reproduce exact values
1019        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; // Skip boundary points that might be outside
1025            }
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        // Linear interpolation on triangulations should reproduce linear functions exactly
1043        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        // z = x + y at (0.5, 0.5) = 1.0
1069        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    // === Nearest vertex tests ===
1077
1078    #[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        // Very close to (0, 0) should return value at (0, 0) = 0.0
1113        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    // === Clough-Tocher tests ===
1124
1125    #[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        // At data points, Clough-Tocher should reproduce exact values
1136        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        // Test at several interior points
1168        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            // We only check that CT produces reasonable results
1183            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            // Clough-Tocher should generally be more accurate for smooth functions
1190            // (or at least not much worse)
1191            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    // === Batch evaluation tests ===
1201
1202    #[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    // === Exterior handling tests ===
1230
1231    #[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        // (-0.1, -0.1) should be nearest to (0, 0) with value 0
1273        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    // === Edge case tests ===
1282
1283    #[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]); // Only 2 values for 4 points
1296        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    // === Convenience constructor tests ===
1309
1310    #[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    // === Accessor tests ===
1341
1342    #[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    // === Convergence test ===
1355
1356    #[test]
1357    fn test_linear_convergence_quadratic() {
1358        // For f(x,y) = x^2 + y^2, linear triangulation interpolation
1359        // should converge as the grid is refined.
1360        // Use an off-grid point to avoid zero error from hitting a grid node.
1361        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        // Overall, error should decrease with refinement
1399        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}