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