use crate::bspline::BSpline;
use crate::error::{InterpolateError, InterpolateResult};
use crate::spline_calculus::PiecewisePolynomial;
use crate::traits::InterpolationFloat;
use scirs2_core::ndarray::{Array1, Array2};
use scirs2_core::numeric::Zero;
use std::fmt::{Debug, Display};
use std::ops::{AddAssign, DivAssign, MulAssign, RemAssign, SubAssign};
pub trait Differentiable {
type Derivative;
fn derivative(&self, order: usize) -> InterpolateResult<Self::Derivative>;
}
impl<T> Differentiable for BSpline<T>
where
T: InterpolationFloat
+ Zero
+ AddAssign
+ SubAssign
+ MulAssign
+ DivAssign
+ RemAssign
+ Debug
+ Display
+ std::ops::Add<Output = T>
+ std::ops::Sub<Output = T>
+ std::ops::Mul<Output = T>
+ std::ops::Div<Output = T>,
{
type Derivative = BSpline<T>;
fn derivative(&self, order: usize) -> InterpolateResult<BSpline<T>> {
if order == 0 {
return Ok(self.clone());
}
let mut current = self.clone();
for _ in 0..order {
current = bspline_derivative_once(¤t)?;
if current.degree() == 0 {
break;
}
}
Ok(current)
}
}
fn bspline_derivative_once<T>(spline: &BSpline<T>) -> InterpolateResult<BSpline<T>>
where
T: InterpolationFloat
+ Zero
+ AddAssign
+ SubAssign
+ MulAssign
+ DivAssign
+ RemAssign
+ Debug
+ Display
+ std::ops::Add<Output = T>
+ std::ops::Sub<Output = T>
+ std::ops::Mul<Output = T>
+ std::ops::Div<Output = T>,
{
let k = spline.degree();
let t = spline.knot_vector();
let c = spline.coefficients();
let n = c.len();
if k == 0 {
if t.len() < 3 {
return Err(InterpolateError::invalid_input(
"cannot differentiate a degree-0 B-spline with fewer than 3 knots".to_string(),
));
}
let new_t = t.slice(scirs2_core::ndarray::s![1..t.len() - 1]).to_owned();
let new_c = Array1::<T>::zeros(new_t.len() - 1);
return BSpline::new(&new_t.view(), &new_c.view(), 0, spline.extrapolate_mode());
}
if n < 2 {
return Err(InterpolateError::invalid_input(
"B-spline must have at least 2 coefficients to differentiate".to_string(),
));
}
let new_k = k - 1;
let new_n = n - 1;
let k_f = T::from_usize(k).ok_or_else(|| {
InterpolateError::ComputationError("failed to convert B-spline degree to float".to_string())
})?;
let mut new_c = Array1::<T>::zeros(new_n);
for i in 0..new_n {
let denom = t[i + k + 1] - t[i + 1];
if denom.abs() < T::epsilon() {
new_c[i] = T::zero();
} else {
new_c[i] = k_f * (c[i + 1] - c[i]) / denom;
}
}
let new_t = t.slice(scirs2_core::ndarray::s![1..t.len() - 1]).to_owned();
if new_t.len() != new_n + new_k + 1 {
return Err(InterpolateError::ComputationError(format!(
"B-spline derivative knot count mismatch: expected {}, got {}",
new_n + new_k + 1,
new_t.len()
)));
}
BSpline::new(
&new_t.view(),
&new_c.view(),
new_k,
spline.extrapolate_mode(),
)
}
pub fn differentiate_piecewise_cubic<F: InterpolationFloat>(
breakpoints: &Array1<F>,
coeffs: &Array2<F>,
order: usize,
) -> InterpolateResult<PiecewisePolynomial<F>> {
if order == 0 {
return Ok(PiecewisePolynomial::new(
breakpoints.clone(),
coeffs.clone(),
)?);
}
let n_seg = coeffs.nrows();
if n_seg == 0 {
return Err(InterpolateError::invalid_input(
"cannot differentiate empty piecewise polynomial".to_string(),
));
}
if coeffs.ncols() != 4 {
return Err(InterpolateError::invalid_input(
"differentiate_piecewise_cubic expects coefficients of shape (n_seg, 4)".to_string(),
));
}
let remaining_cols = if order >= 4 { 0usize } else { 4 - order };
if remaining_cols == 0 {
let zero_coeffs = Array2::<F>::zeros((n_seg, 1));
return Ok(PiecewisePolynomial::new(breakpoints.clone(), zero_coeffs)?);
}
let mut work: Array2<F> = Array2::zeros((n_seg, 4));
for i in 0..n_seg {
for j in 0..4 {
work[[i, j]] = coeffs[[i, j]];
}
}
let mut current_cols = 4usize;
for _ in 0..order {
if current_cols <= 1 {
let zero_coeffs = Array2::<F>::zeros((n_seg, 1));
return Ok(PiecewisePolynomial::new(breakpoints.clone(), zero_coeffs)?);
}
let new_cols = current_cols - 1;
let mut new_work: Array2<F> = Array2::zeros((n_seg, new_cols));
for i in 0..n_seg {
for j in 0..new_cols {
let factor = F::from_usize(j + 1).ok_or_else(|| {
InterpolateError::ComputationError(
"Failed to convert derivative factor to float".to_string(),
)
})?;
new_work[[i, j]] = work[[i, j + 1]] * factor;
}
}
work = new_work;
current_cols = new_cols;
}
let mut out: Array2<F> = Array2::zeros((n_seg, current_cols));
for i in 0..n_seg {
for j in 0..current_cols {
out[[i, j]] = work[[i, j]];
}
}
Ok(PiecewisePolynomial::new(breakpoints.clone(), out)?)
}
use crate::spline::CubicSpline;
impl<F: InterpolationFloat> Differentiable for CubicSpline<F> {
type Derivative = PiecewisePolynomial<F>;
fn derivative(&self, order: usize) -> InterpolateResult<PiecewisePolynomial<F>> {
if order == 0 {
return Ok(PiecewisePolynomial::new(
self.x().clone(),
self.coeffs().clone(),
)?);
}
differentiate_piecewise_cubic(self.x(), self.coeffs(), order)
}
}
use crate::advanced::akima::AkimaSpline;
impl<F: InterpolationFloat> Differentiable for AkimaSpline<F> {
type Derivative = PiecewisePolynomial<F>;
fn derivative(&self, order: usize) -> InterpolateResult<PiecewisePolynomial<F>> {
if order == 0 {
return Ok(PiecewisePolynomial::new(
self.breakpoints_owned(),
self.segment_coeffs_owned(),
)?);
}
differentiate_piecewise_cubic(
&self.breakpoints_owned(),
&self.segment_coeffs_owned(),
order,
)
}
}
use crate::interp1d::pchip::PchipInterpolator;
use scirs2_core::numeric::FromPrimitive;
impl<F: InterpolationFloat + FromPrimitive> Differentiable for PchipInterpolator<F> {
type Derivative = PiecewisePolynomial<F>;
fn derivative(&self, order: usize) -> InterpolateResult<PiecewisePolynomial<F>> {
let (breakpoints, cubic_coeffs) = self.to_power_basis_coeffs()?;
if order == 0 {
return Ok(PiecewisePolynomial::new(breakpoints, cubic_coeffs)?);
}
differentiate_piecewise_cubic(&breakpoints, &cubic_coeffs, order)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bspline::BSpline;
use crate::bspline_modules::ExtrapolateMode;
use crate::interp1d::pchip::PchipInterpolator;
use crate::spline::CubicSpline;
use scirs2_core::ndarray::array;
#[test]
fn bspline_derivative_degree_decrease_by_one() {
let t = array![0.0f64, 0.0, 0.0, 0.0, 1.0, 2.0, 3.0, 3.0, 3.0, 3.0];
let c = array![0.0f64, 1.0, 2.0, 1.0, 0.0, 1.0];
let spline = BSpline::new(&t.view(), &c.view(), 3, ExtrapolateMode::Extrapolate)
.expect("Construction failed");
let d = Differentiable::derivative(&spline, 1usize).expect("Derivative failed");
assert_eq!(d.degree(), 2, "degree should decrease by 1");
assert_eq!(d.coefficients().len(), c.len() - 1);
}
#[test]
fn second_derivative_of_constant_spline_is_zero() {
let x = array![0.0f64, 1.0, 2.0, 3.0];
let y = array![5.0f64, 5.0, 5.0, 5.0];
let spline = CubicSpline::new(&x.view(), &y.view()).expect("Construction failed");
let d2 = Differentiable::derivative(&spline, 2usize).expect("2nd derivative failed");
for &v in d2.coeffs().iter() {
assert!(v.abs() < 1e-10, "Expected zero, got {}", v);
}
}
#[test]
fn cubic_spline_derivative_matches_analytic_for_x_cubed() {
let x = array![0.0f64, 1.0, 2.0, 3.0];
let y = array![0.0f64, 1.0, 8.0, 27.0];
let spline = CubicSpline::new(&x.view(), &y.view()).expect("Construction failed");
let d1 = Differentiable::derivative(&spline, 1usize).expect("1st derivative failed");
for xi in [0.5, 1.0, 1.5, 2.0, 2.5] {
let h = 1e-6;
let fd = (spline.evaluate(xi + h).expect("eval+h")
- spline.evaluate(xi - h).expect("eval-h"))
/ (2.0 * h);
let symbolic = d1.evaluate(xi).expect("symbolic eval");
assert!(
(symbolic - fd).abs() < 1e-4,
"xi={xi}: symbolic={symbolic}, fd={fd}"
);
}
}
#[test]
fn pchip_derivative_preserves_monotonicity() {
let x = array![0.0f64, 1.0, 2.0, 3.0, 4.0];
let y = array![0.0f64, 1.0, 2.0, 3.0, 4.0]; let pchip =
PchipInterpolator::new(&x.view(), &y.view(), false).expect("Construction failed");
let d1 = Differentiable::derivative(&pchip, 1usize).expect("1st derivative failed");
for xi in [0.5f64, 1.5, 2.5, 3.5] {
let v = d1.evaluate(xi).expect("eval");
assert!((v - 1.0).abs() < 1e-10, "xi={xi}: derivative={v}");
}
}
}