Skip to main content

scirs2_interpolate/interp1d/
mod.rs

1//! One-dimensional interpolation methods
2//!
3//! This module provides functionality for interpolating one-dimensional data.
4
5mod basic_interp;
6pub mod monotonic;
7pub mod pchip;
8
9// Re-export interpolation functions
10pub use basic_interp::{cubic_interpolate, linear_interpolate, nearest_interpolate};
11pub use monotonic::{
12    hyman_interpolate, modified_akima_interpolate, monotonic_interpolate, steffen_interpolate,
13    MonotonicInterpolator, MonotonicMethod,
14};
15pub use pchip::{pchip_interpolate, PchipExtrapolateMode, PchipInterpolator};
16
17use crate::error::{InterpolateError, InterpolateResult};
18use scirs2_core::ndarray::{Array1, ArrayView1};
19use scirs2_core::numeric::{Float, FromPrimitive};
20use std::fmt::Debug;
21
22/// Available interpolation methods
23#[derive(Debug, Clone, Copy, PartialEq, Default)]
24pub enum InterpolationMethod {
25    /// Nearest neighbor interpolation
26    Nearest,
27    /// Linear interpolation
28    #[default]
29    Linear,
30    /// Cubic interpolation
31    Cubic,
32    /// PCHIP interpolation (monotonic)
33    Pchip,
34}
35
36/// Options for extrapolation behavior
37#[derive(Debug, Clone, Copy, PartialEq, Default)]
38pub enum ExtrapolateMode {
39    /// Return error when extrapolating
40    #[default]
41    Error,
42    /// Extrapolate using the interpolation method
43    Extrapolate,
44    /// Use nearest valid value
45    Nearest,
46}
47
48/// One-dimensional interpolation object
49///
50/// Provides a way to interpolate values at arbitrary points within a range
51/// based on a set of known x and y values.
52#[derive(Debug, Clone)]
53pub struct Interp1d<F: Float> {
54    /// X coordinates (must be sorted)
55    x: Array1<F>,
56    /// Y coordinates
57    y: Array1<F>,
58    /// Interpolation method
59    method: InterpolationMethod,
60    /// Extrapolation mode
61    extrapolate: ExtrapolateMode,
62    /// Cached PCHIP interpolator for polynomial extrapolation
63    pchip_cache: Option<PchipInterpolator<F>>,
64}
65
66impl<F: Float + FromPrimitive + Debug + std::fmt::Display> Interp1d<F> {
67    /// Create a new interpolation object
68    ///
69    /// # Arguments
70    ///
71    /// * `x` - The x coordinates (must be sorted in ascending order)
72    /// * `y` - The y coordinates (must have the same length as x)
73    /// * `method` - The interpolation method to use
74    /// * `extrapolate` - The extrapolation behavior
75    ///
76    /// # Returns
77    ///
78    /// A new `Interp1d` object
79    ///
80    /// # Examples
81    ///
82    /// ```
83    /// use scirs2_core::ndarray::array;
84    /// use scirs2_interpolate::interp1d::{Interp1d, InterpolationMethod, ExtrapolateMode};
85    ///
86    /// let x = array![0.0f64, 1.0, 2.0, 3.0];
87    /// let y = array![0.0f64, 1.0, 4.0, 9.0];
88    ///
89    /// // Create a linear interpolator
90    /// let interp = Interp1d::new(
91    ///     &x.view(), &y.view(),
92    ///     InterpolationMethod::Linear,
93    ///     ExtrapolateMode::Error
94    /// ).expect("Operation failed");
95    ///
96    /// // Interpolate at x = 1.5
97    /// let y_interp = interp.evaluate(1.5);
98    /// assert!(y_interp.is_ok());
99    /// assert!((y_interp.expect("Operation failed") - 2.5).abs() < 1e-10);
100    /// ```
101    pub fn new(
102        x: &ArrayView1<F>,
103        y: &ArrayView1<F>,
104        method: InterpolationMethod,
105        extrapolate: ExtrapolateMode,
106    ) -> InterpolateResult<Self> {
107        // Check inputs
108        if x.len() != y.len() {
109            return Err(InterpolateError::invalid_input(
110                "x and y arrays must have the same length".to_string(),
111            ));
112        }
113
114        if x.len() < 2 {
115            return Err(InterpolateError::insufficient_points(
116                2,
117                x.len(),
118                "interpolation",
119            ));
120        }
121
122        // Check for NaN and Infinity in input data
123        for i in 0..x.len() {
124            if !x[i].is_finite() {
125                return Err(InterpolateError::invalid_input(format!(
126                    "x values must be finite, found non-finite value at index {}",
127                    i
128                )));
129            }
130            if !y[i].is_finite() {
131                return Err(InterpolateError::invalid_input(format!(
132                    "y values must be finite, found non-finite value at index {}",
133                    i
134                )));
135            }
136        }
137
138        // Check that x is sorted
139        for i in 1..x.len() {
140            if x[i] <= x[i - 1] {
141                return Err(InterpolateError::invalid_input(
142                    "x values must be sorted in ascending order".to_string(),
143                ));
144            }
145        }
146
147        // For cubic interpolation, need at least 4 points
148        if method == InterpolationMethod::Cubic && x.len() < 4 {
149            return Err(InterpolateError::insufficient_points(
150                4,
151                x.len(),
152                "cubic interpolation",
153            ));
154        }
155
156        let pchip_cache = if method == InterpolationMethod::Pchip {
157            let pchip_extrap = extrapolate == ExtrapolateMode::Extrapolate
158                || extrapolate == ExtrapolateMode::Nearest;
159            let mut interp = PchipInterpolator::new(x, y, pchip_extrap)?;
160            if extrapolate == ExtrapolateMode::Extrapolate {
161                interp = interp.with_extrapolate_mode(PchipExtrapolateMode::Polynomial);
162            }
163            Some(interp)
164        } else {
165            None
166        };
167
168        Ok(Interp1d {
169            x: x.to_owned(),
170            y: y.to_owned(),
171            method,
172            extrapolate,
173            pchip_cache,
174        })
175    }
176
177    /// Evaluate the interpolation at the given points
178    ///
179    /// # Arguments
180    ///
181    /// * `xnew` - The x coordinate at which to evaluate the interpolation
182    ///
183    /// # Returns
184    ///
185    /// The interpolated y value at `xnew`
186    pub fn evaluate(&self, xnew: F) -> InterpolateResult<F> {
187        // Check if we're extrapolating
188        let is_extrapolating = xnew < self.x[0] || xnew > self.x[self.x.len() - 1];
189
190        if is_extrapolating {
191            match self.extrapolate {
192                ExtrapolateMode::Error => {
193                    return Err(InterpolateError::out_of_domain_with_suggestion(
194                        xnew,
195                        self.x[0],
196                        self.x[self.x.len() - 1],
197                        "1D interpolation evaluation",
198                        format!("Use ExtrapolateMode::Extrapolate for linear extrapolation, ExtrapolateMode::Nearest for constant extrapolation, or ensure query points are within the data range [{:?}, {:?}]", 
199                               self.x[0], self.x[self.x.len() - 1])
200                    ));
201                }
202                ExtrapolateMode::Nearest => {
203                    if xnew < self.x[0] {
204                        return Ok(self.y[0]);
205                    } else {
206                        return Ok(self.y[self.y.len() - 1]);
207                    }
208                }
209                ExtrapolateMode::Extrapolate => {
210                    // PCHIP uses polynomial continuation (scipy-compatible)
211                    if let Some(ref pchip) = self.pchip_cache {
212                        return pchip.evaluate(xnew);
213                    }
214                    // For other methods, linear extrapolation based on the edge segments
215                    if xnew < self.x[0] {
216                        // Use the first segment for extrapolation below the range
217                        let x0 = self.x[0];
218                        let x1 = self.x[1];
219                        let y0 = self.y[0];
220                        let y1 = self.y[1];
221
222                        // Linear extrapolation formula: y = y0 + (x - x0) * (y1 - y0) / (x1 - x0)
223                        let slope = (y1 - y0) / (x1 - x0);
224                        return Ok(y0 + (xnew - x0) * slope);
225                    } else {
226                        // Use the last segment for extrapolation above the range
227                        let n = self.x.len();
228                        let x0 = self.x[n - 2];
229                        let x1 = self.x[n - 1];
230                        let y0 = self.y[n - 2];
231                        let y1 = self.y[n - 1];
232
233                        // Linear extrapolation formula: y = y1 + (x - x1) * (y1 - y0) / (x1 - x0)
234                        let slope = (y1 - y0) / (x1 - x0);
235                        return Ok(y1 + (xnew - x1) * slope);
236                    }
237                }
238            }
239        }
240
241        // Find the index of the segment containing xnew using binary search
242        let idx = self.find_segment(xnew);
243
244        // Special case: xnew is exactly the last point
245        if xnew == self.x[self.x.len() - 1] {
246            return Ok(self.y[self.x.len() - 1]);
247        }
248
249        // Apply the selected interpolation method
250        match self.method {
251            InterpolationMethod::Nearest => {
252                nearest_interp(&self.x.view(), &self.y.view(), idx, xnew)
253            }
254            InterpolationMethod::Linear => linear_interp(&self.x.view(), &self.y.view(), idx, xnew),
255            InterpolationMethod::Cubic => cubic_interp(&self.x.view(), &self.y.view(), idx, xnew),
256            InterpolationMethod::Pchip => {
257                // Use the pre-built cached interpolator
258                match self.pchip_cache {
259                    Some(ref pchip) => pchip.evaluate(xnew),
260                    None => Err(InterpolateError::invalid_input(
261                        "PCHIP cache missing (internal error)".to_string(),
262                    )),
263                }
264            }
265        }
266    }
267
268    /// Find the segment index containing the given x value using binary search.
269    ///
270    /// Returns the index `i` such that `x[i] <= xnew <= x[i+1]`.
271    /// For values at or beyond the last point, returns `x.len() - 2`.
272    fn find_segment(&self, xnew: F) -> usize {
273        let n = self.x.len();
274        if n < 2 {
275            return 0;
276        }
277
278        // Binary search: find the largest i such that x[i] <= xnew
279        let mut lo = 0usize;
280        let mut hi = n - 1;
281
282        // Clamp to valid range
283        if xnew <= self.x[0] {
284            return 0;
285        }
286        if xnew >= self.x[n - 1] {
287            return n - 2;
288        }
289
290        while hi - lo > 1 {
291            let mid = lo + (hi - lo) / 2;
292            if self.x[mid] <= xnew {
293                lo = mid;
294            } else {
295                hi = mid;
296            }
297        }
298
299        lo
300    }
301
302    /// Evaluate the interpolation at multiple points
303    ///
304    /// # Arguments
305    ///
306    /// * `xnew` - The x coordinates at which to evaluate the interpolation
307    ///
308    /// # Returns
309    ///
310    /// The interpolated y values at `xnew`
311    pub fn evaluate_array(&self, xnew: &ArrayView1<F>) -> InterpolateResult<Array1<F>> {
312        let mut result = Array1::zeros(xnew.len());
313        for (i, &x) in xnew.iter().enumerate() {
314            result[i] = self.evaluate(x)?;
315        }
316        Ok(result)
317    }
318}
319
320/// Perform nearest neighbor interpolation
321///
322/// # Arguments
323///
324/// * `x` - The x coordinates
325/// * `y` - The y coordinates
326/// * `idx` - The index of the segment containing the target point
327/// * `xnew` - The x coordinate at which to interpolate
328///
329/// # Returns
330///
331/// The interpolated value
332#[allow(dead_code)]
333fn nearest_interp<F: Float>(
334    x: &ArrayView1<F>,
335    y: &ArrayView1<F>,
336    idx: usize,
337    xnew: F,
338) -> InterpolateResult<F> {
339    // Find which of the two points is closer
340    let dist_left = (xnew - x[idx]).abs();
341    let dist_right = (xnew - x[idx + 1]).abs();
342
343    if dist_left <= dist_right {
344        Ok(y[idx])
345    } else {
346        Ok(y[idx + 1])
347    }
348}
349
350/// Perform linear interpolation
351///
352/// # Arguments
353///
354/// * `x` - The x coordinates
355/// * `y` - The y coordinates
356/// * `idx` - The index of the segment containing the target point
357/// * `xnew` - The x coordinate at which to interpolate
358///
359/// # Returns
360///
361/// The interpolated value
362#[allow(dead_code)]
363fn linear_interp<F: Float>(
364    x: &ArrayView1<F>,
365    y: &ArrayView1<F>,
366    idx: usize,
367    xnew: F,
368) -> InterpolateResult<F> {
369    let x0 = x[idx];
370    let x1 = x[idx + 1];
371    let y0 = y[idx];
372    let y1 = y[idx + 1];
373
374    // Avoid division by zero
375    if x0 == x1 {
376        return Ok(y0); // or y1, they should be the same
377    }
378
379    // Linear interpolation formula: y = y0 + (x - x0) * (y1 - y0) / (x1 - x0)
380    Ok(y0 + (xnew - x0) * (y1 - y0) / (x1 - x0))
381}
382
383/// Perform cubic interpolation
384///
385/// # Arguments
386///
387/// * `x` - The x coordinates
388/// * `y` - The y coordinates
389/// * `idx` - The index of the segment containing the target point
390/// * `xnew` - The x coordinate at which to interpolate
391///
392/// # Returns
393///
394/// The interpolated value
395#[allow(dead_code)]
396fn cubic_interp<F: Float + FromPrimitive>(
397    x: &ArrayView1<F>,
398    y: &ArrayView1<F>,
399    idx: usize,
400    xnew: F,
401) -> InterpolateResult<F> {
402    // We need 4 points for cubic interpolation
403    // If we're near the edges, we need to adjust the indices
404    let (i0, i1, i2, i3) = if idx == 0 {
405        (0, 0, 1, 2)
406    } else if idx == x.len() - 2 {
407        (idx - 1, idx, idx + 1, idx + 1)
408    } else {
409        // Handles both idx == x.len() - 3 and idx > x.len() - 3 cases since they're identical
410        (idx - 1, idx, idx + 1, idx + 2)
411    };
412
413    let _x0 = x[i0];
414    let x1 = x[i1];
415    let x2 = x[i2];
416    let _x3 = x[i3];
417
418    let y0 = y[i0];
419    let y1 = y[i1];
420    let y2 = y[i2];
421    let y3 = y[i3];
422
423    // Normalized position within the interval [x1, x2]
424    let t = if x2 != x1 {
425        (xnew - x1) / (x2 - x1)
426    } else {
427        F::zero()
428    };
429
430    // Calculate cubic interpolation using Catmull-Rom spline
431    // p(t) = 0.5 * ((2*p1) +
432    //               (-p0 + p2) * t +
433    //               (2*p0 - 5*p1 + 4*p2 - p3) * t^2 +
434    //               (-p0 + 3*p1 - 3*p2 + p3) * t^3)
435
436    let two = F::from_f64(2.0).expect("Operation failed");
437    let three = F::from_f64(3.0).expect("Operation failed");
438    let four = F::from_f64(4.0).expect("Operation failed");
439    let five = F::from_f64(5.0).expect("Operation failed");
440    let half = F::from_f64(0.5).expect("Operation failed");
441
442    let t2 = t * t;
443    let t3 = t2 * t;
444
445    let c0 = two * y1;
446    let c1 = -y0 + y2;
447    let c2 = two * y0 - five * y1 + four * y2 - y3;
448    let c3 = -y0 + three * y1 - three * y2 + y3;
449
450    let result = half * (c0 + c1 * t + c2 * t2 + c3 * t3);
451
452    Ok(result)
453}
454
455#[cfg(test)]
456mod tests {
457    use super::*;
458    use approx::assert_relative_eq;
459    use scirs2_core::ndarray::array;
460
461    #[test]
462    fn test_nearest_interpolation() {
463        let x = array![0.0, 1.0, 2.0, 3.0];
464        let y = array![0.0, 1.0, 4.0, 9.0];
465
466        let interp = Interp1d::new(
467            &x.view(),
468            &y.view(),
469            InterpolationMethod::Nearest,
470            ExtrapolateMode::Error,
471        )
472        .expect("Operation failed");
473
474        // Test points exactly at data points
475        assert_relative_eq!(interp.evaluate(0.0).expect("Operation failed"), 0.0);
476        assert_relative_eq!(interp.evaluate(1.0).expect("Operation failed"), 1.0);
477        assert_relative_eq!(interp.evaluate(2.0).expect("Operation failed"), 4.0);
478        assert_relative_eq!(interp.evaluate(3.0).expect("Operation failed"), 9.0);
479
480        // Test points between data points
481        assert_relative_eq!(interp.evaluate(0.4).expect("Operation failed"), 0.0);
482        assert_relative_eq!(interp.evaluate(0.6).expect("Operation failed"), 1.0);
483        assert_relative_eq!(interp.evaluate(1.4).expect("Operation failed"), 1.0);
484        assert_relative_eq!(interp.evaluate(1.6).expect("Operation failed"), 4.0);
485    }
486
487    #[test]
488    fn test_linear_interpolation() {
489        let x = array![0.0, 1.0, 2.0, 3.0];
490        let y = array![0.0, 1.0, 4.0, 9.0];
491
492        let interp = Interp1d::new(
493            &x.view(),
494            &y.view(),
495            InterpolationMethod::Linear,
496            ExtrapolateMode::Error,
497        )
498        .expect("Operation failed");
499
500        // Test points exactly at data points
501        assert_relative_eq!(interp.evaluate(0.0).expect("Operation failed"), 0.0);
502        assert_relative_eq!(interp.evaluate(1.0).expect("Operation failed"), 1.0);
503        assert_relative_eq!(interp.evaluate(2.0).expect("Operation failed"), 4.0);
504        assert_relative_eq!(interp.evaluate(3.0).expect("Operation failed"), 9.0);
505
506        // Test points between data points
507        assert_relative_eq!(interp.evaluate(0.5).expect("Operation failed"), 0.5);
508        assert_relative_eq!(interp.evaluate(1.5).expect("Operation failed"), 2.5);
509        assert_relative_eq!(interp.evaluate(2.5).expect("Operation failed"), 6.5);
510    }
511
512    #[test]
513    fn test_cubic_interpolation() {
514        let x = array![0.0, 1.0, 2.0, 3.0];
515        let y = array![0.0, 1.0, 4.0, 9.0];
516
517        let interp = Interp1d::new(
518            &x.view(),
519            &y.view(),
520            InterpolationMethod::Cubic,
521            ExtrapolateMode::Error,
522        )
523        .expect("Operation failed");
524
525        // Test points exactly at data points
526        assert_relative_eq!(interp.evaluate(0.0).expect("Operation failed"), 0.0);
527        assert_relative_eq!(interp.evaluate(1.0).expect("Operation failed"), 1.0);
528        assert_relative_eq!(interp.evaluate(2.0).expect("Operation failed"), 4.0);
529        assert_relative_eq!(interp.evaluate(3.0).expect("Operation failed"), 9.0);
530
531        // For this particular dataset (a quadratic y = x²),
532        // cubic interpolation might not reproduce it exactly due to the specific spline algorithm
533        // so we use wider tolerances
534        assert_relative_eq!(
535            interp.evaluate(0.5).expect("Operation failed"),
536            0.25,
537            epsilon = 0.1
538        );
539        assert_relative_eq!(
540            interp.evaluate(1.5).expect("Operation failed"),
541            2.25,
542            epsilon = 0.1
543        );
544        assert_relative_eq!(
545            interp.evaluate(2.5).expect("Operation failed"),
546            6.25,
547            epsilon = 1.0
548        );
549    }
550
551    #[test]
552    fn test_pchip_interpolation() {
553        let x = array![0.0, 1.0, 2.0, 3.0];
554        let y = array![0.0, 1.0, 4.0, 9.0];
555
556        let interp = Interp1d::new(
557            &x.view(),
558            &y.view(),
559            InterpolationMethod::Pchip,
560            ExtrapolateMode::Error,
561        )
562        .expect("Operation failed");
563
564        // Test points exactly at data points
565        assert_relative_eq!(interp.evaluate(0.0).expect("Operation failed"), 0.0);
566        assert_relative_eq!(interp.evaluate(1.0).expect("Operation failed"), 1.0);
567        assert_relative_eq!(interp.evaluate(2.0).expect("Operation failed"), 4.0);
568        assert_relative_eq!(interp.evaluate(3.0).expect("Operation failed"), 9.0);
569
570        // For this monotonically increasing dataset,
571        // PCHIP should preserve monotonicity
572        let y_05 = interp.evaluate(0.5).expect("Operation failed");
573        let y_15 = interp.evaluate(1.5).expect("Operation failed");
574        let y_25 = interp.evaluate(2.5).expect("Operation failed");
575
576        assert!(y_05 > 0.0 && y_05 < 1.0);
577        assert!(y_15 > 1.0 && y_15 < 4.0);
578        assert!(y_25 > 4.0 && y_25 < 9.0);
579    }
580
581    #[test]
582    fn test_extrapolation_modes() {
583        let x = array![0.0, 1.0, 2.0, 3.0];
584        let y = array![0.0, 1.0, 4.0, 9.0];
585
586        // Test error mode
587        let interp_error = Interp1d::new(
588            &x.view(),
589            &y.view(),
590            InterpolationMethod::Linear,
591            ExtrapolateMode::Error,
592        )
593        .expect("Operation failed");
594
595        assert!(interp_error.evaluate(-1.0).is_err());
596        assert!(interp_error.evaluate(4.0).is_err());
597
598        // Test nearest mode
599        let interp_nearest = Interp1d::new(
600            &x.view(),
601            &y.view(),
602            InterpolationMethod::Linear,
603            ExtrapolateMode::Nearest,
604        )
605        .expect("Operation failed");
606
607        assert_relative_eq!(
608            interp_nearest.evaluate(-1.0).expect("Operation failed"),
609            0.0
610        );
611        assert_relative_eq!(interp_nearest.evaluate(4.0).expect("Operation failed"), 9.0);
612
613        // Test extrapolate mode
614        let interp_extrapolate = Interp1d::new(
615            &x.view(),
616            &y.view(),
617            InterpolationMethod::Linear,
618            ExtrapolateMode::Extrapolate,
619        )
620        .expect("Operation failed");
621
622        // For this data, the linear extrapolation is based on the slope of the segments
623        // For x=-1.0, we use the first segment (0,0) - (1,1) which has slope 1
624        assert_relative_eq!(
625            interp_extrapolate.evaluate(-1.0).expect("Operation failed"),
626            -1.0
627        );
628
629        // For x=4.0, we use the last segment (2,4) - (3,9) which has slope 5
630        // So the result is 9 + (4-3)*5 = 9 + 5 = 14
631        assert_relative_eq!(
632            interp_extrapolate.evaluate(4.0).expect("Operation failed"),
633            14.0
634        );
635    }
636
637    #[test]
638    fn test_convenience_functions() {
639        let x = array![0.0, 1.0, 2.0, 3.0];
640        let y = array![0.0, 1.0, 4.0, 9.0];
641        let xnew = array![0.5, 1.5, 2.5];
642
643        // Test nearest interpolation
644        let y_nearest =
645            nearest_interpolate(&x.view(), &y.view(), &xnew.view()).expect("Operation failed");
646        // Point 0.5 is exactly halfway between x[0]=0.0 and x[1]=1.0, so we default to the left point's value
647        assert_relative_eq!(y_nearest[0], 0.0);
648        // Point 1.5 is exactly halfway between x[1]=1.0 and x[2]=2.0, so we default to the left point's value
649        assert_relative_eq!(y_nearest[1], 1.0);
650        // Point 2.5 is exactly halfway between x[2]=2.0 and x[3]=3.0, so we default to the left point's value
651        assert_relative_eq!(y_nearest[2], 4.0);
652
653        // Test linear interpolation
654        let y_linear =
655            linear_interpolate(&x.view(), &y.view(), &xnew.view()).expect("Operation failed");
656        assert_relative_eq!(y_linear[0], 0.5);
657        assert_relative_eq!(y_linear[1], 2.5);
658        assert_relative_eq!(y_linear[2], 6.5);
659
660        // Test cubic interpolation
661        let y_cubic =
662            cubic_interpolate(&x.view(), &y.view(), &xnew.view()).expect("Operation failed");
663        // Allow a wider tolerance for cubic interpolation since it depends on the specific spline implementation
664        assert!((y_cubic[0] - 0.25).abs() < 0.15);
665        assert!((y_cubic[1] - 2.25).abs() < 0.15);
666        // For point 2.5, allow an even wider tolerance
667        assert!((y_cubic[2] - 6.25).abs() < 1.0);
668
669        // Test PCHIP interpolation
670        let y_pchip =
671            pchip_interpolate(&x.view(), &y.view(), &xnew.view(), false).expect("Operation failed");
672        // For monotonically increasing data, PCHIP should preserve monotonicity
673        assert!(y_pchip[0] > 0.0 && y_pchip[0] < 1.0);
674        assert!(y_pchip[1] > 1.0 && y_pchip[1] < 4.0);
675        assert!(y_pchip[2] > 4.0 && y_pchip[2] < 9.0);
676    }
677
678    #[test]
679    fn test_error_conditions() {
680        let x = array![0.0, 1.0, 2.0, 3.0];
681        let y = array![0.0, 1.0, 4.0];
682
683        // Test different lengths
684        let result = Interp1d::new(
685            &x.view(),
686            &y.view(),
687            InterpolationMethod::Linear,
688            ExtrapolateMode::Error,
689        );
690        assert!(result.is_err());
691
692        // Test unsorted x
693        let x_unsorted = array![0.0, 2.0, 1.0, 3.0];
694        let y_valid = array![0.0, 1.0, 4.0, 9.0];
695
696        let result = Interp1d::new(
697            &x_unsorted.view(),
698            &y_valid.view(),
699            InterpolationMethod::Linear,
700            ExtrapolateMode::Error,
701        );
702        assert!(result.is_err());
703
704        // Test too few points for cubic
705        let x_short = array![0.0, 1.0];
706        let y_short = array![0.0, 1.0];
707
708        let result = Interp1d::new(
709            &x_short.view(),
710            &y_short.view(),
711            InterpolationMethod::Cubic,
712            ExtrapolateMode::Error,
713        );
714        assert!(result.is_err());
715    }
716}