1use crate::error::{InterpolateError, InterpolateResult};
7use crate::interp1d::linear_interpolate;
8use crate::numerical_stability::solve_with_stability_monitoring;
9use crate::spline::CubicSpline;
10use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2};
11use scirs2_core::numeric::{Float, FromPrimitive};
12use std::fmt::{Debug, Display};
13use std::ops::{AddAssign, DivAssign, MulAssign, SubAssign};
14
15#[derive(Debug, Clone, Copy, PartialEq)]
17pub enum Interp2dKind {
18 Linear,
20 Cubic,
22 Quintic,
26}
27
28#[derive(Debug, Clone)]
33pub struct Interp2d<F> {
34 x: Array1<F>,
36 y: Array1<F>,
38 z: Array2<F>,
40 kind: Interp2dKind,
42}
43
44impl<F> Interp2d<F>
45where
46 F: Float + FromPrimitive + Debug + Clone + crate::traits::InterpolationFloat,
47{
48 pub fn new(
89 x: &ArrayView1<F>,
90 y: &ArrayView1<F>,
91 z: &ArrayView2<F>,
92 kind: Interp2dKind,
93 ) -> InterpolateResult<Self> {
94 if z.nrows() != y.len() || z.ncols() != x.len() {
96 return Err(InterpolateError::shape_mismatch(
97 format!("({}, {})", y.len(), x.len()),
98 format!("({}, {})", z.nrows(), z.ncols()),
99 "interp2d z array shape",
100 ));
101 }
102
103 if !is_sorted(x) {
105 return Err(InterpolateError::invalid_input(
106 "x coordinates must be sorted in ascending order",
107 ));
108 }
109
110 if !is_sorted(y) {
111 return Err(InterpolateError::invalid_input(
112 "y coordinates must be sorted in ascending order",
113 ));
114 }
115
116 if x.len() < 2 || y.len() < 2 {
118 return Err(InterpolateError::invalid_input(
119 "need at least 2 points in each dimension",
120 ));
121 }
122
123 Ok(Self {
124 x: x.to_owned(),
125 y: y.to_owned(),
126 z: z.to_owned(),
127 kind,
128 })
129 }
130
131 pub fn evaluate(&self, x_new: F, ynew: F) -> InterpolateResult<F> {
162 match self.kind {
163 Interp2dKind::Linear => self.evaluate_linear(x_new, ynew),
164 Interp2dKind::Cubic => self.evaluate_cubic(x_new, ynew),
165 Interp2dKind::Quintic => self.evaluate_quintic(x_new, ynew),
166 }
167 }
168
169 pub fn evaluate_array(
180 &self,
181 x_new: &ArrayView1<F>,
182 ynew: &ArrayView1<F>,
183 ) -> InterpolateResult<Array1<F>> {
184 if x_new.len() != ynew.len() {
185 return Err(InterpolateError::shape_mismatch(
186 format!("x_new.len() = {}", x_new.len()),
187 format!("ynew.len() = {}", ynew.len()),
188 "interp2d coordinate arrays",
189 ));
190 }
191
192 let mut result = Array1::zeros(x_new.len());
193 for i in 0..x_new.len() {
194 result[i] = self.evaluate(x_new[i], ynew[i])?;
195 }
196 Ok(result)
197 }
198
199 pub fn evaluate_grid(
210 &self,
211 x_new: &ArrayView1<F>,
212 ynew: &ArrayView1<F>,
213 ) -> InterpolateResult<Array2<F>> {
214 let mut result = Array2::zeros((ynew.len(), x_new.len()));
215
216 for (i, &y_val) in ynew.iter().enumerate() {
217 for (j, &x_val) in x_new.iter().enumerate() {
218 result[[i, j]] = self.evaluate(x_val, y_val)?;
219 }
220 }
221
222 Ok(result)
223 }
224
225 fn evaluate_linear(&self, x_new: F, ynew: F) -> InterpolateResult<F> {
227 let y_idx = find_interval(&self.y.view(), ynew);
229
230 let result = if y_idx == 0 && ynew < self.y[0] {
231 let row = self.z.slice(scirs2_core::ndarray::s![0, ..]);
233 linear_interpolate(&self.x.view(), &row, &Array1::from_vec(vec![x_new]).view())?[0]
234 } else if y_idx >= self.y.len() - 1 && ynew > self.y[self.y.len() - 1] {
235 let row = self.z.slice(scirs2_core::ndarray::s![self.y.len() - 1, ..]);
237 linear_interpolate(&self.x.view(), &row, &Array1::from_vec(vec![x_new]).view())?[0]
238 } else {
239 let y_idx = y_idx.min(self.y.len() - 2);
241
242 let row0 = self.z.slice(scirs2_core::ndarray::s![y_idx, ..]);
244 let row1 = self.z.slice(scirs2_core::ndarray::s![y_idx + 1, ..]);
245
246 let val0 =
247 linear_interpolate(&self.x.view(), &row0, &Array1::from_vec(vec![x_new]).view())?
248 [0];
249 let val1 =
250 linear_interpolate(&self.x.view(), &row1, &Array1::from_vec(vec![x_new]).view())?
251 [0];
252
253 let y0 = self.y[y_idx];
255 let y1 = self.y[y_idx + 1];
256
257 if (y1 - y0).abs() < F::epsilon() {
258 val0
259 } else {
260 let t = (ynew - y0) / (y1 - y0);
261 val0 + t * (val1 - val0)
262 }
263 };
264
265 Ok(result)
266 }
267
268 fn evaluate_cubic(&self, x_new: F, ynew: F) -> InterpolateResult<F> {
270 let mut values_at_x = Array1::zeros(self.y.len());
272
273 for (i, &_y_val) in self.y.iter().enumerate() {
274 let row = self.z.slice(scirs2_core::ndarray::s![i, ..]);
275 let spline = CubicSpline::new(&self.x.view(), &row)?;
276 values_at_x[i] = spline.evaluate(x_new)?;
277 }
278
279 let y_spline = CubicSpline::new(&self.y.view(), &values_at_x.view())?;
281 y_spline.evaluate(ynew)
282 }
283
284 fn evaluate_quintic(&self, x_new: F, ynew: F) -> InterpolateResult<F> {
294 let n_x = self.x.len();
295 let n_y = self.y.len();
296
297 if n_x < 3 || n_y < 3 {
298 return Err(InterpolateError::invalid_input(
299 "quintic interpolation requires at least 3 points in each dimension",
300 ));
301 }
302
303 let mut values_at_x = Array1::zeros(n_y);
305 for i in 0..n_y {
306 let row = self.z.slice(scirs2_core::ndarray::s![i, ..]);
307 let spline = QuinticSpline1D::new(&self.x.view(), &row)?;
308 values_at_x[i] = spline.evaluate(x_new);
309 }
310
311 let y_spline = QuinticSpline1D::new(&self.y.view(), &values_at_x.view())?;
313 Ok(y_spline.evaluate(ynew))
314 }
315}
316
317fn small_const<F: Float + FromPrimitive>(value: u32) -> F {
322 F::from_u32(value).unwrap_or_else(|| {
323 let mut acc = F::zero();
324 for _ in 0..value {
325 acc = acc + F::one();
326 }
327 acc
328 })
329}
330
331#[derive(Debug, Clone)]
334struct QuinticSegment<F> {
335 coeffs: [F; 6],
338}
339
340impl<F: Float> QuinticSegment<F> {
341 fn evaluate(&self, t: F) -> F {
344 let mut result = self.coeffs[5];
345 for k in (0..5).rev() {
346 result = result * t + self.coeffs[k];
347 }
348 result
349 }
350
351 #[allow(clippy::too_many_arguments)]
355 fn from_hermite_quintic(y0: F, y1: F, m0: F, m1: F, mm0: F, mm1: F, h: F) -> Self
356 where
357 F: FromPrimitive,
358 {
359 let two = small_const::<F>(2);
360 let three = small_const::<F>(3);
361 let six = small_const::<F>(6);
362 let seven = small_const::<F>(7);
363 let eight = small_const::<F>(8);
364 let twelve = small_const::<F>(12);
365 let fifteen = small_const::<F>(15);
366 let twenty = small_const::<F>(20);
367
368 let h2 = h * h;
369 let h3 = h2 * h;
370 let h4 = h2 * h2;
371 let h5 = h4 * h;
372
373 let a0 = y0;
374 let a1 = m0;
375 let a2 = mm0 / two;
376 let a3 = (-three * mm0 * h2 + mm1 * h2 - twelve * h * m0 - eight * h * m1 - twenty * y0
377 + twenty * y1)
378 / (two * h3);
379 let a4 =
380 (three / two * mm0 * h2 - mm1 * h2 + eight * h * m0 + seven * h * m1 + fifteen * y0
381 - fifteen * y1)
382 / h4;
383 let a5 = (-mm0 * h2 + mm1 * h2 - six * h * m0 - six * h * m1 - twelve * y0 + twelve * y1)
384 / (two * h5);
385
386 Self {
387 coeffs: [a0, a1, a2, a3, a4, a5],
388 }
389 }
390}
391
392struct QuinticSpline1D<F> {
412 x: Array1<F>,
413 segments: Vec<QuinticSegment<F>>,
414}
415
416impl<F> QuinticSpline1D<F>
417where
418 F: Float
419 + FromPrimitive
420 + Debug
421 + Display
422 + AddAssign
423 + SubAssign
424 + MulAssign
425 + DivAssign
426 + Clone
427 + 'static,
428{
429 fn new(x: &ArrayView1<F>, y: &ArrayView1<F>) -> InterpolateResult<Self> {
430 let n = x.len();
431 if n != y.len() {
432 return Err(InterpolateError::ShapeMismatch {
433 expected: format!("{n} elements"),
434 actual: format!("{} elements", y.len()),
435 object: "quintic spline y values".to_string(),
436 });
437 }
438 if n < 3 {
439 return Err(InterpolateError::invalid_input(
440 "quintic spline construction requires at least 3 points",
441 ));
442 }
443
444 let h: Vec<F> = (0..n - 1).map(|i| x[i + 1] - x[i]).collect();
445 for (i, &hi) in h.iter().enumerate() {
446 if hi <= F::zero() {
447 return Err(InterpolateError::invalid_input(format!(
448 "quintic spline requires strictly increasing x values \
449 (non-increasing step between indices {i} and {})",
450 i + 1
451 )));
452 }
453 }
454
455 let two = small_const::<F>(2);
456 let three = small_const::<F>(3);
457 let eight = small_const::<F>(8);
458 let twelve = small_const::<F>(12);
459 let fourteen = small_const::<F>(14);
460 let sixteen = small_const::<F>(16);
461 let twenty = small_const::<F>(20);
462 let thirty = small_const::<F>(30);
463
464 let dim = 2 * n;
467 let mut a = Array2::<F>::zeros((dim, dim));
468 let mut rhs = Array1::<F>::zeros(dim);
469 let idx_m = |i: usize| 2 * i;
470 let idx_mm = |i: usize| 2 * i + 1;
471
472 let mut row = 0usize;
473
474 {
477 let h0 = h[0];
478 let h0_2 = h0 * h0;
479
480 a[(row, idx_m(0))] = -twelve * h0;
481 a[(row, idx_m(1))] = -eight * h0;
482 a[(row, idx_mm(0))] = -three * h0_2;
483 a[(row, idx_mm(1))] = h0_2;
484 rhs[row] = twenty * (y[0] - y[1]);
485 row += 1;
486
487 a[(row, idx_m(0))] = sixteen * h0;
488 a[(row, idx_m(1))] = fourteen * h0;
489 a[(row, idx_mm(0))] = three * h0_2;
490 a[(row, idx_mm(1))] = -two * h0_2;
491 rhs[row] = -thirty * (y[0] - y[1]);
492 row += 1;
493 }
494
495 for i in 1..n - 1 {
499 let h_prev = h[i - 1];
500 let h_next = h[i];
501 let a_coef = F::one() / (h_prev * h_prev * h_prev);
502 let b_coef = F::one() / (h_next * h_next * h_next);
503 let c_coef = a_coef / h_prev;
504 let d_coef = b_coef / h_next;
505
506 a[(row, idx_m(i - 1))] += -eight * a_coef * h_prev;
508 a[(row, idx_m(i))] += -twelve * a_coef * h_prev + twelve * b_coef * h_next;
509 a[(row, idx_m(i + 1))] += eight * b_coef * h_next;
510 a[(row, idx_mm(i - 1))] += -a_coef * h_prev * h_prev;
511 a[(row, idx_mm(i))] +=
512 three * a_coef * h_prev * h_prev + three * b_coef * h_next * h_next;
513 a[(row, idx_mm(i + 1))] += -b_coef * h_next * h_next;
514 rhs[row] = twenty * a_coef * y[i - 1] - twenty * (a_coef + b_coef) * y[i]
515 + twenty * b_coef * y[i + 1];
516 row += 1;
517
518 a[(row, idx_m(i - 1))] += -fourteen * c_coef * h_prev;
520 a[(row, idx_m(i))] += -sixteen * c_coef * h_prev - sixteen * d_coef * h_next;
521 a[(row, idx_m(i + 1))] += -fourteen * d_coef * h_next;
522 a[(row, idx_mm(i - 1))] += -two * c_coef * h_prev * h_prev;
523 a[(row, idx_mm(i))] +=
524 three * c_coef * h_prev * h_prev - three * d_coef * h_next * h_next;
525 a[(row, idx_mm(i + 1))] += two * d_coef * h_next * h_next;
526 rhs[row] = thirty * c_coef * (y[i - 1] - y[i]) + thirty * d_coef * (y[i] - y[i + 1]);
527 row += 1;
528 }
529
530 {
533 let h_last = h[n - 2];
534 let h_last_2 = h_last * h_last;
535
536 a[(row, idx_m(n - 2))] = -eight * h_last;
537 a[(row, idx_m(n - 1))] = -twelve * h_last;
538 a[(row, idx_mm(n - 2))] = -h_last_2;
539 a[(row, idx_mm(n - 1))] = three * h_last_2;
540 rhs[row] = twenty * (y[n - 2] - y[n - 1]);
541 row += 1;
542
543 a[(row, idx_m(n - 2))] = -fourteen * h_last;
544 a[(row, idx_m(n - 1))] = -sixteen * h_last;
545 a[(row, idx_mm(n - 2))] = -two * h_last_2;
546 a[(row, idx_mm(n - 1))] = three * h_last_2;
547 rhs[row] = thirty * (y[n - 2] - y[n - 1]);
548 row += 1;
549 }
550
551 debug_assert_eq!(row, dim);
552
553 let solution = solve_with_stability_monitoring(&a.view(), &rhs.view()).map_err(|e| {
554 InterpolateError::NumericalInstability {
555 message: format!("failed to solve quintic spline derivative system: {e}"),
556 }
557 })?;
558
559 let mut segments = Vec::with_capacity(n - 1);
560 for i in 0..n - 1 {
561 segments.push(QuinticSegment::from_hermite_quintic(
562 y[i],
563 y[i + 1],
564 solution[idx_m(i)],
565 solution[idx_m(i + 1)],
566 solution[idx_mm(i)],
567 solution[idx_mm(i + 1)],
568 h[i],
569 ));
570 }
571
572 Ok(Self {
573 x: x.to_owned(),
574 segments,
575 })
576 }
577
578 fn evaluate(&self, x_new: F) -> F {
582 let n = self.x.len();
583 let idx = find_interval(&self.x.view(), x_new).min(n - 2);
584 let t = x_new - self.x[idx];
585 self.segments[idx].evaluate(t)
586 }
587}
588
589#[allow(dead_code)]
591fn is_sorted<F: PartialOrd>(arr: &ArrayView1<F>) -> bool {
592 for window in arr.windows(2) {
593 if window[0] > window[1] {
594 return false;
595 }
596 }
597 true
598}
599
600#[allow(dead_code)]
602fn find_interval<F: PartialOrd>(arr: &ArrayView1<F>, value: F) -> usize {
603 let slice: &[F] = arr.as_slice().expect("Operation failed");
605 match slice.binary_search_by(|x| x.partial_cmp(&value).expect("Operation failed")) {
606 Ok(idx) => idx,
607 Err(idx) => {
608 if idx == 0 {
609 0
610 } else if idx >= arr.len() {
611 arr.len() - 1
612 } else {
613 idx - 1
614 }
615 }
616 }
617}
618
619#[allow(dead_code)]
640pub fn interp2d<F>(
641 x: &ArrayView1<F>,
642 y: &ArrayView1<F>,
643 z: &ArrayView2<F>,
644 kind: Interp2dKind,
645) -> InterpolateResult<Interp2d<F>>
646where
647 F: Float + FromPrimitive + Debug + Clone + crate::traits::InterpolationFloat,
648{
649 Interp2d::new(x, y, z, kind)
650}
651
652#[cfg(test)]
653mod tests {
654 use super::*;
655 use approx::assert_abs_diff_eq;
656 use scirs2_core::ndarray::{array, Array2};
657
658 #[test]
659 fn test_linear_interpolation() -> InterpolateResult<()> {
660 let x = array![0.0, 1.0, 2.0];
662 let y = array![0.0, 1.0];
663 let z = Array2::from_shape_fn((2, 3), |(i, j)| y[i] + x[j]);
664
665 let interp = Interp2d::new(&x.view(), &y.view(), &z.view(), Interp2dKind::Linear)?;
666
667 assert_abs_diff_eq!(interp.evaluate(0.0, 0.0)?, 0.0, epsilon = 1e-10);
669 assert_abs_diff_eq!(interp.evaluate(1.0, 0.0)?, 1.0, epsilon = 1e-10);
670 assert_abs_diff_eq!(interp.evaluate(0.0, 1.0)?, 1.0, epsilon = 1e-10);
671 assert_abs_diff_eq!(interp.evaluate(2.0, 1.0)?, 3.0, epsilon = 1e-10);
672
673 assert_abs_diff_eq!(interp.evaluate(0.5, 0.5)?, 1.0, epsilon = 1e-10);
675 assert_abs_diff_eq!(interp.evaluate(1.5, 0.5)?, 2.0, epsilon = 1e-10);
676
677 Ok(())
678 }
679
680 #[test]
681 fn test_cubic_interpolation() -> InterpolateResult<()> {
682 let x = array![0.0, 1.0, 2.0, 3.0];
684 let y = array![0.0, 1.0, 2.0, 3.0];
685 let z = Array2::from_shape_fn((4, 4), |(i, j)| {
686 let x_val = x[j];
687 let y_val = y[i];
688 x_val * x_val + y_val * y_val });
690
691 let interp = Interp2d::new(&x.view(), &y.view(), &z.view(), Interp2dKind::Cubic)?;
692
693 assert_abs_diff_eq!(interp.evaluate(0.0, 0.0)?, 0.0, epsilon = 1e-10);
695 assert_abs_diff_eq!(interp.evaluate(1.0, 1.0)?, 2.0, epsilon = 1e-10);
696
697 let result = interp.evaluate(1.5, 1.5)?;
699 let expected = 1.5 * 1.5 + 1.5 * 1.5; assert!((result - expected).abs() < 0.5); Ok(())
703 }
704
705 #[test]
706 fn test_grid_evaluation() -> InterpolateResult<()> {
707 let x = array![0.0, 1.0];
708 let y = array![0.0, 1.0];
709 let z = Array2::from_shape_fn((2, 2), |(i, j)| y[i] + x[j]);
710
711 let interp = Interp2d::new(&x.view(), &y.view(), &z.view(), Interp2dKind::Linear)?;
712
713 let x_new = array![0.0, 0.5, 1.0];
714 let ynew = array![0.0, 0.5, 1.0];
715
716 let result = interp.evaluate_grid(&x_new.view(), &ynew.view())?;
717
718 assert_eq!(result.shape(), &[3, 3]);
719 assert_abs_diff_eq!(result[[0, 0]], 0.0, epsilon = 1e-10); assert_abs_diff_eq!(result[[1, 1]], 1.0, epsilon = 1e-10); assert_abs_diff_eq!(result[[2, 2]], 2.0, epsilon = 1e-10); Ok(())
724 }
725
726 #[test]
727 fn test_validation() {
728 let x = array![0.0, 1.0];
729 let y = array![0.0, 1.0];
730 let z = Array2::zeros((3, 2)); let result = Interp2d::new(&x.view(), &y.view(), &z.view(), Interp2dKind::Linear);
733 assert!(result.is_err());
734 }
735
736 #[test]
737 fn test_unsorted_coordinates() {
738 let x = array![1.0, 0.0]; let y = array![0.0, 1.0];
740 let z = Array2::zeros((2, 2));
741
742 let result = Interp2d::new(&x.view(), &y.view(), &z.view(), Interp2dKind::Linear);
743 assert!(result.is_err());
744 }
745
746 #[test]
747 fn test_quintic_spline_1d_exact_quadratic_reproduction() -> InterpolateResult<()> {
748 let x = array![0.0, 0.3, 0.9, 1.5, 2.2, 3.0, 3.7];
755 let poly = |v: f64| 3.0 - 2.0 * v + 0.5 * v * v;
756 let y = x.mapv(poly);
757
758 let spline = QuinticSpline1D::new(&x.view(), &y.view())?;
759
760 for &xq in &[0.05, 0.6, 1.1, 1.9, 2.6, 3.5] {
761 let got = spline.evaluate(xq);
762 let expected = poly(xq);
763 assert!(
764 (got - expected).abs() < 1e-9,
765 "quintic spline should exactly reproduce a quadratic: got {got}, \
766 expected {expected} at x={xq}"
767 );
768 }
769
770 Ok(())
771 }
772
773 #[test]
774 fn test_quintic_spline_1d_converges_faster_than_cubic_order() -> InterpolateResult<()> {
775 fn error_at(n: usize) -> InterpolateResult<f64> {
782 let x = Array1::linspace(0.0, 2.0 * std::f64::consts::PI, n);
783 let y = x.mapv(|v: f64| v.sin());
784 let spline = QuinticSpline1D::new(&x.view(), &y.view())?;
785 let h = x[1] - x[0];
786 let xq = std::f64::consts::PI + 0.31 * h; Ok((spline.evaluate(xq) - xq.sin()).abs())
788 }
789
790 let e11 = error_at(11)?;
791 let e21 = error_at(21)?;
792 let e41 = error_at(41)?;
793
794 assert!(e11 > 0.0 && e21 > 0.0 && e41 > 0.0);
795 assert!(
796 e11 / e21 > 50.0,
797 "expected quintic-order convergence (>50x per doubling), got {}x \
798 (e11={e11}, e21={e21})",
799 e11 / e21
800 );
801 assert!(
802 e21 / e41 > 50.0,
803 "expected quintic-order convergence (>50x per doubling), got {}x \
804 (e21={e21}, e41={e41})",
805 e21 / e41
806 );
807
808 Ok(())
809 }
810
811 #[test]
812 fn test_quintic_spline_1d_rejects_degenerate_input() {
813 let x = array![0.0, 1.0];
814 let y = array![0.0, 1.0];
815 assert!(QuinticSpline1D::new(&x.view(), &y.view()).is_err());
817
818 let x2 = array![0.0, 1.0, 1.0];
819 let y2 = array![0.0, 1.0, 2.0];
820 assert!(QuinticSpline1D::new(&x2.view(), &y2.view()).is_err());
822 }
823
824 #[test]
825 fn test_interp2d_quintic_reproduces_separable_quadratic_exactly() -> InterpolateResult<()> {
826 let x = array![0.0, 0.4, 1.1, 1.8, 2.6, 3.5];
833 let y = array![0.0, 0.5, 1.3, 2.1, 2.9];
834 let z = Array2::from_shape_fn((y.len(), x.len()), |(i, j)| x[j] * x[j] + y[i] * y[i]);
835
836 let interp = Interp2d::new(&x.view(), &y.view(), &z.view(), Interp2dKind::Quintic)?;
837
838 for &(xq, yq) in &[(0.2, 0.3), (1.5, 1.0), (3.2, 2.5), (0.05, 2.85)] {
839 let got = interp.evaluate(xq, yq)?;
840 let expected = xq * xq + yq * yq;
841 assert!(
842 (got - expected).abs() < 1e-8,
843 "quintic Interp2d should exactly reproduce x^2+y^2: got {got}, \
844 expected {expected} at ({xq}, {yq})"
845 );
846 }
847
848 Ok(())
849 }
850
851 #[test]
852 fn test_interp2d_quintic_is_not_a_silent_cubic_fallback() -> InterpolateResult<()> {
853 let n = 9;
860 let x = Array1::linspace(0.0, 3.0, n);
861 let y = Array1::linspace(0.0, 2.0, n);
862 let z = Array2::from_shape_fn((n, n), |(i, j)| x[j].sin() + y[i].cos());
863
864 let quintic = Interp2d::new(&x.view(), &y.view(), &z.view(), Interp2dKind::Quintic)?;
865 let cubic = Interp2d::new(&x.view(), &y.view(), &z.view(), Interp2dKind::Cubic)?;
866
867 let (xq, yq) = (1.35, 0.83);
868 let quintic_val = quintic.evaluate(xq, yq)?;
869 let cubic_val = cubic.evaluate(xq, yq)?;
870
871 assert!(
872 (quintic_val - cubic_val).abs() > 1e-8,
873 "Quintic ({quintic_val}) must not silently match Cubic ({cubic_val})"
874 );
875
876 Ok(())
877 }
878}