use numra_core::Scalar;
use crate::cubic_spline::coefficients_from_slopes;
use crate::error::InterpError;
use crate::{eval_piecewise_cubic, eval_piecewise_cubic_deriv, integrate_piecewise_cubic};
use crate::{validate_data, Interpolant};
pub struct Pchip<S: Scalar> {
x: Vec<S>,
a: Vec<S>,
b: Vec<S>,
c: Vec<S>,
d: Vec<S>,
}
impl<S: Scalar> Pchip<S> {
pub fn new(x: &[S], y: &[S]) -> Result<Self, InterpError> {
validate_data(x, y, 2)?;
let n = x.len();
let slopes = compute_pchip_slopes(x, y);
let (a, b, c, d) = coefficients_from_slopes(x, y, &slopes);
Ok(Self {
x: x[..n].to_vec(),
a,
b,
c,
d,
})
}
}
impl<S: Scalar> Interpolant<S> for Pchip<S> {
fn interpolate(&self, x: S) -> S {
eval_piecewise_cubic(&self.x, &self.a, &self.b, &self.c, &self.d, x)
}
fn derivative(&self, x: S) -> Option<S> {
Some(eval_piecewise_cubic_deriv(
&self.x, &self.b, &self.c, &self.d, x,
))
}
fn integrate(&self, a: S, b: S) -> Option<S> {
Some(integrate_piecewise_cubic(
&self.x, &self.a, &self.b, &self.c, &self.d, a, b,
))
}
}
fn compute_pchip_slopes<S: Scalar>(x: &[S], y: &[S]) -> Vec<S> {
let n = x.len();
let mut slopes = vec![S::ZERO; n];
if n == 2 {
let s = (y[1] - y[0]) / (x[1] - x[0]);
slopes[0] = s;
slopes[1] = s;
return slopes;
}
let n_seg = n - 1;
let h: Vec<S> = (0..n_seg).map(|i| x[i + 1] - x[i]).collect();
let s: Vec<S> = (0..n_seg).map(|i| (y[i + 1] - y[i]) / h[i]).collect();
for i in 1..n - 1 {
if (s[i - 1] > S::ZERO && s[i] > S::ZERO) || (s[i - 1] < S::ZERO && s[i] < S::ZERO) {
let w1 = S::TWO * h[i] + h[i - 1];
let w2 = h[i] + S::TWO * h[i - 1];
slopes[i] = (w1 + w2) / (w1 / s[i - 1] + w2 / s[i]);
}
}
slopes[0] = endpoint_slope(h[0], h[1], s[0], s[1]);
slopes[n - 1] = endpoint_slope(h[n_seg - 1], h[n_seg - 2], s[n_seg - 1], s[n_seg - 2]);
slopes
}
fn endpoint_slope<S: Scalar>(h1: S, h2: S, s1: S, s2: S) -> S {
let d = ((S::TWO * h1 + h2) * s1 - h1 * s2) / (h1 + h2);
if (d > S::ZERO) != (s1 > S::ZERO) {
return S::ZERO;
}
let three = S::from_f64(3.0);
if (s1 > S::ZERO) != (s2 > S::ZERO) && d.abs() > three * s1.abs() {
return three * s1;
}
d
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_pchip_at_knots() {
let x = vec![0.0, 1.0, 2.0, 3.0, 4.0];
let y = vec![0.0, 1.0, 0.5, 1.5, 1.0];
let p = Pchip::new(&x, &y).unwrap();
for (&xi, &yi) in x.iter().zip(y.iter()) {
assert_relative_eq!(p.interpolate(xi), yi, epsilon = 1e-12);
}
}
#[test]
fn test_pchip_monotonicity() {
let x = vec![0.0, 1.0, 2.0, 5.0, 10.0];
let y = vec![0.0, 1.0, 4.0, 5.0, 10.0];
let p = Pchip::new(&x, &y).unwrap();
let mut prev = p.interpolate(0.0);
for i in 1..100 {
let xi = i as f64 * 10.0 / 100.0;
let yi = p.interpolate(xi);
assert!(
yi >= prev - 1e-10,
"Monotonicity violation at x={}: {} < {}",
xi,
yi,
prev
);
prev = yi;
}
}
#[test]
fn test_pchip_step_function() {
let x = vec![0.0, 1.0, 1.5, 2.0, 3.0];
let y = vec![0.0, 0.0, 1.0, 1.0, 1.0];
let p = Pchip::new(&x, &y).unwrap();
for i in 0..100 {
let xi = i as f64 * 3.0 / 100.0;
let yi = p.interpolate(xi);
assert!(yi >= -0.01 && yi <= 1.01, "Overshoot at x={}: y={}", xi, yi);
}
}
#[test]
fn test_pchip_two_points() {
let p = Pchip::new(&[0.0, 1.0], &[0.0, 1.0]).unwrap();
assert_relative_eq!(p.interpolate(0.5), 0.5, epsilon = 1e-14);
}
#[test]
fn test_pchip_derivative() {
let x = vec![0.0, 1.0, 2.0, 3.0];
let y = vec![0.0, 1.0, 4.0, 9.0]; let p = Pchip::new(&x, &y).unwrap();
let d = p.derivative(1.0).unwrap();
assert!((d - 2.0).abs() < 1.0, "Derivative at 1.0: {}", d);
}
#[test]
fn test_pchip_integrate() {
let x = vec![0.0, 1.0, 2.0, 3.0];
let y = vec![0.0, 1.0, 2.0, 3.0];
let p = Pchip::new(&x, &y).unwrap();
let integral = p.integrate(0.0, 3.0).unwrap();
assert_relative_eq!(integral, 4.5, epsilon = 1e-10);
}
}