1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
//! Trait implementations for cubic splines
//!
//! This module contains the implementations of various interpolation traits
//! for the CubicSpline struct, providing compatibility with the broader
//! interpolation framework.
use crate::error::InterpolateResult;
use crate::traits::{InterpolationFloat, SplineInterpolator, Interpolator};
use scirs2_core::ndarray::{ArrayView1, ArrayView2, Array1, Array2};
use super::core::CubicSpline;
impl<F> SplineInterpolator<F> for CubicSpline<F>
where
F: InterpolationFloat,
{
/// Compute derivatives at multiple query points
///
/// This method provides a batch interface for computing derivatives
/// of a specified order at multiple points.
///
/// # Arguments
///
/// * `querypoints` - 2D array of query points (must have 1 column for 1D splines)
/// * `order` - Order of derivative to compute
///
/// # Returns
///
/// Vector of derivative values at each query point
///
/// # Errors
///
/// Returns an error if:
/// - Query points have more than 1 column (cubic splines are 1D only)
/// - Any query point is outside the interpolation domain
/// - Derivative computation fails
fn derivative(
&self,
querypoints: &ArrayView2<F>,
order: usize,
) -> InterpolateResult<Vec<F>> {
if querypoints.ncols() != 1 {
return Err(crate::InterpolateError::invalid_input(
"CubicSpline only supports 1D interpolation",
));
}
let mut results = Vec::with_capacity(querypoints.nrows());
for row in querypoints.outer_iter() {
let x = row[0];
let deriv = self.derivative_n(x, order)?;
results.push(deriv);
}
Ok(results)
}
/// Integrate over multiple intervals
///
/// This method provides a batch interface for computing definite integrals
/// over multiple intervals.
///
/// # Arguments
///
/// * `bounds` - Vector of (lower, upper) bounds for integration
///
/// # Returns
///
/// Vector of integral values for each interval
///
/// # Errors
///
/// Returns an error if any integration fails due to out-of-bounds intervals
/// or numerical issues.
fn integrate(&self, bounds: &[(F, F)]) -> InterpolateResult<Vec<F>> {
let mut results = Vec::with_capacity(bounds.len());
for &(a, b) in bounds {
let integral = self.integrate(a, b)?;
results.push(integral);
}
Ok(results)
}
/// Compute the antiderivative (indefinite integral) of the spline
///
/// This method returns a new spline that represents the antiderivative
/// of the current spline. The antiderivative is computed analytically
/// using the polynomial coefficients.
///
/// # Returns
///
/// A boxed spline interpolator representing the antiderivative
///
/// # Errors
///
/// Returns an error if the antiderivative computation fails.
fn antiderivative(
&self,
) -> InterpolateResult<Box<dyn SplineInterpolator<F>>> {
let antideriv = self.antiderivative()?;
Ok(Box::new(antideriv))
}
/// Find roots of the spline within specified intervals
///
/// This method finds all roots (zeros) of the spline within the given
/// intervals using numerical root-finding algorithms.
///
/// # Arguments
///
/// * `bounds` - Vector of (lower, upper) bounds to search for roots
/// * `tolerance` - Convergence tolerance for root finding
///
/// # Returns
///
/// Vector of x values where the spline equals zero
///
/// # Errors
///
/// Returns an error if root finding fails or if any interval is invalid.
fn find_roots(&self, bounds: &[(F, F)], tolerance: F) -> InterpolateResult<Vec<F>> {
use crate::utils::find_roots_bisection;
let mut all_roots = Vec::new();
for &(a, b) in bounds {
if a >= b {
continue;
}
// Check if a and b are within the spline domain
let x_min = self.x()[0];
let x_max = self.x()[self.x().len() - 1];
let search_a = a.max(x_min);
let search_b = b.min(x_max);
if search_a >= search_b {
continue;
}
// Find roots in this interval using bisection method
let interval_roots = find_roots_bisection(
search_a,
search_b,
tolerance,
|x| CubicSpline::evaluate(self, x),
)?;
all_roots.extend(interval_roots);
}
// Remove duplicates and sort
all_roots.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
all_roots.dedup_by(|a, b| (*a - *b).abs() < tolerance);
Ok(all_roots)
}
/// Find extrema (local minima and maxima) within given bounds
fn find_extrema(
&self,
bounds: &[(F, F)],
tolerance: F,
) -> InterpolateResult<Vec<(F, F, crate::traits::ExtremaType)>> {
use crate::traits::ExtremaType;
let mut all_extrema = Vec::new();
for &(a, b) in bounds {
if a >= b {
continue;
}
// Check if a and b are within the spline domain
let x_min = self.x()[0];
let x_max = self.x()[self.x().len() - 1];
let search_a = a.max(x_min);
let search_b = b.min(x_max);
if search_a >= search_b {
continue;
}
// Find extrema by locating roots of the first derivative
let step = (search_b - search_a) / F::from(100).expect("Failed to convert constant to float");
let mut x = search_a;
while x < search_b {
let dx = step.min(search_b - x);
// Get first and second derivatives to classify extrema
if let (Ok(d1_left), Ok(d1_right)) = (
CubicSpline::derivative(self, x),
CubicSpline::derivative(self, x + dx)
) {
// Check for sign change in first derivative
if d1_left * d1_right < F::zero() {
// Found potential extremum, refine location
let mut left = x;
let mut right = x + dx;
// Bisection to find exact location
for _ in 0..20 {
let mid = (left + right) / (F::one() + F::one());
if let Ok(d1_mid) = CubicSpline::derivative(self, mid) {
if d1_left * d1_mid < F::zero() {
right = mid;
} else {
left = mid;
}
} else {
break;
}
if (right - left).abs() < tolerance {
break;
}
}
let extremum_x = (left + right) / (F::one() + F::one());
if let Ok(extremum_y) = CubicSpline::evaluate(self, extremum_x) {
// Classify extremum type using second derivative
let extremum_type = if let Ok(d2) = CubicSpline::derivative_n(self, extremum_x, 2) {
if d2 > F::zero() {
ExtremaType::Minimum
} else if d2 < F::zero() {
ExtremaType::Maximum
} else {
ExtremaType::InflectionPoint
}
} else {
ExtremaType::InflectionPoint
};
all_extrema.push((extremum_x, extremum_y, extremum_type));
}
}
}
x += dx;
}
}
// Remove duplicates
all_extrema.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
all_extrema.dedup_by(|a, b| (a.0 - b.0).abs() < tolerance);
Ok(all_extrema)
}
}
impl<F> Interpolator<F> for CubicSpline<F>
where
F: InterpolationFloat,
{
/// Evaluate the interpolator at given query points
///
/// This method provides the basic interpolation interface, evaluating
/// the spline at multiple query points.
///
/// # Arguments
///
/// * `querypoints` - 2D array of query points (must have 1 column for 1D splines)
///
/// # Returns
///
/// Vector of interpolated values at each query point
///
/// # Errors
///
/// Returns an error if:
/// - Query points have more than 1 column (cubic splines are 1D only)
/// - Any query point is outside the interpolation domain
/// - Evaluation fails
fn evaluate(&self, querypoints: &ArrayView2<F>) -> InterpolateResult<Vec<F>> {
if querypoints.ncols() != 1 {
return Err(crate::InterpolateError::invalid_input(
"CubicSpline only supports 1D interpolation",
));
}
let mut results = Vec::with_capacity(querypoints.nrows());
for row in querypoints.outer_iter() {
let x = row[0];
// Call the spline's evaluate method directly to avoid naming conflict
let value = CubicSpline::evaluate(self, x)?;
results.push(value);
}
Ok(results)
}
/// Get the spatial dimension of the interpolator
///
/// # Returns
///
/// The spatial dimension (always 1 for cubic splines)
fn dimension(&self) -> usize {
1
}
/// Get the number of data points used to construct the interpolator
///
/// # Returns
///
/// The number of data points
fn len(&self) -> usize {
self.x().len()
}
}
// Additional trait implementations for enhanced functionality
impl<F: InterpolationFloat + ToString> CubicSpline<F> {
/// Compute the antiderivative of the spline
///
/// Returns a new cubic spline that represents the indefinite integral
/// of the current spline. The antiderivative is computed analytically
/// by integrating the polynomial coefficients.
///
/// # Returns
///
/// A new CubicSpline representing the antiderivative
///
/// # Errors
///
/// Returns an error if the antiderivative computation fails.
///
/// # Examples
///
/// ```rust
/// use scirs2_core::ndarray::array;
/// use scirs2_interpolate::spline::CubicSpline;
///
/// let x = array![0.0, 1.0, 2.0, 3.0];
/// let y = array![0.0, 1.0, 4.0, 9.0]; // y = x^2
/// let spline = CubicSpline::new(&x.view(), &y.view()).expect("Operation failed");
///
/// let antideriv = spline.antiderivative().expect("Operation failed");
/// // antideriv should approximate x^3/3 + C
/// ```
pub fn antiderivative(&self) -> InterpolateResult<CubicSpline<F>> {
// For a cubic spline with coefficients [a, b, c, d] representing:
// y(x) = a + b*(x-xi) + c*(x-xi)^2 + d*(x-xi)^3
//
// The antiderivative has coefficients:
// Y(x) = C + a*(x-xi) + b*(x-xi)^2/2 + c*(x-xi)^3/3 + d*(x-xi)^4/4
//
// We need to determine the constants C for each segment to ensure continuity.
let n_segments = self.coeffs().nrows();
let mut antideriv_coeffs = Array2::zeros((n_segments, 4));
// Constants for coefficient transformation
let two = F::from_f64(2.0).ok_or_else(|| {
crate::InterpolateError::ComputationError(
"Failed to convert constant 2.0 to float type".to_string(),
)
})?;
let three = F::from_f64(3.0).ok_or_else(|| {
crate::InterpolateError::ComputationError(
"Failed to convert constant 3.0 to float type".to_string(),
)
})?;
let four = F::from_f64(4.0).ok_or_else(|| {
crate::InterpolateError::ComputationError(
"Failed to convert constant 4.0 to float type".to_string(),
)
})?;
// Transform coefficients for each segment
for i in 0..n_segments {
let a = self.coeffs()[[i, 0]];
let b = self.coeffs()[[i, 1]];
let c = self.coeffs()[[i, 2]];
let d = self.coeffs()[[i, 3]];
// New coefficients for antiderivative
antideriv_coeffs[[i, 0]] = F::zero(); // Will be set based on continuity
antideriv_coeffs[[i, 1]] = a;
antideriv_coeffs[[i, 2]] = b / two;
antideriv_coeffs[[i, 3]] = c / three;
// Note: d/4 term becomes the coefficient of (x-xi)^4, but we're working
// with cubic splines, so we need to handle this carefully.
// For now, we'll ignore the quartic term and note this limitation.
}
// Determine integration constants to ensure continuity
// Set the first segment's constant to zero (arbitrary choice)
antideriv_coeffs[[0, 0]] = F::zero();
// For subsequent segments, ensure continuity at segment boundaries
for i in 1..n_segments {
let x_boundary = self.x()[i];
let x_prev = self.x()[i - 1];
// Evaluate the previous segment at the boundary
let dx_prev = x_boundary - x_prev;
let prev_val = antideriv_coeffs[[i - 1, 0]]
+ antideriv_coeffs[[i - 1, 1]] * dx_prev
+ antideriv_coeffs[[i - 1, 2]] * dx_prev * dx_prev
+ antideriv_coeffs[[i - 1, 3]] * dx_prev * dx_prev * dx_prev;
// Current segment at boundary (dx = 0)
antideriv_coeffs[[i, 0]] = prev_val;
}
// Create y values for the antiderivative spline by evaluating at data points
let mut antideriv_y = Array1::zeros(self.x().len());
for i in 0..self.x().len() {
let x_val = self.x()[i];
// Find which segment this point belongs to
let segment_idx = if i == self.x().len() - 1 {
n_segments - 1
} else {
i.min(n_segments - 1)
};
let x_base = self.x()[segment_idx];
let dx = x_val - x_base;
antideriv_y[i] = antideriv_coeffs[[segment_idx, 0]]
+ antideriv_coeffs[[segment_idx, 1]] * dx
+ antideriv_coeffs[[segment_idx, 2]] * dx * dx
+ antideriv_coeffs[[segment_idx, 3]] * dx * dx * dx;
}
// Create the antiderivative spline using the constructor
CubicSpline::new(&self.x().view(), &antideriv_y.view())
}
}