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}