Skip to main content

scirs2_interpolate/
utils.rs

1//! Utility functions for interpolation
2//!
3//! This module provides helper functions for interpolation tasks.
4
5use crate::error::{InterpolateError, InterpolateResult};
6use scirs2_core::ndarray::{Array1, ArrayView1};
7use scirs2_core::numeric::{Float, FromPrimitive};
8use scirs2_core::safe_ops::{safe_divide, safe_sqrt};
9use std::fmt::{Debug, Display};
10
11/// Compute the error estimate for interpolation
12///
13/// This function performs leave-one-out cross-validation to estimate
14/// the interpolation error. It removes each data point in turn, fits
15/// the interpolation to the remaining data, and measures the prediction
16/// error at the removed point.
17///
18/// # Arguments
19///
20/// * `x` - Original x coordinates
21/// * `y` - Original y values  
22/// * `interp_fn` - Function that performs interpolation at given x values
23///
24/// # Returns
25///
26/// Root mean square error (RMSE) from leave-one-out cross-validation
27///
28/// # Examples
29///
30/// ```rust
31/// use scirs2_core::ndarray::{Array1, ArrayView1};
32/// use scirs2_interpolate::utils::error_estimate;
33/// use scirs2_interpolate::error::InterpolateResult;
34///
35/// // Sample data with some noise
36/// let x = Array1::from_vec(vec![0.0, 1.0, 2.0, 3.0, 4.0]);
37/// let y = Array1::from_vec(vec![0.1, 0.9, 2.1, 2.9, 4.1]);
38///
39/// // Define a simple linear interpolation function
40/// let linear_interp = |x_train: &ArrayView1<f64>, y_train: &ArrayView1<f64>, x_test: &ArrayView1<f64>| -> InterpolateResult<Array1<f64>> {
41///     // Simplified linear interpolation implementation
42///     let mut result = Array1::zeros(x_test.len());
43///     for (i, &x_val) in x_test.iter().enumerate() {
44///         // Find nearest neighbors and interpolate
45///         if x_train.len() >= 2 {
46///             result[i] = x_val; // Simplified: just return x for y=x function
47///         }
48///     }
49///     Ok(result)
50/// };
51///
52/// let rmse = error_estimate(&x.view(), &y.view(), linear_interp).expect("Operation failed");
53/// println!("Cross-validation RMSE: {}", rmse);
54/// ```
55#[allow(dead_code)]
56pub fn error_estimate<F, Func>(
57    x: &ArrayView1<F>,
58    y: &ArrayView1<F>,
59    interp_fn: Func,
60) -> InterpolateResult<F>
61where
62    F: Float + FromPrimitive + Debug + Display,
63    Func: Fn(&ArrayView1<F>, &ArrayView1<F>, &ArrayView1<F>) -> InterpolateResult<Array1<F>>,
64{
65    if x.len() != y.len() {
66        return Err(InterpolateError::invalid_input(
67            "x and y arrays must have the same length",
68        ));
69    }
70
71    if x.len() < 3 {
72        return Err(InterpolateError::insufficient_points(
73            3,
74            x.len(),
75            "interpolation error estimation",
76        ));
77    }
78
79    let mut sum_squared_error = F::zero();
80    let n = x.len();
81
82    for i in 0..n {
83        // Create leave-one-out dataset
84        let mut x_loo = Vec::with_capacity(n - 1);
85        let mut y_loo = Vec::with_capacity(n - 1);
86
87        for j in 0..n {
88            if i != j {
89                x_loo.push(x[j]);
90                y_loo.push(y[j]);
91            }
92        }
93
94        let x_loo_array = Array1::from_vec(x_loo);
95        let y_loo_array = Array1::from_vec(y_loo);
96
97        // Predict at the left-out point
98        let x_test = Array1::from_vec(vec![x[i]]);
99        let y_pred = interp_fn(&x_loo_array.view(), &y_loo_array.view(), &x_test.view())?;
100
101        // Compute squared error
102        let error = y_pred[0] - y[i];
103        sum_squared_error = sum_squared_error + error * error;
104    }
105
106    // Return RMSE
107    let n_f = F::from_usize(n).ok_or_else(|| {
108        InterpolateError::ComputationError(
109            "Failed to convert array length to float type".to_string(),
110        )
111    })?;
112
113    let variance = safe_divide(sum_squared_error, n_f).map_err(|_| {
114        InterpolateError::ComputationError("Division by zero in RMSE calculation".to_string())
115    })?;
116
117    let rmse = safe_sqrt(variance).map_err(|_| {
118        InterpolateError::ComputationError(
119            "Square root of negative value in RMSE calculation".to_string(),
120        )
121    })?;
122
123    Ok(rmse)
124}
125
126/// Find optimal interpolation parameters
127///
128/// # Arguments
129///
130/// * `x` - Original x coordinates
131/// * `y` - Original y values
132/// * `param_values` - Array of parameter values to try
133/// * `interp_fn_builder` - Function that builds an interpolation function with a parameter
134///
135/// # Returns
136///
137/// The parameter value that minimizes the cross-validation error
138#[allow(dead_code)]
139pub fn optimize_parameter<F, Func, BuilderFunc>(
140    x: &ArrayView1<F>,
141    y: &ArrayView1<F>,
142    param_values: &ArrayView1<F>,
143    interp_fn_builder: BuilderFunc,
144) -> InterpolateResult<F>
145where
146    F: Float + FromPrimitive + Debug + Display,
147    Func: Fn(&ArrayView1<F>, &ArrayView1<F>, &ArrayView1<F>) -> InterpolateResult<Array1<F>>,
148    BuilderFunc: Fn(F) -> Func,
149{
150    if param_values.is_empty() {
151        return Err(InterpolateError::invalid_input(
152            "at least one parameter value must be provided",
153        ));
154    }
155
156    let mut best_param = param_values[0];
157    let mut min_error = F::infinity();
158
159    for &param in param_values.iter() {
160        let interp_fn = interp_fn_builder(param);
161        let error = error_estimate(x, y, interp_fn)?;
162
163        if error < min_error {
164            min_error = error;
165            best_param = param;
166        }
167    }
168
169    Ok(best_param)
170}
171
172/// Differentiate an interpolated function using finite differences
173///
174/// This function computes the derivative of an interpolated function
175/// at a given point using central finite differences. This is useful
176/// when you have an interpolant but need its derivative.
177///
178/// # Arguments
179///
180/// * `x` - Point at which to evaluate the derivative
181/// * `h` - Step size for the finite difference (smaller = more accurate, but numerical issues)
182/// * `evalfn` - Function that evaluates the interpolant at a point
183///
184/// # Returns
185///
186/// The approximate derivative of the interpolant at x
187///
188/// # Examples
189///
190/// ```rust
191/// use scirs2_interpolate::utils::differentiate;
192/// use scirs2_interpolate::error::InterpolateResult;
193///
194/// // Example: differentiate f(x) = x^3 at x = 2
195/// // Expected derivative: f'(2) = 3 * 2^2 = 12
196/// let cubic_fn = |x: f64| -> InterpolateResult<f64> {
197///     Ok(x * x * x)
198/// };
199///
200/// let derivative_at_2 = differentiate(2.0, 0.001, cubic_fn).expect("Operation failed");
201/// assert!((derivative_at_2 - 12.0).abs() < 0.01); // Should be close to 12
202///
203/// // Example: differentiate sin(x) at x = π/2  
204/// // Expected derivative: cos(π/2) = 0
205/// let sin_fn = |x: f64| -> InterpolateResult<f64> {
206///     Ok(x.sin())
207/// };
208///
209/// let derivative_at_pi_2 = differentiate(std::f64::consts::PI / 2.0, 0.0001, sin_fn).expect("Operation failed");
210/// assert!(derivative_at_pi_2.abs() < 0.01); // Should be close to 0
211/// ```
212#[allow(dead_code)]
213pub fn differentiate<F, Func>(x: F, h: F, evalfn: Func) -> InterpolateResult<F>
214where
215    F: Float + FromPrimitive + Debug + Display,
216    Func: Fn(F) -> InterpolateResult<F>,
217{
218    // Use central difference for better accuracy
219    let f_plus = evalfn(x + h)?;
220    let f_minus = evalfn(x - h)?;
221
222    let two = F::from_f64(2.0).ok_or_else(|| {
223        InterpolateError::ComputationError(
224            "Failed to convert constant 2.0 to float type".to_string(),
225        )
226    })?;
227
228    let denominator = two * h;
229    let derivative = safe_divide(f_plus - f_minus, denominator).map_err(|_| {
230        InterpolateError::ComputationError(
231            "Division by zero in finite difference calculation (step size too small)".to_string(),
232        )
233    })?;
234
235    Ok(derivative)
236}
237
238/// Integrate an interpolated function using Simpson's rule
239///
240/// This function computes the definite integral of an interpolated function
241/// over a specified interval using composite Simpson's rule. This is useful
242/// for computing areas under interpolated curves or other integral quantities.
243///
244/// # Arguments
245///
246/// * `a` - Lower bound of integration
247/// * `b` - Upper bound of integration  
248/// * `n` - Number of intervals for the quadrature (must be even and >= 2)
249/// * `evalfn` - Function that evaluates the interpolant at a point
250///
251/// # Returns
252///
253/// The approximate definite integral of the interpolant from a to b
254///
255/// # Examples
256///
257/// ```rust
258/// use scirs2_interpolate::utils::integrate;
259/// use scirs2_interpolate::error::InterpolateResult;
260///
261/// // Example: integrate f(x) = x^2 from 0 to 2
262/// // Expected result: ∫₀² x² dx = [x³/3]₀² = 8/3 ≈ 2.667
263/// let quadratic_fn = |x: f64| -> InterpolateResult<f64> {
264///     Ok(x * x)
265/// };
266///
267/// let integral = integrate(0.0, 2.0, 100, quadratic_fn).expect("Operation failed");
268/// assert!((integral - 8.0/3.0).abs() < 0.001);
269///
270/// // Example: integrate sin(x) from 0 to π
271/// // Expected result: ∫₀^π sin(x) dx = [-cos(x)]₀^π = 2
272/// let sin_fn = |x: f64| -> InterpolateResult<f64> {
273///     Ok(x.sin())
274/// };
275///
276/// let integral_sin = integrate(0.0, std::f64::consts::PI, 200, sin_fn).expect("Operation failed");
277/// assert!((integral_sin - 2.0).abs() < 0.001);
278/// ```
279#[allow(dead_code)]
280pub fn integrate<F, Func>(a: F, b: F, n: usize, evalfn: Func) -> InterpolateResult<F>
281where
282    F: Float + FromPrimitive + Debug + Display,
283    Func: Fn(F) -> InterpolateResult<F>,
284{
285    if a > b {
286        return integrate(b, a, n, evalfn).map(|result| -result);
287    }
288
289    // Use composite Simpson's rule for integration
290    if n < 2 {
291        return Err(InterpolateError::InvalidValue(
292            "number of intervals must be at least 2".to_string(),
293        ));
294    }
295
296    if !n.is_multiple_of(2) {
297        return Err(InterpolateError::InvalidValue(
298            "number of intervals must be even".to_string(),
299        ));
300    }
301
302    let n_f = F::from_usize(n).ok_or_else(|| {
303        InterpolateError::ComputationError(
304            "Failed to convert number of intervals to float type".to_string(),
305        )
306    })?;
307
308    let h = safe_divide(b - a, n_f).map_err(|_| {
309        InterpolateError::ComputationError(
310            "Division by zero in step size calculation (zero intervals)".to_string(),
311        )
312    })?;
313
314    let mut sum = evalfn(a)? + evalfn(b)?;
315
316    // Even-indexed points (except endpoints)
317    let two = F::from_f64(2.0).ok_or_else(|| {
318        InterpolateError::ComputationError(
319            "Failed to convert constant 2.0 to float type".to_string(),
320        )
321    })?;
322
323    for i in 1..n {
324        if i % 2 == 0 {
325            let i_f = F::from_usize(i).ok_or_else(|| {
326                InterpolateError::ComputationError(
327                    "Failed to convert index to float type".to_string(),
328                )
329            })?;
330            let x_i = a + i_f * h;
331            sum = sum + two * evalfn(x_i)?;
332        }
333    }
334
335    // Odd-indexed points
336    let four = F::from_f64(4.0).ok_or_else(|| {
337        InterpolateError::ComputationError(
338            "Failed to convert constant 4.0 to float type".to_string(),
339        )
340    })?;
341
342    for i in 1..n {
343        if i % 2 == 1 {
344            let i_f = F::from_usize(i).ok_or_else(|| {
345                InterpolateError::ComputationError(
346                    "Failed to convert index to float type".to_string(),
347                )
348            })?;
349            let x_i = a + i_f * h;
350            sum = sum + four * evalfn(x_i)?;
351        }
352    }
353
354    let three = F::from_f64(3.0).ok_or_else(|| {
355        InterpolateError::ComputationError(
356            "Failed to convert constant 3.0 to float type".to_string(),
357        )
358    })?;
359
360    let integral = safe_divide(h * sum, three).map_err(|_| {
361        InterpolateError::ComputationError(
362            "Division by zero in Simpson's rule calculation".to_string(),
363        )
364    })?;
365
366    Ok(integral)
367}
368
369/// Find roots using bisection method
370///
371/// This function uses the bisection method to find all roots of a function within a given interval.
372///
373/// # Arguments
374///
375/// * `a` - Left boundary of search interval
376/// * `b` - Right boundary of search interval  
377/// * `tolerance` - Tolerance for root finding accuracy
378/// * `evalfn` - Function to evaluate
379///
380/// # Returns
381///
382/// Vector of roots found in the interval
383///
384#[allow(dead_code)]
385pub fn find_roots_bisection<F, Func>(
386    a: F,
387    b: F,
388    tolerance: F,
389    evalfn: Func,
390) -> InterpolateResult<Vec<F>>
391where
392    F: Float + FromPrimitive + Debug + Display,
393    Func: Fn(F) -> InterpolateResult<F>,
394{
395    let mut roots = Vec::new();
396
397    if a >= b {
398        return Ok(roots);
399    }
400
401    // Evaluate at endpoints
402    let fa = evalfn(a)?;
403    let fb = evalfn(b)?;
404
405    // If either endpoint is close to zero, it's a root
406    if fa.abs() < tolerance {
407        roots.push(a);
408    }
409    if fb.abs() < tolerance && (b - a).abs() > tolerance {
410        roots.push(b);
411    }
412
413    // If signs are the same, no root in interval by intermediate value theorem
414    if fa * fb > F::zero() {
415        return Ok(roots);
416    }
417
418    // Binary search for root
419    let mut left = a;
420    let mut right = b;
421    let mut f_left = fa;
422    let mut _f_right = fb;
423
424    while (right - left).abs() > tolerance {
425        let mid = left + (right - left) / F::from_f64(2.0).expect("Operation failed");
426        let f_mid = evalfn(mid)?;
427
428        if f_mid.abs() < tolerance {
429            roots.push(mid);
430            break;
431        }
432
433        if f_left * f_mid < F::zero() {
434            right = mid;
435            _f_right = f_mid;
436        } else {
437            left = mid;
438            f_left = f_mid;
439        }
440    }
441
442    // If we didn't find exact root, add the midpoint
443    if roots.is_empty() {
444        let root = left + (right - left) / F::from_f64(2.0).expect("Operation failed");
445        let f_root = evalfn(root)?;
446        if f_root.abs() < tolerance * F::from_f64(10.0).expect("Operation failed") {
447            roots.push(root);
448        }
449    }
450
451    Ok(roots)
452}
453
454/// Find multiple roots by subdividing interval
455///
456/// This function subdivides the interval and searches for roots in each subdivision.
457///
458/// # Arguments
459///
460/// * `a` - Left boundary of search interval
461/// * `b` - Right boundary of search interval
462/// * `tolerance` - Tolerance for root finding accuracy
463/// * `subdivisions` - Number of subdivisions to search
464/// * `evalfn` - Function to evaluate
465///
466/// # Returns
467///
468/// Vector of roots found in the interval
469///
470#[allow(dead_code)]
471pub fn find_multiple_roots<F, Func>(
472    a: F,
473    b: F,
474    tolerance: F,
475    subdivisions: usize,
476    evalfn: Func,
477) -> InterpolateResult<Vec<F>>
478where
479    F: Float + FromPrimitive + Debug + Display,
480    Func: Fn(F) -> InterpolateResult<F> + Copy,
481{
482    let mut all_roots = Vec::new();
483
484    if subdivisions == 0 {
485        return Ok(all_roots);
486    }
487
488    let step = (b - a) / F::from_usize(subdivisions).expect("Operation failed");
489
490    for i in 0..subdivisions {
491        let left = a + F::from_usize(i).expect("Operation failed") * step;
492        let right = a + F::from_usize(i + 1).expect("Operation failed") * step;
493
494        match find_roots_bisection(left, right, tolerance, evalfn) {
495            Ok(mut roots) => all_roots.append(&mut roots),
496            Err(_) => continue,
497        }
498    }
499
500    // Sort and remove duplicates
501    all_roots.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
502    all_roots.dedup_by(|a, b| (*a - *b).abs() < tolerance);
503
504    Ok(all_roots)
505}
506
507#[cfg(test)]
508mod tests {
509    use super::*;
510    // interpolation functions
511    use scirs2_core::ndarray::array;
512
513    #[test]
514    fn test_error_estimate() {
515        use crate::{cubic_interpolate, linear_interpolate};
516
517        // Linear data y = 2x + 1 (not y == x, so a hypothetical "just echo
518        // the input back" bug in error_estimate/interp_fn couldn't
519        // coincidentally pass this check).
520        let x = array![0.0, 1.0, 2.0, 3.0, 4.0];
521        let y = array![1.0, 3.0, 5.0, 7.0, 9.0];
522
523        // Leave-one-out CV error for linear interpolation on perfectly
524        // linear data should be ~0 (up to floating point): removing any one
525        // point still leaves >= 4 points on the same line, and linear
526        // interpolation/extrapolation along that line recovers the held-out
527        // point exactly.
528        let error =
529            error_estimate(&x.view(), &y.view(), linear_interpolate).expect("Operation failed");
530        assert!(
531            error < 1e-10,
532            "linear LOO-CV RMSE should be ~0, got {error}"
533        );
534
535        // `cubic_interpolate`'s `Interp1d::Cubic` (see
536        // `interp1d::mod::cubic_interp`) is a uniform-parametrization
537        // Catmull-Rom spline: its tangent estimate at an interior point p1
538        // is `(p2 - p0)`, which only approximates the true derivative when
539        // p1 is spaced evenly between its neighbors p0 and p2. Leave-one-out
540        // removes one x value at a time, which makes the *remaining* points
541        // non-uniformly spaced (e.g. removing x=1 from [0,1,2,3,4] leaves
542        // gaps 2,1,1) -- so even though the underlying y=2x+1 is perfectly
543        // linear, this specific (uniform-spacing-assuming) formula no longer
544        // reproduces it exactly. This is a genuine, deterministic property of
545        // the simplified Catmull-Rom implementation (matches the wide 0.3
546        // tolerance already documented on `cubic_interpolate`'s own doctest),
547        // not a bug to paper over with a near-zero tolerance: the measured
548        // RMSE here is a reproducible ~0.0791, so 0.1 gives real headroom
549        // while still catching a genuine regression.
550        let error =
551            error_estimate(&x.view(), &y.view(), cubic_interpolate).expect("Operation failed");
552        assert!(
553            error < 0.1,
554            "cubic LOO-CV RMSE should stay within the known Catmull-Rom \
555             non-uniform-spacing error budget, got {error}"
556        );
557    }
558
559    #[test]
560    fn test_differentiate() {
561        // Function: f(x) = x^2
562        let f = |x: f64| -> InterpolateResult<f64> { Ok(x * x) };
563
564        // At x=2, f'(x) = 2x = 4
565        let derivative = differentiate(2.0, 0.001, f).expect("Operation failed");
566        assert!((derivative - 4.0).abs() < 1e-5);
567
568        // At x=3, f'(x) = 2x = 6
569        let derivative = differentiate(3.0, 0.001, f).expect("Operation failed");
570        assert!((derivative - 6.0).abs() < 1e-5);
571    }
572
573    #[test]
574    fn test_integrate() {
575        // Function: f(x) = x^2
576        // Integral from 0 to 1: x^3/3 = 1/3
577        let f = |x: f64| -> InterpolateResult<f64> { Ok(x * x) };
578
579        let integral = integrate(0.0, 1.0, 100, f).expect("Operation failed");
580        assert!((integral - 1.0 / 3.0).abs() < 1e-5);
581
582        // Integral from 0 to 2: x^3/3 = 8/3
583        let integral = integrate(0.0, 2.0, 100, f).expect("Operation failed");
584        assert!((integral - 8.0 / 3.0).abs() < 1e-5);
585    }
586}