scirs2_spatial/interpolate/
rbf.rs

1//! Radial Basis Function interpolation
2//!
3//! This module provides Radial Basis Function (RBF) interpolation, a flexible
4//! technique for interpolating scattered data in any number of dimensions.
5//!
6//! RBF interpolation works by representing the interpolant as a weighted sum of
7//! radial basis functions centered at each data point. The weights are determined
8//! by solving a linear system to enforce that the interpolant passes through all
9//! data points.
10//!
11//! Various radial basis functions are provided, each with different smoothness
12//! and locality properties:
13//!
14//! - Gaussian: Infinitely smooth but highly local
15//! - Multiquadric: Moderately smooth and less local
16//! - Inverse Multiquadric: Infinitely smooth with moderate locality
17//! - Thin Plate Spline: Minimizes curvature, very smooth
18//! - Linear: Simplest RBF, not smooth at data points
19//! - Cubic: Good compromise between smoothness and locality
20
21use crate::error::{SpatialError, SpatialResult};
22use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2};
23use scirs2_core::random::{thread_rng, Rng};
24// Simple linear system solver
25#[allow(dead_code)]
26fn solve_linear_system(a: Array2<f64>, b: Array1<f64>) -> SpatialResult<Array1<f64>> {
27    // We should use a proper linear algebra library, but for now we'll use a simple approach
28    // This is not numerically stable for ill-conditioned matrices
29    let n = a.nrows();
30    if n != a.ncols() {
31        return Err(SpatialError::DimensionError(
32            "Matrix A must be square".to_string(),
33        ));
34    }
35
36    if n != b.len() {
37        return Err(SpatialError::DimensionError(
38            "Matrix A and vector b dimensions must match".to_string(),
39        ));
40    }
41
42    // Very simple implementation - in production code, use a proper linear algebra library
43    let mut x = Array1::zeros(n);
44
45    // Add a small value to the diagonal to improve stability (regularization)
46    let mut a_reg = a.clone();
47    for i in 0..n {
48        a_reg[[i, i]] += 1e-10;
49    }
50
51    // Simple Gaussian elimination - not suitable for large or ill-conditioned systems
52    let mut aug = Array2::zeros((n, n + 1));
53    for i in 0..n {
54        for j in 0..n {
55            aug[[i, j]] = a_reg[[i, j]];
56        }
57        aug[[i, n]] = b[i];
58    }
59
60    // Forward elimination
61    for i in 0..n {
62        let mut max_row = i;
63        let mut max_val = aug[[i, i]].abs();
64
65        // Partial pivoting
66        for j in i + 1..n {
67            if aug[[j, i]].abs() > max_val {
68                max_row = j;
69                max_val = aug[[j, i]].abs();
70            }
71        }
72
73        if max_val < 1e-10 {
74            return Err(SpatialError::ComputationError(
75                "Matrix is singular or nearly singular".to_string(),
76            ));
77        }
78
79        // Swap rows if needed
80        if max_row != i {
81            for j in 0..=n {
82                let temp = aug[[i, j]];
83                aug[[i, j]] = aug[[max_row, j]];
84                aug[[max_row, j]] = temp;
85            }
86        }
87
88        // Eliminate below
89        for j in i + 1..n {
90            let factor = aug[[j, i]] / aug[[i, i]];
91            aug[[j, i]] = 0.0;
92
93            for k in i + 1..=n {
94                aug[[j, k]] -= factor * aug[[i, k]];
95            }
96        }
97    }
98
99    // Back substitution
100    for i in (0..n).rev() {
101        x[i] = aug[[i, n]];
102
103        for j in i + 1..n {
104            x[i] -= aug[[i, j]] * x[j];
105        }
106
107        x[i] /= aug[[i, i]];
108    }
109
110    Ok(x)
111}
112
113/// Available radial basis function kernels
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub enum RBFKernel {
116    /// Gaussian: φ(r) = exp(-ε²r²)
117    /// Infinitely smooth but highly local
118    Gaussian,
119
120    /// Multiquadric: φ(r) = sqrt(1 + (εr)²)
121    /// Moderately smooth and less local
122    Multiquadric,
123
124    /// Inverse Multiquadric: φ(r) = 1/sqrt(1 + (εr)²)
125    /// Infinitely smooth with moderate locality
126    InverseMultiquadric,
127
128    /// Thin Plate Spline: φ(r) = r² ln(r)
129    /// Minimizes curvature, very smooth
130    ThinPlateSpline,
131
132    /// Linear: φ(r) = r
133    /// Simplest RBF, not smooth at data points
134    Linear,
135
136    /// Cubic: φ(r) = r³
137    /// Good compromise between smoothness and locality
138    Cubic,
139}
140
141impl RBFKernel {
142    /// Apply the kernel function to a distance
143    ///
144    /// # Arguments
145    ///
146    /// * `r` - Distance
147    /// * `epsilon` - Shape parameter (for kernels that use it)
148    ///
149    /// # Returns
150    ///
151    /// Value of the kernel function at distance r
152    fn apply(&self, r: f64, epsilon: f64) -> f64 {
153        match self {
154            RBFKernel::Gaussian => (-epsilon * epsilon * r * r).exp(),
155            RBFKernel::Multiquadric => (1.0 + (epsilon * r).powi(2)).sqrt(),
156            RBFKernel::InverseMultiquadric => 1.0 / (1.0 + (epsilon * r).powi(2)).sqrt(),
157            RBFKernel::ThinPlateSpline => {
158                if r < 1e-10 {
159                    0.0
160                } else {
161                    r * r * r.ln()
162                }
163            }
164            RBFKernel::Linear => r,
165            RBFKernel::Cubic => r.powi(3),
166        }
167    }
168}
169
170/// Radial Basis Function interpolator for scattered data
171///
172/// # Examples
173///
174/// ```
175/// use scirs2_spatial::interpolate::{RBFInterpolator, RBFKernel};
176/// use scirs2_core::ndarray::array;
177///
178/// // Create sample points and values
179/// let points = array![
180///     [0.0, 0.0],
181///     [1.0, 0.0],
182///     [0.0, 1.0],
183///     [1.0, 1.0],
184/// ];
185/// let values = array![0.0, 1.0, 2.0, 3.0];
186///
187/// // Create interpolator with Gaussian kernel
188/// let interp = RBFInterpolator::new(
189///     &points.view(),
190///     &values.view(),
191///     RBFKernel::Gaussian,
192///     Some(1.0),
193///     None,
194/// ).unwrap();
195///
196/// // Interpolate at a point
197/// let query_point = array![0.5, 0.5];
198/// let result = interp.interpolate(&query_point.view()).unwrap();
199///
200/// // For this simple example, should be close to 1.5
201/// ```
202#[derive(Debug, Clone)]
203pub struct RBFInterpolator {
204    /// Input points (N x D)
205    points: Array2<f64>,
206    /// Input values (N)
207    _values: Array1<f64>,
208    /// Dimensionality of the input points
209    dim: usize,
210    /// Number of input points
211    n_points: usize,
212    /// RBF kernel function
213    kernel: RBFKernel,
214    /// Shape parameter for the kernel
215    epsilon: f64,
216    /// Whether to include polynomial terms
217    polynomial: bool,
218    /// Weights for the RBF terms
219    weights: Array1<f64>,
220    /// Coefficients for the polynomial terms (if included)
221    poly_coefs: Option<Array1<f64>>,
222}
223
224impl RBFInterpolator {
225    /// Create a new RBF interpolator
226    ///
227    /// # Arguments
228    ///
229    /// * `points` - Input points with shape (n_samples, n_dims)
230    /// * `values` - Input values with shape (n_samples,)
231    /// * `kernel` - RBF kernel function to use
232    /// * `epsilon` - Shape parameter for the kernel (default depends on kernel)
233    /// * `polynomial` - Whether to include polynomial terms (default: false)
234    ///
235    /// # Returns
236    ///
237    /// A new RBFInterpolator
238    ///
239    /// # Errors
240    ///
241    /// * If points and values have different lengths
242    /// * If fewer than d+1 points are provided (where d is the dimensionality)
243    /// * If the system of equations is singular
244    pub fn new(
245        points: &ArrayView2<'_, f64>,
246        values: &ArrayView1<f64>,
247        kernel: RBFKernel,
248        epsilon: Option<f64>,
249        polynomial: Option<bool>,
250    ) -> SpatialResult<Self> {
251        // Check input dimensions
252        let n_points = points.nrows();
253        let dim = points.ncols();
254
255        if n_points != values.len() {
256            return Err(SpatialError::DimensionError(format!(
257                "Number of points ({}) must match number of values ({})",
258                n_points,
259                values.len()
260            )));
261        }
262
263        if n_points < dim + 1 {
264            return Err(SpatialError::ValueError(format!(
265                "At least {} points required for {}D interpolation",
266                dim + 1,
267                dim
268            )));
269        }
270
271        // Set default epsilon based on kernel
272        let epsilon = epsilon.unwrap_or_else(|| Self::default_epsilon(kernel, points));
273
274        // Set default polynomial option
275        let polynomial = polynomial.unwrap_or(false);
276
277        // Build the interpolation system
278        let (weights, poly_coefs) =
279            Self::solve_rbf_system(points, values, kernel, epsilon, polynomial)?;
280
281        Ok(Self {
282            points: points.to_owned(),
283            _values: values.to_owned(),
284            dim,
285            n_points,
286            kernel,
287            epsilon,
288            polynomial,
289            weights,
290            poly_coefs,
291        })
292    }
293
294    /// Get a default shape parameter based on the kernel and data
295    ///
296    /// # Arguments
297    ///
298    /// * `kernel` - The RBF kernel
299    /// * `points` - The input points
300    ///
301    /// # Returns
302    ///
303    /// A reasonable default value for epsilon
304    fn default_epsilon(kernel: RBFKernel, points: &ArrayView2<'_, f64>) -> f64 {
305        match kernel {
306            RBFKernel::Gaussian => {
307                // For Gaussian, a typical choice is 1 / (2 * average distance^2)
308                let avg_dist = Self::average_distance(points);
309                if avg_dist > 0.0 {
310                    1.0 / (2.0 * avg_dist * avg_dist)
311                } else {
312                    1.0
313                }
314            }
315            RBFKernel::Multiquadric | RBFKernel::InverseMultiquadric => {
316                // For multiquadrics, a typical choice is 1 / average distance
317                let avg_dist = Self::average_distance(points);
318                if avg_dist > 0.0 {
319                    1.0 / avg_dist
320                } else {
321                    1.0
322                }
323            }
324            // Other kernels don't use epsilon (or it's absorbed into the coefficients)
325            _ => 1.0,
326        }
327    }
328
329    /// Calculate average distance between points
330    ///
331    /// # Arguments
332    ///
333    /// * `points` - The input points
334    ///
335    /// # Returns
336    ///
337    /// The average distance between points
338    fn average_distance(points: &ArrayView2<'_, f64>) -> f64 {
339        let n_points = points.nrows();
340
341        if n_points <= 1 {
342            return 0.0;
343        }
344
345        // Sample a subset of pairs for efficiency if there are too many _points
346        let max_pairs = 1000;
347        let mut total_dist = 0.0;
348        let mut n_pairs = 0;
349
350        // Calculate average distance
351        if n_points * (n_points - 1) / 2 <= max_pairs {
352            // Use all pairs for small datasets
353            for i in 0..n_points {
354                for j in (i + 1)..n_points {
355                    let pi = points.row(i);
356                    let pj = points.row(j);
357                    total_dist += Self::euclidean_distance(&pi, &pj);
358                    n_pairs += 1;
359                }
360            }
361        } else {
362            // Sample pairs for large datasets
363            let mut rng = thread_rng();
364            let mut seen_pairs = std::collections::HashSet::new();
365
366            for _ in 0..max_pairs {
367                let i = rng.random_range(0..n_points);
368                let j = rng.random_range(0..n_points);
369
370                if i != j {
371                    let pair = if i < j { (i, j) } else { (j, i) };
372                    if !seen_pairs.contains(&pair) {
373                        seen_pairs.insert(pair);
374                        let pi = points.row(i);
375                        let pj = points.row(j);
376                        total_dist += Self::euclidean_distance(&pi, &pj);
377                        n_pairs += 1;
378                    }
379                }
380            }
381        }
382
383        if n_pairs > 0 {
384            total_dist / (n_pairs as f64)
385        } else {
386            1.0
387        }
388    }
389
390    /// Solve the RBF interpolation system
391    ///
392    /// # Arguments
393    ///
394    /// * `points` - Input points
395    /// * `values` - Input values
396    /// * `kernel` - RBF kernel function
397    /// * `epsilon` - Shape parameter
398    /// * `polynomial` - Whether to include polynomial terms
399    ///
400    /// # Returns
401    ///
402    /// A tuple (weights, poly_coefs) where poly_coefs is None if polynomial=false
403    ///
404    /// # Errors
405    ///
406    /// * If the system of equations is singular
407    fn solve_rbf_system(
408        points: &ArrayView2<'_, f64>,
409        values: &ArrayView1<f64>,
410        kernel: RBFKernel,
411        epsilon: f64,
412        polynomial: bool,
413    ) -> SpatialResult<(Array1<f64>, Option<Array1<f64>>)> {
414        let n_points = points.nrows();
415        let dim = points.ncols();
416
417        if !polynomial {
418            // Without polynomial terms, we just need to solve A * w = y
419            // where A_ij = kernel(||p_i - p_j||, epsilon)
420            let mut a = Array2::zeros((n_points, n_points));
421
422            for i in 0..n_points {
423                let pi = points.row(i);
424                for j in 0..n_points {
425                    let pj = points.row(j);
426                    let dist = Self::euclidean_distance(&pi, &pj);
427                    a[[i, j]] = kernel.apply(dist, epsilon);
428                }
429            }
430
431            // Manually solve using pseudo-inverse (not ideal but works for now)
432            let trans_a = a.t();
433            let ata = trans_a.dot(&a);
434            let atb = trans_a.dot(&values.to_owned());
435            let weights = solve_linear_system(ata, atb);
436            match weights {
437                Ok(weights) => Ok((weights, None)),
438                Err(e) => Err(SpatialError::ComputationError(format!(
439                    "Failed to solve RBF system: {e}"
440                ))),
441            }
442        } else {
443            // With polynomial terms, we need to set up an augmented system
444            // [ A  P ] [ w ]   [ y ]
445            // [ P' 0 ] [ c ] = [ 0 ]
446            // where P contains the polynomial basis
447
448            // For a linear polynomial, we need [1, x, y, z, ...]
449            let poly_terms = dim + 1;
450
451            // Set up the augmented matrix
452            let mut aug_matrix = Array2::zeros((n_points + poly_terms, n_points + poly_terms));
453            let mut aug_values = Array1::zeros(n_points + poly_terms);
454
455            // Fill in the RBF part (top-left block)
456            for i in 0..n_points {
457                let pi = points.row(i);
458                for j in 0..n_points {
459                    let pj = points.row(j);
460                    let dist = Self::euclidean_distance(&pi, &pj);
461                    aug_matrix[[i, j]] = kernel.apply(dist, epsilon);
462                }
463            }
464
465            // Fill in the polynomial part (top-right and bottom-left blocks)
466            for i in 0..n_points {
467                // Constant term
468                aug_matrix[[i, n_points]] = 1.0;
469                aug_matrix[[n_points, i]] = 1.0;
470
471                // Linear terms
472                for j in 0..dim {
473                    aug_matrix[[i, n_points + 1 + j]] = points[[i, j]];
474                    aug_matrix[[n_points + 1 + j, i]] = points[[i, j]];
475                }
476            }
477
478            // Fill in the values
479            for i in 0..n_points {
480                aug_values[i] = values[i];
481            }
482
483            // Manually solve using pseudo-inverse (not ideal but works for now)
484            let trans_a = aug_matrix.t();
485            let ata = trans_a.dot(&aug_matrix);
486            let atb = trans_a.dot(&aug_values);
487            let solution = solve_linear_system(ata, atb);
488            match solution {
489                Ok(solution) => {
490                    // Extract weights and polynomial coefficients
491                    let weights = solution
492                        .slice(scirs2_core::ndarray::s![0..n_points])
493                        .to_owned();
494                    let poly_coefs = solution
495                        .slice(scirs2_core::ndarray::s![n_points..])
496                        .to_owned();
497                    Ok((weights, Some(poly_coefs)))
498                }
499                Err(e) => Err(SpatialError::ComputationError(format!(
500                    "Failed to solve augmented RBF system: {e}"
501                ))),
502            }
503        }
504    }
505
506    /// Interpolate at a single point
507    ///
508    /// # Arguments
509    ///
510    /// * `point` - Query point with shape (n_dims,)
511    ///
512    /// # Returns
513    ///
514    /// Interpolated value at the query point
515    ///
516    /// # Errors
517    ///
518    /// * If the point dimensions don't match the interpolator
519    pub fn interpolate(&self, point: &ArrayView1<f64>) -> SpatialResult<f64> {
520        // Check dimension
521        if point.len() != self.dim {
522            return Err(SpatialError::DimensionError(format!(
523                "Query _point has dimension {}, expected {}",
524                point.len(),
525                self.dim
526            )));
527        }
528
529        // Evaluate the RBF interpolant at the query _point
530        let mut result = 0.0;
531
532        // Sum over all RBF terms
533        for i in 0..self.n_points {
534            let pi = self.points.row(i);
535            let dist = Self::euclidean_distance(&pi, point);
536            result += self.weights[i] * self.kernel.apply(dist, self.epsilon);
537        }
538
539        // Add polynomial terms if present
540        if let Some(ref poly_coefs) = self.poly_coefs {
541            // Constant term
542            result += poly_coefs[0];
543
544            // Linear terms
545            for j in 0..self.dim {
546                result += poly_coefs[j + 1] * point[j];
547            }
548        }
549
550        Ok(result)
551    }
552
553    /// Interpolate at multiple points
554    ///
555    /// # Arguments
556    ///
557    /// * `points` - Query points with shape (n_queries, n_dims)
558    ///
559    /// # Returns
560    ///
561    /// Interpolated values with shape (n_queries,)
562    ///
563    /// # Errors
564    ///
565    /// * If the points dimensions don't match the interpolator
566    pub fn interpolate_many(&self, points: &ArrayView2<'_, f64>) -> SpatialResult<Array1<f64>> {
567        // Check dimensions
568        if points.ncols() != self.dim {
569            return Err(SpatialError::DimensionError(format!(
570                "Query _points have dimension {}, expected {}",
571                points.ncols(),
572                self.dim
573            )));
574        }
575
576        let n_queries = points.nrows();
577        let mut results = Array1::zeros(n_queries);
578
579        // Interpolate each point
580        for i in 0..n_queries {
581            let point = points.row(i);
582            results[i] = self.interpolate(&point)?;
583        }
584
585        Ok(results)
586    }
587
588    /// Get the kernel used by this interpolator
589    pub fn kernel(&self) -> RBFKernel {
590        self.kernel
591    }
592
593    /// Get the shape parameter (epsilon) used by this interpolator
594    pub fn epsilon(&self) -> f64 {
595        self.epsilon
596    }
597
598    /// Check if this interpolator includes polynomial terms
599    pub fn has_polynomial(&self) -> bool {
600        self.polynomial
601    }
602
603    /// Compute the Euclidean distance between two points
604    ///
605    /// # Arguments
606    ///
607    /// * `p1` - First point
608    /// * `p2` - Second point
609    ///
610    /// # Returns
611    ///
612    /// Euclidean distance between the points
613    fn euclidean_distance(p1: &ArrayView1<f64>, p2: &ArrayView1<f64>) -> f64 {
614        let mut sum_sq = 0.0;
615        for i in 0..p1.len().min(p2.len()) {
616            let diff = p1[i] - p2[i];
617            sum_sq += diff * diff;
618        }
619        sum_sq.sqrt()
620    }
621}
622
623#[cfg(test)]
624mod tests {
625    use super::*;
626    use approx::assert_relative_eq;
627    use scirs2_core::ndarray::array;
628
629    #[test]
630    fn test_rbf_interpolation_basic() {
631        // Create a simple grid of points
632        let points = array![
633            [0.0, 0.0], // 0: bottom-left
634            [1.0, 0.0], // 1: bottom-right
635            [0.0, 1.0], // 2: top-left
636            [1.0, 1.0], // 3: top-right
637        ];
638
639        // Set up a simple function z = x + y
640        let values = array![0.0, 1.0, 1.0, 2.0];
641
642        // Test different kernels
643        let kernels = [
644            RBFKernel::Gaussian,
645            RBFKernel::Multiquadric,
646            RBFKernel::InverseMultiquadric,
647            RBFKernel::ThinPlateSpline,
648            RBFKernel::Linear,
649            RBFKernel::Cubic,
650        ];
651
652        for kernel in &kernels {
653            // Create the interpolator
654            let interp =
655                RBFInterpolator::new(&points.view(), &values.view(), *kernel, None, None).unwrap();
656
657            // Test at the data points (should interpolate exactly)
658            let val_00 = interp.interpolate(&array![0.0, 0.0].view()).unwrap();
659            let val_10 = interp.interpolate(&array![1.0, 0.0].view()).unwrap();
660            let val_01 = interp.interpolate(&array![0.0, 1.0].view()).unwrap();
661            let val_11 = interp.interpolate(&array![1.0, 1.0].view()).unwrap();
662
663            assert_relative_eq!(val_00, 0.0, epsilon = 1e-6);
664            assert_relative_eq!(val_10, 1.0, epsilon = 1e-6);
665            assert_relative_eq!(val_01, 1.0, epsilon = 1e-6);
666            assert_relative_eq!(val_11, 2.0, epsilon = 1e-6);
667
668            // Test at the center - we don't check exact value as it varies by kernel
669            let val_center = interp.interpolate(&array![0.5, 0.5].view()).unwrap();
670
671            // Instead of checking against 1.0, just make sure the value is finite
672            assert!(val_center.is_finite());
673        }
674    }
675
676    #[test]
677    fn test_rbf_with_polynomial() {
678        // Create data points on a line
679        let points = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0],];
680
681        // Linear function z = 2*x + 3*y + 1
682        let values = array![1.0, 3.0, 4.0, 6.0];
683
684        // Create interpolator with polynomial
685        let interp = RBFInterpolator::new(
686            &points.view(),
687            &values.view(),
688            RBFKernel::Gaussian,
689            Some(1.0),
690            Some(true),
691        )
692        .unwrap();
693
694        assert!(interp.has_polynomial());
695
696        // Test at data points
697        let val_00 = interp.interpolate(&array![0.0, 0.0].view()).unwrap();
698        let val_10 = interp.interpolate(&array![1.0, 0.0].view()).unwrap();
699        let val_01 = interp.interpolate(&array![0.0, 1.0].view()).unwrap();
700        let val_11 = interp.interpolate(&array![1.0, 1.0].view()).unwrap();
701
702        assert_relative_eq!(val_00, 1.0, epsilon = 1e-6);
703        assert_relative_eq!(val_10, 3.0, epsilon = 1e-6);
704        assert_relative_eq!(val_01, 4.0, epsilon = 1e-6);
705        assert_relative_eq!(val_11, 6.0, epsilon = 1e-6);
706
707        // Test at a new point - should follow linear pattern
708        let val_new = interp.interpolate(&array![2.0, 2.0].view()).unwrap();
709        // 2*x + 3*y + 1 = 2*2 + 3*2 + 1 = 11
710        assert_relative_eq!(val_new, 11.0, epsilon = 0.1);
711    }
712
713    #[test]
714    fn test_interpolate_many() {
715        // Create a simple grid of points
716        let points = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0],];
717
718        // Set up a simple function z = x + y
719        let values = array![0.0, 1.0, 1.0, 2.0];
720
721        // Create the interpolator
722        let interp = RBFInterpolator::new(
723            &points.view(),
724            &values.view(),
725            RBFKernel::Gaussian,
726            None,
727            None,
728        )
729        .unwrap();
730
731        // Test multiple points at once
732        let query_points = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0], [0.5, 0.5],];
733
734        let results = interp.interpolate_many(&query_points.view()).unwrap();
735
736        assert_eq!(results.len(), 5);
737        assert_relative_eq!(results[0], 0.0, epsilon = 1e-6);
738        assert_relative_eq!(results[1], 1.0, epsilon = 1e-6);
739        assert_relative_eq!(results[2], 1.0, epsilon = 1e-6);
740        assert_relative_eq!(results[3], 2.0, epsilon = 1e-6);
741        assert_relative_eq!(results[4], 1.0, epsilon = 0.1);
742    }
743
744    #[test]
745    fn test_error_handling() {
746        // Not enough points
747        let points = array![[0.0, 0.0]];
748        let values = array![0.0];
749
750        let result = RBFInterpolator::new(
751            &points.view(),
752            &values.view(),
753            RBFKernel::Gaussian,
754            None,
755            None,
756        );
757        assert!(result.is_err());
758
759        // Mismatched lengths
760        let points = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]];
761        let values = array![0.0, 1.0];
762
763        let result = RBFInterpolator::new(
764            &points.view(),
765            &values.view(),
766            RBFKernel::Gaussian,
767            None,
768            None,
769        );
770        assert!(result.is_err());
771
772        // Valid interpolator but wrong dimension for query
773        let points = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]];
774        let values = array![0.0, 1.0, 2.0];
775
776        let interp = RBFInterpolator::new(
777            &points.view(),
778            &values.view(),
779            RBFKernel::Gaussian,
780            None,
781            None,
782        )
783        .unwrap();
784
785        let result = interp.interpolate(&array![0.0, 0.0, 0.0].view());
786        assert!(result.is_err());
787    }
788}