use crate::core::errors::RustyQLibError;
#[derive(Debug, Clone)]
pub struct Pchip {
xs: Vec<f64>,
ys: Vec<f64>,
d: Vec<f64>,
}
impl Pchip {
pub fn new(xs: &[f64], ys: &[f64]) -> Result<Self, RustyQLibError> {
let n = xs.len();
if n < 2 || ys.len() != n {
return Err(RustyQLibError::invalid_input("pchip", "need at least two knots with matching y values"));
}
if xs.windows(2).any(|w| w[1] <= w[0]) {
return Err(RustyQLibError::invalid_input("pchip", "knots must be strictly increasing"));
}
let h: Vec<f64> = xs.windows(2).map(|w| w[1] - w[0]).collect();
let delta: Vec<f64> =
(0..n - 1).map(|i| (ys[i + 1] - ys[i]) / h[i]).collect();
let mut d = vec![0.0; n];
if n == 2 {
d[0] = delta[0];
d[1] = delta[0];
} else {
for i in 1..n - 1 {
if delta[i - 1] * delta[i] > 0.0 {
let w1 = 2.0 * h[i] + h[i - 1];
let w2 = h[i] + 2.0 * h[i - 1];
d[i] = (w1 + w2) / (w1 / delta[i - 1] + w2 / delta[i]);
}
}
d[0] = end_slope(h[0], h[1], delta[0], delta[1]);
d[n - 1] = end_slope(h[n - 2], h[n - 3], delta[n - 2], delta[n - 3]);
}
Ok(Pchip { xs: xs.to_vec(), ys: ys.to_vec(), d })
}
pub fn eval(&self, x: f64) -> f64 {
let n = self.xs.len();
if x <= self.xs[0] {
return self.ys[0] + self.d[0] * (x - self.xs[0]);
}
if x >= self.xs[n - 1] {
return self.ys[n - 1] + self.d[n - 1] * (x - self.xs[n - 1]);
}
let i = self.xs[1..n - 1].partition_point(|&xi| xi < x);
hermite(
x,
self.xs[i],
self.xs[i + 1],
self.ys[i],
self.ys[i + 1],
self.d[i],
self.d[i + 1],
)
}
}
fn end_slope(h0: f64, h1: f64, delta0: f64, delta1: f64) -> f64 {
let d = ((2.0 * h0 + h1) * delta0 - h0 * delta1) / (h0 + h1);
if d * delta0 <= 0.0 {
0.0
} else if delta0 * delta1 < 0.0 && d.abs() > 3.0 * delta0.abs() {
3.0 * delta0
} else {
d
}
}
pub(crate) fn hermite(x: f64, x0: f64, x1: f64, y0: f64, y1: f64, d0: f64, d1: f64) -> f64 {
let h = x1 - x0;
let t = (x - x0) / h;
let t2 = t * t;
let t3 = t2 * t;
y0 * (2.0 * t3 - 3.0 * t2 + 1.0)
+ y1 * (-2.0 * t3 + 3.0 * t2)
+ d0 * h * (t3 - 2.0 * t2 + t)
+ d1 * h * (t3 - t2)
}
#[cfg(test)]
mod tests {
use super::super::cubic_spline::{BoundaryCondition, CubicSpline};
use super::*;
fn step_data() -> (Vec<f64>, Vec<f64>) {
(
vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0],
vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
)
}
#[test]
fn interpolates_the_knots() {
let (xs, ys) = step_data();
let p = Pchip::new(&xs, &ys).unwrap();
for (x, y) in xs.iter().zip(&ys) {
assert!((p.eval(*x) - y).abs() < 1e-14);
}
}
#[test]
fn preserves_monotonicity_where_a_cubic_spline_overshoots() {
let (xs, ys) = step_data();
let pchip = Pchip::new(&xs, &ys).unwrap();
let spline = CubicSpline::new(&xs, &ys, BoundaryCondition::Natural).unwrap();
let mut prev = f64::NEG_INFINITY;
let mut spline_overshoots = false;
for i in 0..=500 {
let x = i as f64 * 0.01;
let v = pchip.eval(x);
assert!(v >= prev - 1e-12, "pchip not monotone at {x}");
assert!((-1e-12..=1.0 + 1e-12).contains(&v), "pchip overshoots at {x}");
prev = v;
let s = spline.eval(x);
if !(0.0..=1.0).contains(&s) {
spline_overshoots = true;
}
}
assert!(spline_overshoots, "cubic spline unexpectedly shape-preserving");
}
#[test]
fn flat_data_stays_exactly_flat() {
let xs = [0.0, 1.0, 2.0, 3.0];
let ys = [5.0, 5.0, 5.0, 5.0];
let p = Pchip::new(&xs, &ys).unwrap();
for i in 0..=30 {
assert!((p.eval(i as f64 * 0.1) - 5.0).abs() < 1e-12);
}
}
}