pub(crate) fn natural_second_derivatives(x: &[f64], y: &[f64]) -> Vec<f64> {
let n = x.len().min(y.len());
if n < 3 {
return vec![0.0; n];
}
let mut y2 = vec![0.0_f64; n];
let mut u = vec![0.0_f64; n];
for i in 1..n - 1 {
let sig = (x[i] - x[i - 1]) / (x[i + 1] - x[i - 1]);
let p = sig * y2[i - 1] + 2.0;
y2[i] = (sig - 1.0) / p;
let d = (y[i + 1] - y[i]) / (x[i + 1] - x[i]) - (y[i] - y[i - 1]) / (x[i] - x[i - 1]);
u[i] = (6.0 * d / (x[i + 1] - x[i - 1]) - sig * u[i - 1]) / p;
}
for k in (0..n - 1).rev() {
y2[k] = y2[k] * y2[k + 1] + u[k];
}
y2
}
#[cfg(test)]
mod tests {
use super::*;
fn splint(x: &[f64], y: &[f64], y2: &[f64], xq: f64) -> f64 {
let hi = x.partition_point(|&v| v < xq).clamp(1, x.len() - 1);
let lo = hi - 1;
let h = x[hi] - x[lo];
let a = (x[hi] - xq) / h;
let b = (xq - x[lo]) / h;
a * y[lo]
+ b * y[hi]
+ ((a * a - 1.0) * a * y2[lo] + (b * b - 1.0) * b * y2[hi]) * h * h / 6.0
}
#[test]
fn interpolates_exactly_at_the_knots() {
let x = [0.0, 1.0, 2.5, 4.0, 7.0];
let y = [1.0, 3.0, -2.0, 0.5, 4.0];
let y2 = natural_second_derivatives(&x, &y);
for i in 0..x.len() {
let got = splint(&x, &y, &y2, x[i]);
assert!((got - y[i]).abs() < 1e-12, "knot {i}: {got} vs {}", y[i]);
}
}
#[test]
fn end_second_derivatives_are_zero() {
let x = [0.0, 1.0, 2.0, 3.0, 4.0];
let y = [0.0, 1.0, 8.0, 27.0, 64.0];
let y2 = natural_second_derivatives(&x, &y);
assert_eq!(y2[0], 0.0);
assert_eq!(y2[y2.len() - 1], 0.0);
}
#[test]
fn reproduces_a_cubic_between_the_ends() {
let x: Vec<f64> = (0..41).map(|i| f64::from(i) * 0.25).collect();
let y: Vec<f64> = x.iter().map(|v| v * v * v - 2.0 * v).collect();
let y2 = natural_second_derivatives(&x, &y);
for q in [3.1_f64, 4.4, 5.7, 6.3] {
let want = q * q * q - 2.0 * q;
let got = splint(&x, &y, &y2, q);
assert!((got - want).abs() < 1e-6, "at {q}: {got} vs {want}");
}
}
#[test]
fn is_smoother_than_linear_interpolation_on_a_curved_function() {
let x: Vec<f64> = (0..20).map(|i| f64::from(i) * 0.5).collect();
let y: Vec<f64> = x.iter().map(|v| v.sin()).collect();
let y2 = natural_second_derivatives(&x, &y);
let mut lin_err = 0.0_f64;
let mut spl_err = 0.0_f64;
for k in 0..200 {
let q = 1.0 + f64::from(k) * 0.03;
let want = q.sin();
let hi = x.partition_point(|&v| v < q).clamp(1, x.len() - 1);
let lo = hi - 1;
let t = (q - x[lo]) / (x[hi] - x[lo]);
lin_err = lin_err.max((y[lo] + t * (y[hi] - y[lo]) - want).abs());
spl_err = spl_err.max((splint(&x, &y, &y2, q) - want).abs());
}
assert!(
spl_err < lin_err / 20.0,
"spline {spl_err:.2e} should beat linear {lin_err:.2e} by a wide margin"
);
}
#[test]
fn short_tables_degenerate_to_linear() {
assert_eq!(natural_second_derivatives(&[1.0], &[5.0]), vec![0.0]);
assert_eq!(
natural_second_derivatives(&[1.0, 2.0], &[5.0, 7.0]),
vec![0.0, 0.0]
);
}
}
pub(crate) fn natural_second_derivatives_dedup(x: &[f64], y: &[f64]) -> Vec<f64> {
let mut keep: Vec<usize> = Vec::with_capacity(x.len());
for (i, &xi) in x.iter().enumerate() {
match keep.last() {
Some(&prev) if xi <= x[prev] => {}
_ => keep.push(i),
}
}
if keep.len() == x.len() {
return natural_second_derivatives(x, y);
}
let sub_x: Vec<f64> = keep.iter().map(|&i| x[i]).collect();
let sub_y: Vec<f64> = keep.iter().map(|&i| y[i]).collect();
let sub_y2 = natural_second_derivatives(&sub_x, &sub_y);
let mut y2 = vec![0.0_f64; x.len()];
let mut k = 0usize;
for (i, slot) in y2.iter_mut().enumerate() {
if k + 1 < keep.len() && keep[k + 1] <= i {
k += 1;
}
*slot = sub_y2[k];
}
y2
}
#[cfg(test)]
mod dedup_tests {
use super::*;
#[test]
fn passes_through_when_already_strictly_increasing() {
let x = [0.0, 1.0, 2.0, 3.0];
let y = [0.0, 1.0, 4.0, 9.0];
let y2 = natural_second_derivatives_dedup(&x, &y);
assert_eq!(y2, natural_second_derivatives(&x, &y));
}
#[test]
fn tolerates_a_repeated_abscissa() {
let x = [0.0, 1.0, 1.0, 2.0, 3.0];
let y = [0.0, 1.0, 5.0, 4.0, 9.0];
let y2 = natural_second_derivatives_dedup(&x, &y);
assert_eq!(y2.len(), x.len());
assert!(
y2.iter().all(|v| v.is_finite()),
"coefficients must be finite: {y2:?}"
);
}
#[test]
fn coefficients_match_the_deduplicated_fit() {
let x = [0.0, 1.0, 1.0, 2.0, 3.0, 4.0];
let y = [0.0, 1.0, 9.0, 4.0, 9.0, 16.0];
let y2 = natural_second_derivatives_dedup(&x, &y);
let ref_y2 =
natural_second_derivatives(&[0.0, 1.0, 2.0, 3.0, 4.0], &[0.0, 1.0, 4.0, 9.0, 16.0]);
assert_eq!(y2[0], ref_y2[0]);
assert_eq!(y2[1], ref_y2[1]);
assert_eq!(y2[2], ref_y2[1]);
assert_eq!(y2[3], ref_y2[2]);
assert_eq!(y2[5], ref_y2[4]);
}
#[test]
fn mismatched_lengths_use_the_shorter() {
let y2 = natural_second_derivatives_dedup(&[0.0, 1.0, 2.0], &[0.0, 1.0]);
assert!(y2.iter().all(|v| v.is_finite()));
}
}