use nalgebra::DVector;
use std::f64;
pub fn lagrange_interpolate(x: f64, x_vals: &DVector<f64>, y_vals: &DVector<f64>) -> f64 {
let n = x_vals.len();
let mut result = 0.0;
for i in 0..n {
let mut term = y_vals[i];
for j in 0..n {
if i != j {
term *= (x - x_vals[j]) / (x_vals[i] - x_vals[j]);
}
}
result += term;
}
result
}
pub fn newton_divided_differences(x_vals: &DVector<f64>, y_vals: &DVector<f64>) -> DVector<f64> {
let n = x_vals.len();
let mut coef = y_vals.clone();
for j in 1..n {
for i in (j..n).rev() {
coef[i] = (coef[i] - coef[i - 1]) / (x_vals[i] - x_vals[i - j]);
}
}
coef
}
pub fn newton_interpolate(x: f64, x_vals: &DVector<f64>, coef: &DVector<f64>) -> f64 {
let n = coef.len();
let mut result = coef[n - 1];
for i in (0..n - 1).rev() {
result = result * (x - x_vals[i]) + coef[i];
}
result
}
#[derive(Clone, Copy, Debug)]
pub enum InterpolationMethod {
Pchip {
space: InterpolationSpace,
clamp: bool,
},
}
#[derive(Clone, Copy, Debug)]
pub enum InterpolationSpace {
Linear,
Log,
}
use std::cmp::Ordering;
const LOG_FLOOR: f64 = -690.7755;
pub struct Pchip {
x: Vec<f64>,
y: Vec<f64>,
m: Vec<f64>,
y_min: f64,
y_max: f64,
space: InterpolationSpace,
}
impl Pchip {
pub fn new(x: &[f64], y: &[f64], space: InterpolationSpace) -> Self {
assert!(x.len() >= 2, "PCHIP requires at least two points");
assert_eq!(x.len(), y.len(), "x and y must have same length");
for i in 1..x.len() {
assert!(x[i] > x[i - 1], "x must be strictly increasing");
}
let y_transformed: Vec<f64> = match space {
InterpolationSpace::Linear => y.to_vec(),
InterpolationSpace::Log => y
.iter()
.map(|&v| {
assert!(v > 0.0, "Log-space PCHIP requires strictly positive data");
v.ln().max(LOG_FLOOR)
})
.collect(),
};
let n = x.len();
let mut h = vec![0.0; n - 1];
let mut delta = vec![0.0; n - 1];
for i in 0..n - 1 {
h[i] = x[i + 1] - x[i];
delta[i] = (y_transformed[i + 1] - y_transformed[i]) / h[i];
}
let mut m = vec![0.0; n];
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];
m[i] = (w1 + w2) / (w1 / delta[i - 1] + w2 / delta[i]);
} else {
m[i] = 0.0;
}
}
m[0] = delta[0];
m[n - 1] = delta[n - 2];
let (y_min, y_max) = match space {
InterpolationSpace::Linear => {
let min = y.iter().cloned().fold(f64::INFINITY, f64::min);
let max = y.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
(min, max)
}
InterpolationSpace::Log => {
let min = y.iter().cloned().fold(f64::INFINITY, f64::min);
let max = y.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
(min, max)
}
};
Self {
x: x.to_vec(),
y: y_transformed,
m,
y_min,
y_max,
space,
}
}
pub fn eval(&self, xq: f64, clamp: bool) -> f64 {
let n = self.x.len();
let i = match self
.x
.binary_search_by(|v| v.partial_cmp(&xq).unwrap_or(Ordering::Less))
{
Ok(i) => {
return self.post_process(self.y[i], clamp);
}
Err(i) => {
if i == 0 {
return self.post_process(self.y[0] + self.m[0] * (xq - self.x[0]), clamp);
} else if i >= n {
return self
.post_process(self.y[n - 1] + self.m[n - 1] * (xq - self.x[n - 1]), clamp);
}
i - 1
}
};
let h = self.x[i + 1] - self.x[i];
let t = (xq - self.x[i]) / h;
let h00 = 2.0 * t * t * t - 3.0 * t * t + 1.0;
let h10 = t * t * t - 2.0 * t * t + t;
let h01 = -2.0 * t * t * t + 3.0 * t * t;
let h11 = t * t * t - t * t;
let yq =
h00 * self.y[i] + h10 * h * self.m[i] + h01 * self.y[i + 1] + h11 * h * self.m[i + 1];
self.post_process(yq, clamp)
}
#[inline]
fn post_process(&self, y_internal: f64, clamp: bool) -> f64 {
let y_phys = match self.space {
InterpolationSpace::Linear => y_internal,
InterpolationSpace::Log => y_internal.exp(),
};
if clamp {
y_phys.clamp(self.y_min, self.y_max)
} else {
y_phys
}
}
}
#[cfg(test)]
mod tests_PCHIP {
use super::*;
const EPS: f64 = 1e-12;
#[test]
fn pchip_preserves_monotonicity_linear() {
let x = vec![0.0, 1.0, 2.0, 3.0, 4.0];
let y = vec![0.0, 0.5, 1.0, 1.5, 2.0];
let pchip = Pchip::new(&x, &y, InterpolationSpace::Linear);
let mut prev = pchip.eval(0.0, false);
for i in 1..500 {
let xq = 4.0 * i as f64 / 500.0;
let yq = pchip.eval(xq, false);
assert!(
yq + EPS >= prev,
"Monotonicity violated: {} < {} at x={}",
yq,
prev,
xq
);
assert!(yq >= 0.0 - EPS);
assert!(yq <= 2.0 + EPS);
prev = yq;
}
}
#[test]
fn pchip_no_overshoot_nonlinear() {
let x = vec![0.0, 1.0, 2.0, 3.0];
let y = vec![0.0, 0.01, 0.5, 1.0];
let pchip = Pchip::new(&x, &y, InterpolationSpace::Linear);
let y_min = *y.iter().min_by(|a, b| a.partial_cmp(b).unwrap()).unwrap();
let y_max = *y.iter().max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap();
for i in 0..500 {
let xq = 3.0 * i as f64 / 500.0;
let yq = pchip.eval(xq, false);
assert!(
yq >= y_min - EPS && yq <= y_max + EPS,
"Overshoot detected: y={} at x={}",
yq,
xq
);
}
}
#[test]
fn pchip_log_space_preserves_positivity() {
let x = vec![300.0, 600.0, 1000.0, 2000.0];
let y = vec![1e-12, 1e-9, 1e-6, 1e-3];
let pchip = Pchip::new(&x, &y, InterpolationSpace::Log);
for i in 0..1000 {
let xq = 300.0 + (2000.0 - 300.0) * i as f64 / 1000.0;
let yq = pchip.eval(xq, false);
assert!(
yq > 0.0,
"Log-space PCHIP produced non-positive value: {} at x={}",
yq,
xq
);
}
}
#[test]
fn pchip_log_space_respects_ratios() {
let x = vec![1.0, 2.0, 3.0];
let y = vec![1e-6, 1e-4, 1e-2];
let pchip = Pchip::new(&x, &y, InterpolationSpace::Log);
let y_mid = pchip.eval(2.0, false);
assert!((y_mid - 1e-4).abs() / 1e-4 < 1e-6);
let y_quarter = pchip.eval(1.5, false);
let expected = (1e-6_f64 * 1e-4_f64).sqrt();
let rel_err = (y_quarter - expected).abs() / expected;
assert!(
rel_err < 1e-3,
"Log-space interpolation is not multiplicative enough"
);
}
#[test]
fn clamping_works_when_enabled() {
let x = vec![0.0, 1.0, 2.0];
let y = vec![0.2, 0.5, 0.8];
let pchip = Pchip::new(&x, &y, InterpolationSpace::Linear);
let yq = pchip.eval(-10.0, true);
assert!(yq >= 0.2 && yq <= 0.8, "Clamping failed: yq={}", yq);
}
#[test]
fn pchip_exact_at_nodes() {
let x = vec![0.0, 1.0, 2.0, 3.0];
let y = vec![1.0, 4.0, 2.0, 8.0];
let pchip = Pchip::new(&x, &y, InterpolationSpace::Linear);
for i in 0..x.len() {
let result = pchip.eval(x[i], false);
assert!(
(result - y[i]).abs() < EPS,
"Not exact at node {}: {} vs {}",
i,
result,
y[i]
);
}
}
#[test]
fn pchip_extrapolation_linear() {
let x = vec![0.0, 1.0, 2.0];
let y = vec![0.0, 1.0, 4.0];
let pchip = Pchip::new(&x, &y, InterpolationSpace::Linear);
let left_extrap = pchip.eval(-1.0, false);
assert!(
left_extrap.is_finite(),
"Left extrapolation should be finite"
);
let right_extrap = pchip.eval(3.0, false);
assert!(
right_extrap.is_finite(),
"Right extrapolation should be finite"
);
}
#[test]
fn pchip_handles_flat_segments() {
let x = vec![0.0, 1.0, 2.0, 3.0];
let y = vec![1.0, 1.0, 1.0, 2.0];
let pchip = Pchip::new(&x, &y, InterpolationSpace::Linear);
let mid_val = pchip.eval(1.5, false);
assert!(
(mid_val - 1.0).abs() < 0.1,
"Flat segment handling failed: {}",
mid_val
);
}
#[test]
fn pchip_log_space_positivity() {
let x = vec![0.0, 1.0, 2.0];
let y = vec![1e-10, 1e-5, 1.0];
let pchip = Pchip::new(&x, &y, InterpolationSpace::Log);
for i in 0..100 {
let xq = 2.0 * i as f64 / 100.0;
let yq = pchip.eval(xq, false);
assert!(
yq > 0.0,
"Log-space result not positive: {} at x={}",
yq,
xq
);
}
}
#[test]
fn pchip_reasonable_accuracy() {
let x = vec![0.0, 1.0, 2.0, 3.0];
let y: Vec<f64> = x.iter().map(|&xi| xi * xi).collect();
let pchip = Pchip::new(&x, &y, InterpolationSpace::Linear);
let test_x = 1.5;
let expected = test_x * test_x;
let result = pchip.eval(test_x, false);
let rel_error = (result - expected).abs() / expected;
assert!(
rel_error < 0.1,
"Accuracy test failed: relative error {} too large",
rel_error
);
}
#[test]
fn pchip_minimum_points() {
let x = vec![0.0, 1.0];
let y = vec![1.0, 3.0];
let pchip = Pchip::new(&x, &y, InterpolationSpace::Linear);
let result = pchip.eval(0.5, false);
assert!(
result.is_finite() && result > 0.0,
"Minimum points test failed: {}",
result
);
}
#[test]
#[should_panic(expected = "PCHIP requires at least two points")]
fn pchip_panics_single_point() {
let x = vec![1.0];
let y = vec![2.0];
Pchip::new(&x, &y, InterpolationSpace::Linear);
}
#[test]
#[should_panic(expected = "x and y must have same length")]
fn pchip_panics_mismatched_lengths() {
let x = vec![0.0, 1.0];
let y = vec![1.0];
Pchip::new(&x, &y, InterpolationSpace::Linear);
}
#[test]
#[should_panic(expected = "x must be strictly increasing")]
fn pchip_panics_non_increasing() {
let x = vec![0.0, 1.0, 0.5];
let y = vec![1.0, 2.0, 1.5];
Pchip::new(&x, &y, InterpolationSpace::Linear);
}
#[test]
#[should_panic(expected = "Log-space PCHIP requires strictly positive data")]
fn pchip_panics_negative_log() {
let x = vec![0.0, 1.0, 2.0];
let y = vec![1.0, -1.0, 2.0];
Pchip::new(&x, &y, InterpolationSpace::Log);
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_lagrange_linear_interpolation() {
let x_vals = DVector::from_vec(vec![0.0, 1.0]);
let y_vals = DVector::from_vec(vec![1.0, 3.0]);
assert_relative_eq!(
lagrange_interpolate(0.0, &x_vals, &y_vals),
1.0,
epsilon = 1e-10
);
assert_relative_eq!(
lagrange_interpolate(1.0, &x_vals, &y_vals),
3.0,
epsilon = 1e-10
);
assert_relative_eq!(
lagrange_interpolate(0.5, &x_vals, &y_vals),
2.0,
epsilon = 1e-10
);
assert_relative_eq!(
lagrange_interpolate(2.0, &x_vals, &y_vals),
5.0,
epsilon = 1e-10
);
}
#[test]
fn test_lagrange_quadratic_interpolation() {
let x_vals = DVector::from_vec(vec![0.0, 1.0, 2.0]);
let y_vals = DVector::from_vec(vec![1.0, 0.0, 1.0]);
assert_relative_eq!(
lagrange_interpolate(0.0, &x_vals, &y_vals),
1.0,
epsilon = 1e-10
);
assert_relative_eq!(
lagrange_interpolate(1.0, &x_vals, &y_vals),
0.0,
epsilon = 1e-10
);
assert_relative_eq!(
lagrange_interpolate(2.0, &x_vals, &y_vals),
1.0,
epsilon = 1e-10
);
assert_relative_eq!(
lagrange_interpolate(0.5, &x_vals, &y_vals),
0.25,
epsilon = 1e-10
);
assert_relative_eq!(
lagrange_interpolate(1.5, &x_vals, &y_vals),
0.25,
epsilon = 1e-10
);
assert_relative_eq!(
lagrange_interpolate(3.0, &x_vals, &y_vals),
4.0,
epsilon = 1e-10
);
}
#[test]
fn test_lagrange_cubic_interpolation() {
let x_vals = DVector::from_vec(vec![0.0, 1.0, 2.0, 3.0]);
let y_vals = DVector::from_vec(vec![-6.0, 0.0, 0.0, 0.0]);
assert_relative_eq!(
lagrange_interpolate(0.0, &x_vals, &y_vals),
-6.0,
epsilon = 1e-10
);
assert_relative_eq!(
lagrange_interpolate(1.0, &x_vals, &y_vals),
0.0,
epsilon = 1e-10
);
assert_relative_eq!(
lagrange_interpolate(2.0, &x_vals, &y_vals),
0.0,
epsilon = 1e-10
);
assert_relative_eq!(
lagrange_interpolate(3.0, &x_vals, &y_vals),
0.0,
epsilon = 1e-10
);
assert_relative_eq!(
lagrange_interpolate(1.5, &x_vals, &y_vals),
0.375,
epsilon = 1e-10
);
}
#[test]
fn test_newton_divided_differences_linear() {
let x_vals = DVector::from_vec(vec![0.0, 1.0]);
let y_vals = DVector::from_vec(vec![1.0, 3.0]);
let coef = newton_divided_differences(&x_vals, &y_vals);
assert_relative_eq!(coef[0], 1.0, epsilon = 1e-10);
assert_relative_eq!(coef[1], 2.0, epsilon = 1e-10);
}
#[test]
fn test_newton_divided_differences_quadratic() {
let x_vals = DVector::from_vec(vec![0.0, 1.0, 2.0]);
let y_vals = DVector::from_vec(vec![0.0, 1.0, 4.0]);
let coef = newton_divided_differences(&x_vals, &y_vals);
assert_relative_eq!(coef[0], 0.0, epsilon = 1e-10);
assert_relative_eq!(coef[1], 1.0, epsilon = 1e-10);
assert_relative_eq!(coef[2], 1.0, epsilon = 1e-10);
}
#[test]
fn test_newton_interpolate_linear() {
let x_vals = DVector::from_vec(vec![0.0, 1.0]);
let y_vals = DVector::from_vec(vec![1.0, 3.0]);
let coef = newton_divided_differences(&x_vals, &y_vals);
assert_relative_eq!(
newton_interpolate(0.0, &x_vals, &coef),
1.0,
epsilon = 1e-10
);
assert_relative_eq!(
newton_interpolate(1.0, &x_vals, &coef),
3.0,
epsilon = 1e-10
);
assert_relative_eq!(
newton_interpolate(0.5, &x_vals, &coef),
2.0,
epsilon = 1e-10
);
assert_relative_eq!(
newton_interpolate(2.0, &x_vals, &coef),
5.0,
epsilon = 1e-10
);
}
#[test]
fn test_newton_interpolate_quadratic() {
let x_vals = DVector::from_vec(vec![0.0, 1.0, 2.0]);
let y_vals = DVector::from_vec(vec![1.0, 0.0, 1.0]);
let coef = newton_divided_differences(&x_vals, &y_vals);
assert_relative_eq!(
newton_interpolate(0.0, &x_vals, &coef),
1.0,
epsilon = 1e-10
);
assert_relative_eq!(
newton_interpolate(1.0, &x_vals, &coef),
0.0,
epsilon = 1e-10
);
assert_relative_eq!(
newton_interpolate(2.0, &x_vals, &coef),
1.0,
epsilon = 1e-10
);
assert_relative_eq!(
newton_interpolate(0.5, &x_vals, &coef),
0.25,
epsilon = 1e-10
);
assert_relative_eq!(
newton_interpolate(1.5, &x_vals, &coef),
0.25,
epsilon = 1e-10
);
}
#[test]
fn test_newton_vs_lagrange_equivalence() {
let x_vals = DVector::from_vec(vec![0.0, 1.0, 2.0, 3.0]);
let y_vals = DVector::from_vec(vec![1.0, 4.0, 9.0, 16.0]);
let coef = newton_divided_differences(&x_vals, &y_vals);
let test_points = vec![-1.0, 0.5, 1.5, 2.5, 4.0];
for &x in &test_points {
let lagrange_result = lagrange_interpolate(x, &x_vals, &y_vals);
let newton_result = newton_interpolate(x, &x_vals, &coef);
assert_relative_eq!(lagrange_result, newton_result, epsilon = 1e-10,)
}
}
#[test]
fn test_interpolation_with_uneven_spacing() {
let x_vals = DVector::from_vec(vec![-2.0, -0.5, 1.0, 3.5]);
let y_vals = DVector::from_vec(vec![4.0, 0.25, 1.0, 12.25]);
let coef = newton_divided_differences(&x_vals, &y_vals);
for i in 0..x_vals.len() {
assert_relative_eq!(
lagrange_interpolate(x_vals[i], &x_vals, &y_vals),
y_vals[i],
epsilon = 1e-10
);
assert_relative_eq!(
newton_interpolate(x_vals[i], &x_vals, &coef),
y_vals[i],
epsilon = 1e-10
);
}
let test_x = 0.0;
let expected_y = 0.0;
assert_relative_eq!(
lagrange_interpolate(test_x, &x_vals, &y_vals),
expected_y,
epsilon = 1e-10
);
assert_relative_eq!(
newton_interpolate(test_x, &x_vals, &coef),
expected_y,
epsilon = 1e-10
);
}
#[test]
fn test_single_point_interpolation() {
let x_vals = DVector::from_vec(vec![1.0]);
let y_vals = DVector::from_vec(vec![5.0]);
let coef = newton_divided_differences(&x_vals, &y_vals);
assert_relative_eq!(
lagrange_interpolate(0.0, &x_vals, &y_vals),
5.0,
epsilon = 1e-10
);
assert_relative_eq!(
lagrange_interpolate(1.0, &x_vals, &y_vals),
5.0,
epsilon = 1e-10
);
assert_relative_eq!(
lagrange_interpolate(10.0, &x_vals, &y_vals),
5.0,
epsilon = 1e-10
);
assert_relative_eq!(
newton_interpolate(0.0, &x_vals, &coef),
5.0,
epsilon = 1e-10
);
assert_relative_eq!(
newton_interpolate(1.0, &x_vals, &coef),
5.0,
epsilon = 1e-10
);
assert_relative_eq!(
newton_interpolate(10.0, &x_vals, &coef),
5.0,
epsilon = 1e-10
);
}
#[test]
fn test_interpolation_with_sine_function() {
use std::f64::consts::PI;
let x_vals = DVector::from_vec(vec![0.0, PI / 6.0, PI / 4.0, PI / 3.0, PI / 2.0]);
let y_vals = DVector::from_vec(vec![
0.0, 0.5, 2_f64.sqrt() / 2.0, 3_f64.sqrt() / 2.0, 1.0, ]);
let coef = newton_divided_differences(&x_vals, &y_vals);
for i in 0..x_vals.len() {
assert_relative_eq!(
lagrange_interpolate(x_vals[i], &x_vals, &y_vals),
y_vals[i],
epsilon = 1e-10
);
assert_relative_eq!(
newton_interpolate(x_vals[i], &x_vals, &coef),
y_vals[i],
epsilon = 1e-10
)
}
}
}