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 ¶m 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}