1use numra_core::Scalar;
11
12use crate::cubic_spline::coefficients_from_slopes;
13use crate::error::InterpError;
14use crate::{eval_piecewise_cubic, eval_piecewise_cubic_deriv, integrate_piecewise_cubic};
15use crate::{validate_data, Interpolant};
16
17pub struct Pchip<S: Scalar> {
22 x: Vec<S>,
23 a: Vec<S>,
24 b: Vec<S>,
25 c: Vec<S>,
26 d: Vec<S>,
27}
28
29impl<S: Scalar> Pchip<S> {
30 pub fn new(x: &[S], y: &[S]) -> Result<Self, InterpError> {
36 validate_data(x, y, 2)?;
37 let n = x.len();
38 let slopes = compute_pchip_slopes(x, y);
39 let (a, b, c, d) = coefficients_from_slopes(x, y, &slopes);
40
41 Ok(Self {
42 x: x[..n].to_vec(),
43 a,
44 b,
45 c,
46 d,
47 })
48 }
49}
50
51impl<S: Scalar> Interpolant<S> for Pchip<S> {
52 fn interpolate(&self, x: S) -> S {
53 eval_piecewise_cubic(&self.x, &self.a, &self.b, &self.c, &self.d, x)
54 }
55
56 fn derivative(&self, x: S) -> Option<S> {
57 Some(eval_piecewise_cubic_deriv(
58 &self.x, &self.b, &self.c, &self.d, x,
59 ))
60 }
61
62 fn integrate(&self, a: S, b: S) -> Option<S> {
63 Some(integrate_piecewise_cubic(
64 &self.x, &self.a, &self.b, &self.c, &self.d, a, b,
65 ))
66 }
67}
68
69fn compute_pchip_slopes<S: Scalar>(x: &[S], y: &[S]) -> Vec<S> {
71 let n = x.len();
72 let mut slopes = vec![S::ZERO; n];
73
74 if n == 2 {
75 let s = (y[1] - y[0]) / (x[1] - x[0]);
76 slopes[0] = s;
77 slopes[1] = s;
78 return slopes;
79 }
80
81 let n_seg = n - 1;
83 let h: Vec<S> = (0..n_seg).map(|i| x[i + 1] - x[i]).collect();
84 let s: Vec<S> = (0..n_seg).map(|i| (y[i + 1] - y[i]) / h[i]).collect();
85
86 for i in 1..n - 1 {
88 if (s[i - 1] > S::ZERO && s[i] > S::ZERO) || (s[i - 1] < S::ZERO && s[i] < S::ZERO) {
89 let w1 = S::TWO * h[i] + h[i - 1];
91 let w2 = h[i] + S::TWO * h[i - 1];
92 slopes[i] = (w1 + w2) / (w1 / s[i - 1] + w2 / s[i]);
93 }
94 }
96
97 slopes[0] = endpoint_slope(h[0], h[1], s[0], s[1]);
99 slopes[n - 1] = endpoint_slope(h[n_seg - 1], h[n_seg - 2], s[n_seg - 1], s[n_seg - 2]);
100
101 slopes
102}
103
104fn endpoint_slope<S: Scalar>(h1: S, h2: S, s1: S, s2: S) -> S {
106 let d = ((S::TWO * h1 + h2) * s1 - h1 * s2) / (h1 + h2);
107
108 if (d > S::ZERO) != (s1 > S::ZERO) {
110 return S::ZERO;
111 }
112 let three = S::from_f64(3.0);
113 if (s1 > S::ZERO) != (s2 > S::ZERO) && d.abs() > three * s1.abs() {
114 return three * s1;
115 }
116 d
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122 use approx::assert_relative_eq;
123
124 #[test]
125 fn test_pchip_at_knots() {
126 let x = vec![0.0, 1.0, 2.0, 3.0, 4.0];
127 let y = vec![0.0, 1.0, 0.5, 1.5, 1.0];
128 let p = Pchip::new(&x, &y).unwrap();
129 for (&xi, &yi) in x.iter().zip(y.iter()) {
130 assert_relative_eq!(p.interpolate(xi), yi, epsilon = 1e-12);
131 }
132 }
133
134 #[test]
135 fn test_pchip_monotonicity() {
136 let x = vec![0.0, 1.0, 2.0, 5.0, 10.0];
138 let y = vec![0.0, 1.0, 4.0, 5.0, 10.0];
139 let p = Pchip::new(&x, &y).unwrap();
140
141 let mut prev = p.interpolate(0.0);
143 for i in 1..100 {
144 let xi = i as f64 * 10.0 / 100.0;
145 let yi = p.interpolate(xi);
146 assert!(
147 yi >= prev - 1e-10,
148 "Monotonicity violation at x={}: {} < {}",
149 xi,
150 yi,
151 prev
152 );
153 prev = yi;
154 }
155 }
156
157 #[test]
158 fn test_pchip_step_function() {
159 let x = vec![0.0, 1.0, 1.5, 2.0, 3.0];
161 let y = vec![0.0, 0.0, 1.0, 1.0, 1.0];
162 let p = Pchip::new(&x, &y).unwrap();
163
164 for i in 0..100 {
166 let xi = i as f64 * 3.0 / 100.0;
167 let yi = p.interpolate(xi);
168 assert!(yi >= -0.01 && yi <= 1.01, "Overshoot at x={}: y={}", xi, yi);
169 }
170 }
171
172 #[test]
173 fn test_pchip_two_points() {
174 let p = Pchip::new(&[0.0, 1.0], &[0.0, 1.0]).unwrap();
175 assert_relative_eq!(p.interpolate(0.5), 0.5, epsilon = 1e-14);
176 }
177
178 #[test]
179 fn test_pchip_derivative() {
180 let x = vec![0.0, 1.0, 2.0, 3.0];
181 let y = vec![0.0, 1.0, 4.0, 9.0]; let p = Pchip::new(&x, &y).unwrap();
183 let d = p.derivative(1.0).unwrap();
185 assert!((d - 2.0).abs() < 1.0, "Derivative at 1.0: {}", d);
186 }
187
188 #[test]
189 fn test_pchip_integrate() {
190 let x = vec![0.0, 1.0, 2.0, 3.0];
192 let y = vec![0.0, 1.0, 2.0, 3.0];
193 let p = Pchip::new(&x, &y).unwrap();
194 let integral = p.integrate(0.0, 3.0).unwrap();
195 assert_relative_eq!(integral, 4.5, epsilon = 1e-10);
196 }
197}