use crate::errors::QlResult;
use crate::fail;
use crate::math::interpolations::Interpolation;
use crate::types::{Real, Size};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum CubicDerivativeApprox {
Parabolic,
Kruger,
FritschButland,
Harmonic,
Akima,
Spline,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum CubicBoundaryCondition {
SecondDerivative,
FirstDerivative,
NotAKnot,
Lagrange,
}
#[derive(Clone, Copy, Debug)]
struct CubicConfig {
da: CubicDerivativeApprox,
monotonic: bool,
left_cond: CubicBoundaryCondition,
left_value: Real,
right_cond: CubicBoundaryCondition,
right_value: Real,
}
impl CubicConfig {
fn defaults(da: CubicDerivativeApprox) -> Self {
CubicConfig {
da,
monotonic: false,
left_cond: CubicBoundaryCondition::SecondDerivative,
left_value: 0.0,
right_cond: CubicBoundaryCondition::SecondDerivative,
right_value: 0.0,
}
}
}
pub struct CubicInterpolation {
x: Vec<Real>,
y: Vec<Real>,
a: Vec<Real>,
b: Vec<Real>,
c: Vec<Real>,
primitive_const: Vec<Real>,
config: CubicConfig,
allow_extrapolation: bool,
}
impl CubicInterpolation {
pub fn new(x: Vec<Real>, y: Vec<Real>, da: CubicDerivativeApprox) -> QlResult<Self> {
Self::build(x, y, CubicConfig::defaults(da))
}
fn build(x: Vec<Real>, y: Vec<Real>, config: CubicConfig) -> QlResult<Self> {
if x.len() != y.len() {
fail!(
"x and y must have equal length ({} vs {})",
x.len(),
y.len()
);
}
if x.len() < 2 {
fail!(
"cubic interpolation needs at least 2 points, got {}",
x.len()
);
}
for &xi in &x {
if !xi.is_finite() {
fail!("x values must be finite, got {xi}");
}
}
for &yi in &y {
if !yi.is_finite() {
fail!("y values must be finite, got {yi}");
}
}
for w in x.windows(2) {
if w[1] <= w[0] {
fail!("x values must be strictly increasing");
}
}
if config.da == CubicDerivativeApprox::Akima && x.len() < 4 {
fail!(
"Akima approximation requires at least 4 points, got {}",
x.len()
);
}
let n = x.len();
let mut dx = vec![0.0; n - 1];
let mut s = vec![0.0; n - 1];
for i in 0..n - 1 {
dx[i] = x[i + 1] - x[i];
s[i] = (y[i + 1] - y[i]) / dx[i];
}
let mut d = if config.da == CubicDerivativeApprox::Spline {
spline_node_derivatives(&config, &x, &y, &dx, &s)?
} else {
node_derivatives(&config, &dx, &s)
};
if config.monotonic {
apply_hyman_filter(&mut d, &dx, &s);
}
let mut a = vec![0.0; n - 1];
let mut b = vec![0.0; n - 1];
let mut c = vec![0.0; n - 1];
for i in 0..n - 1 {
a[i] = d[i];
b[i] = (3.0 * s[i] - d[i + 1] - 2.0 * d[i]) / dx[i];
c[i] = (d[i + 1] + d[i] - 2.0 * s[i]) / (dx[i] * dx[i]);
}
let mut primitive_const = vec![0.0; n - 1];
for i in 1..n - 1 {
primitive_const[i] = primitive_const[i - 1]
+ dx[i - 1]
* (y[i - 1]
+ dx[i - 1]
* (a[i - 1] / 2.0
+ dx[i - 1] * (b[i - 1] / 3.0 + dx[i - 1] * c[i - 1] / 4.0)));
}
Ok(CubicInterpolation {
x,
y,
a,
b,
c,
primitive_const,
config,
allow_extrapolation: false,
})
}
pub fn with_extrapolation(mut self, allow: bool) -> Self {
self.allow_extrapolation = allow;
self
}
pub fn allows_extrapolation(&self) -> bool {
self.allow_extrapolation
}
pub fn with_boundary_conditions(
self,
left_cond: CubicBoundaryCondition,
left_value: Real,
right_cond: CubicBoundaryCondition,
right_value: Real,
) -> QlResult<Self> {
if !left_value.is_finite() || !right_value.is_finite() {
fail!("boundary values must be finite, got left {left_value}, right {right_value}");
}
let config = CubicConfig {
left_cond,
left_value,
right_cond,
right_value,
..self.config
};
let allow_extrapolation = self.allow_extrapolation;
let mut rebuilt = Self::build(self.x, self.y, config)?;
rebuilt.allow_extrapolation = allow_extrapolation;
Ok(rebuilt)
}
pub fn with_monotonicity(self, monotonic: bool) -> QlResult<Self> {
let config = CubicConfig {
monotonic,
..self.config
};
let allow_extrapolation = self.allow_extrapolation;
let mut rebuilt = Self::build(self.x, self.y, config)?;
rebuilt.allow_extrapolation = allow_extrapolation;
Ok(rebuilt)
}
pub fn second_derivative(&self, x: Real) -> QlResult<Real> {
self.check_range(x)?;
let j = self.locate(x);
let dx = x - self.x[j];
Ok(2.0 * self.b[j] + 6.0 * self.c[j] * dx)
}
fn locate(&self, x: Real) -> Size {
let n = self.x.len();
if x < self.x[0] {
0
} else if x > self.x[n - 1] {
n - 2
} else {
self.x[..n - 1].partition_point(|&xi| xi <= x) - 1
}
}
fn check_range(&self, x: Real) -> QlResult<()> {
if x.is_nan() {
fail!("interpolation cannot be evaluated at NaN");
}
if !self.allow_extrapolation && !self.is_in_range(x) {
fail!(
"interpolation range is [{}, {}]: extrapolation at {x} not allowed",
self.x_min(),
self.x_max()
);
}
Ok(())
}
}
fn node_derivatives(config: &CubicConfig, dx: &[Real], s: &[Real]) -> Vec<Real> {
let n = dx.len() + 1;
if n == 2 {
return vec![s[0], s[0]];
}
let mut d = vec![0.0; n];
match config.da {
CubicDerivativeApprox::Spline => unreachable!("spline handled by the caller"),
CubicDerivativeApprox::Parabolic => {
for i in 1..n - 1 {
d[i] = (dx[i - 1] * s[i] + dx[i] * s[i - 1]) / (dx[i] + dx[i - 1]);
}
d[0] = ((2.0 * dx[0] + dx[1]) * s[0] - dx[0] * s[1]) / (dx[0] + dx[1]);
d[n - 1] = ((2.0 * dx[n - 2] + dx[n - 3]) * s[n - 2] - dx[n - 2] * s[n - 3])
/ (dx[n - 2] + dx[n - 3]);
}
CubicDerivativeApprox::Kruger => {
for i in 1..n - 1 {
d[i] = if s[i - 1] * s[i] < 0.0 || s[i - 1] == 0.0 || s[i] == 0.0 {
0.0
} else {
2.0 / (1.0 / s[i - 1] + 1.0 / s[i])
};
}
d[0] = (3.0 * s[0] - d[1]) / 2.0;
d[n - 1] = (3.0 * s[n - 2] - d[n - 2]) / 2.0;
}
CubicDerivativeApprox::FritschButland => {
for i in 1..n - 1 {
let s_min = s[i - 1].min(s[i]);
let s_max = s[i - 1].max(s[i]);
d[i] = if s_max + 2.0 * s_min == 0.0 {
if s_min * s_max < 0.0 {
Real::MIN
} else if s_min * s_max == 0.0 {
0.0
} else {
Real::MAX
}
} else {
3.0 * s_min * s_max / (s_max + 2.0 * s_min)
};
}
d[0] = ((2.0 * dx[0] + dx[1]) * s[0] - dx[0] * s[1]) / (dx[0] + dx[1]);
d[n - 1] = ((2.0 * dx[n - 2] + dx[n - 3]) * s[n - 2] - dx[n - 2] * s[n - 3])
/ (dx[n - 2] + dx[n - 3]);
}
CubicDerivativeApprox::Harmonic => {
for i in 1..n - 1 {
let w1 = 2.0 * dx[i] + dx[i - 1];
let w2 = dx[i] + 2.0 * dx[i - 1];
d[i] = if s[i - 1] * s[i] <= 0.0 {
0.0
} else {
(w1 + w2) / (w1 / s[i - 1] + w2 / s[i])
};
}
d[0] = ((2.0 * dx[0] + dx[1]) * s[0] - dx[0] * s[1]) / (dx[1] + dx[0]);
if d[0] * s[0] < 0.0 {
d[0] = 0.0;
} else if s[0] * s[1] < 0.0 && d[0].abs() > (3.0 * s[0]).abs() {
d[0] = 3.0 * s[0];
}
d[n - 1] = ((2.0 * dx[n - 2] + dx[n - 3]) * s[n - 2] - dx[n - 2] * s[n - 3])
/ (dx[n - 3] + dx[n - 2]);
if d[n - 1] * s[n - 2] < 0.0 {
d[n - 1] = 0.0;
} else if s[n - 2] * s[n - 3] < 0.0 && d[n - 1].abs() > (3.0 * s[n - 2]).abs() {
d[n - 1] = 3.0 * s[n - 2];
}
}
#[allow(clippy::float_cmp)]
CubicDerivativeApprox::Akima => {
let w0 = (s[1] - s[0]).abs();
let w0b = (2.0 * s[0] * s[1] - 4.0 * s[0] * s[0] * s[1]).abs();
d[0] = if w0 + w0b == 0.0 {
s[0]
} else {
(w0 * 2.0 * s[0] * s[1] + w0b * s[0]) / (w0 + w0b)
};
let w1 = (s[2] - s[1]).abs();
let w1b = (s[0] - 2.0 * s[0] * s[1]).abs();
d[1] = if w1 + w1b == 0.0 {
s[1]
} else {
(w1 * s[0] + w1b * s[1]) / (w1 + w1b)
};
for i in 2..n - 2 {
#[allow(clippy::if_same_then_else)]
let di = if s[i - 2] == s[i - 1] && s[i] != s[i + 1] {
s[i - 1]
} else if s[i - 2] != s[i - 1] && s[i] == s[i + 1] {
s[i]
} else if s[i] == s[i - 1] {
s[i]
} else if s[i - 2] == s[i - 1] && s[i - 1] != s[i] && s[i] == s[i + 1] {
(s[i - 1] + s[i]) / 2.0
} else {
let wl = (s[i + 1] - s[i]).abs();
let wr = (s[i - 1] - s[i - 2]).abs();
(wl * s[i - 1] + wr * s[i]) / (wl + wr)
};
d[i] = di;
}
let wn2 = (2.0 * s[n - 2] * s[n - 3] - s[n - 2]).abs();
let wn2b = (s[n - 3] - s[n - 4]).abs();
d[n - 2] = if wn2 + wn2b == 0.0 {
s[n - 3]
} else {
(wn2 * s[n - 3] + wn2b * s[n - 2]) / (wn2 + wn2b)
};
let wn1 = (4.0 * s[n - 2] * s[n - 2] * s[n - 3] - 2.0 * s[n - 2] * s[n - 3]).abs();
let wn1b = (s[n - 2] - s[n - 3]).abs();
d[n - 1] = if wn1 + wn1b == 0.0 {
s[n - 2]
} else {
(wn1 * s[n - 2] + wn1b * 2.0 * s[n - 2] * s[n - 3]) / (wn1 + wn1b)
};
}
}
d
}
fn apply_hyman_filter(d: &mut [Real], dx: &[Real], s: &[Real]) {
let n = d.len();
for i in 0..n {
let correction = if i == 0 {
if d[i] * s[0] > 0.0 {
d[i].signum() * d[i].abs().min((3.0 * s[0]).abs())
} else {
0.0
}
} else if i == n - 1 {
if d[i] * s[n - 2] > 0.0 {
d[i].signum() * d[i].abs().min((3.0 * s[n - 2]).abs())
} else {
0.0
}
} else {
let pm = (s[i - 1] * dx[i] + s[i] * dx[i - 1]) / (dx[i - 1] + dx[i]);
let mut m = 3.0 * s[i - 1].abs().min(s[i].abs()).min(pm.abs());
if i > 1 && (s[i - 1] - s[i - 2]) * (s[i] - s[i - 1]) > 0.0 {
let pd = (s[i - 1] * (2.0 * dx[i - 1] + dx[i - 2]) - s[i - 2] * dx[i - 1])
/ (dx[i - 2] + dx[i - 1]);
if pm * pd > 0.0 && pm * (s[i - 1] - s[i - 2]) > 0.0 {
m = m.max(1.5 * pm.abs().min(pd.abs()));
}
}
if i < n - 2 && (s[i] - s[i - 1]) * (s[i + 1] - s[i]) > 0.0 {
let pu =
(s[i] * (2.0 * dx[i] + dx[i + 1]) - s[i + 1] * dx[i]) / (dx[i] + dx[i + 1]);
if pm * pu > 0.0 && -pm * (s[i] - s[i - 1]) > 0.0 {
m = m.max(1.5 * pm.abs().min(pu.abs()));
}
}
if d[i] * pm > 0.0 {
d[i].signum() * d[i].abs().min(m)
} else {
0.0
}
};
d[i] = correction;
}
}
fn spline_node_derivatives(
config: &CubicConfig,
x: &[Real],
y: &[Real],
dx: &[Real],
s: &[Real],
) -> QlResult<Vec<Real>> {
let n = dx.len() + 1;
let left_nak = config.left_cond == CubicBoundaryCondition::NotAKnot;
let right_nak = config.right_cond == CubicBoundaryCondition::NotAKnot;
if (left_nak || right_nak) && n < 3 {
fail!("the not-a-knot spline boundary condition requires at least 3 points, got {n}");
}
if left_nak && right_nak && n < 4 {
fail!("two not-a-knot spline boundary conditions require at least 4 points, got {n}");
}
if (config.left_cond == CubicBoundaryCondition::Lagrange
|| config.right_cond == CubicBoundaryCondition::Lagrange)
&& n < 4
{
fail!("the Lagrange spline boundary condition requires at least 4 points, got {n}");
}
let mut lower = vec![0.0; n - 1];
let mut diag = vec![0.0; n];
let mut upper = vec![0.0; n - 1];
let mut rhs = vec![0.0; n];
for i in 1..n - 1 {
lower[i - 1] = dx[i];
diag[i] = 2.0 * (dx[i] + dx[i - 1]);
upper[i] = dx[i - 1];
rhs[i] = 3.0 * (dx[i] * s[i - 1] + dx[i - 1] * s[i]);
}
match config.left_cond {
CubicBoundaryCondition::SecondDerivative => {
diag[0] = 2.0;
upper[0] = 1.0;
rhs[0] = 3.0 * s[0] - config.left_value * dx[0] / 2.0;
}
CubicBoundaryCondition::FirstDerivative => {
diag[0] = 1.0;
upper[0] = 0.0;
rhs[0] = config.left_value;
}
CubicBoundaryCondition::NotAKnot => {
diag[0] = dx[1] * (dx[1] + dx[0]);
upper[0] = (dx[0] + dx[1]) * (dx[0] + dx[1]);
rhs[0] = s[0] * dx[1] * (2.0 * dx[1] + 3.0 * dx[0]) + s[1] * dx[0] * dx[0];
}
CubicBoundaryCondition::Lagrange => {
diag[0] = 1.0;
upper[0] = 0.0;
rhs[0] = cubic_interpolating_polynomial_derivative(
[x[0], x[1], x[2], x[3]],
[y[0], y[1], y[2], y[3]],
x[0],
);
}
}
match config.right_cond {
CubicBoundaryCondition::SecondDerivative => {
lower[n - 2] = 1.0;
diag[n - 1] = 2.0;
rhs[n - 1] = 3.0 * s[n - 2] + config.right_value * dx[n - 2] / 2.0;
}
CubicBoundaryCondition::FirstDerivative => {
lower[n - 2] = 0.0;
diag[n - 1] = 1.0;
rhs[n - 1] = config.right_value;
}
CubicBoundaryCondition::NotAKnot => {
lower[n - 2] = -(dx[n - 2] + dx[n - 3]) * (dx[n - 2] + dx[n - 3]);
diag[n - 1] = -dx[n - 3] * (dx[n - 3] + dx[n - 2]);
rhs[n - 1] = -s[n - 3] * dx[n - 2] * dx[n - 2]
- s[n - 2] * dx[n - 3] * (3.0 * dx[n - 2] + 2.0 * dx[n - 3]);
}
CubicBoundaryCondition::Lagrange => {
lower[n - 2] = 0.0;
diag[n - 1] = 1.0;
rhs[n - 1] = cubic_interpolating_polynomial_derivative(
[x[n - 4], x[n - 3], x[n - 2], x[n - 1]],
[y[n - 4], y[n - 3], y[n - 2], y[n - 1]],
x[n - 1],
);
}
}
solve_tridiagonal(&lower, &diag, &upper, &rhs)
}
fn cubic_interpolating_polynomial_derivative(xs: [Real; 4], ys: [Real; 4], x: Real) -> Real {
let [a, b, c, d] = xs;
let [u, v, w, z] = ys;
let num = (((a - c) * (b - c) * (c - x) * z - (a - d) * (b - d) * (d - x) * w)
* (a - x + b - x)
+ ((a - c) * (b - c) * z - (a - d) * (b - d) * w) * (a - x) * (b - x))
* (a - b)
+ ((a - c) * (a - d) * v - (b - c) * (b - d) * u) * (c - d) * (c - x) * (d - x)
+ ((a - c) * (a - d) * (a - x) * v - (b - c) * (b - d) * (b - x) * u)
* (c - x + d - x)
* (c - d);
let den = (a - b) * (a - c) * (a - d) * (b - c) * (b - d) * (c - d);
-num / den
}
fn solve_tridiagonal(
lower: &[Real],
diag: &[Real],
upper: &[Real],
rhs: &[Real],
) -> QlResult<Vec<Real>> {
let n = diag.len();
let mut cp = vec![0.0; n];
let mut dp = vec![0.0; n];
if diag[0] == 0.0 {
fail!("tridiagonal system is singular (zero pivot at row 0)");
}
cp[0] = upper[0] / diag[0];
dp[0] = rhs[0] / diag[0];
for i in 1..n {
let pivot = diag[i] - lower[i - 1] * cp[i - 1];
if pivot == 0.0 {
fail!("tridiagonal system is singular (zero pivot at row {i})");
}
if i < n - 1 {
cp[i] = upper[i] / pivot;
}
dp[i] = (rhs[i] - lower[i - 1] * dp[i - 1]) / pivot;
}
let mut x = vec![0.0; n];
x[n - 1] = dp[n - 1];
for i in (0..n - 1).rev() {
x[i] = dp[i] - cp[i] * x[i + 1];
}
Ok(x)
}
impl Interpolation for CubicInterpolation {
fn value(&self, x: Real) -> QlResult<Real> {
self.check_range(x)?;
let j = self.locate(x);
let dx = x - self.x[j];
Ok(self.y[j] + dx * (self.a[j] + dx * (self.b[j] + dx * self.c[j])))
}
fn derivative(&self, x: Real) -> QlResult<Real> {
self.check_range(x)?;
let j = self.locate(x);
let dx = x - self.x[j];
Ok(self.a[j] + (2.0 * self.b[j] + 3.0 * self.c[j] * dx) * dx)
}
fn primitive(&self, x: Real) -> QlResult<Real> {
self.check_range(x)?;
let j = self.locate(x);
let dx = x - self.x[j];
Ok(self.primitive_const[j]
+ dx * (self.y[j]
+ dx * (self.a[j] / 2.0 + dx * (self.b[j] / 3.0 + dx * self.c[j] / 4.0))))
}
fn x_min(&self) -> Real {
self.x[0]
}
fn x_max(&self) -> Real {
self.x[self.x.len() - 1]
}
fn is_in_range(&self, x: Real) -> bool {
x >= self.x_min() && x <= self.x_max()
}
}
pub struct ParabolicInterpolation;
impl ParabolicInterpolation {
#[allow(clippy::new_ret_no_self)]
pub fn new(x: Vec<Real>, y: Vec<Real>) -> QlResult<CubicInterpolation> {
CubicInterpolation::new(x, y, CubicDerivativeApprox::Parabolic)
}
}
pub struct KrugerCubicInterpolation;
impl KrugerCubicInterpolation {
#[allow(clippy::new_ret_no_self)]
pub fn new(x: Vec<Real>, y: Vec<Real>) -> QlResult<CubicInterpolation> {
CubicInterpolation::new(x, y, CubicDerivativeApprox::Kruger)
}
}
pub struct HarmonicCubicInterpolation;
impl HarmonicCubicInterpolation {
#[allow(clippy::new_ret_no_self)]
pub fn new(x: Vec<Real>, y: Vec<Real>) -> QlResult<CubicInterpolation> {
CubicInterpolation::new(x, y, CubicDerivativeApprox::Harmonic)
}
}
pub struct AkimaCubicInterpolation;
impl AkimaCubicInterpolation {
#[allow(clippy::new_ret_no_self)]
pub fn new(x: Vec<Real>, y: Vec<Real>) -> QlResult<CubicInterpolation> {
CubicInterpolation::new(x, y, CubicDerivativeApprox::Akima)
}
}
pub struct CubicNaturalSpline;
impl CubicNaturalSpline {
#[allow(clippy::new_ret_no_self)]
pub fn new(x: Vec<Real>, y: Vec<Real>) -> QlResult<CubicInterpolation> {
CubicInterpolation::new(x, y, CubicDerivativeApprox::Spline)
}
}
fn monotonic(
da: CubicDerivativeApprox,
x: Vec<Real>,
y: Vec<Real>,
) -> QlResult<CubicInterpolation> {
CubicInterpolation::build(
x,
y,
CubicConfig {
monotonic: true,
..CubicConfig::defaults(da)
},
)
}
pub struct FritschButlandCubic;
impl FritschButlandCubic {
#[allow(clippy::new_ret_no_self)]
pub fn new(x: Vec<Real>, y: Vec<Real>) -> QlResult<CubicInterpolation> {
monotonic(CubicDerivativeApprox::FritschButland, x, y)
}
}
pub struct MonotonicParabolicInterpolation;
impl MonotonicParabolicInterpolation {
#[allow(clippy::new_ret_no_self)]
pub fn new(x: Vec<Real>, y: Vec<Real>) -> QlResult<CubicInterpolation> {
monotonic(CubicDerivativeApprox::Parabolic, x, y)
}
}
pub struct MonotonicCubicNaturalSpline;
impl MonotonicCubicNaturalSpline {
#[allow(clippy::new_ret_no_self)]
pub fn new(x: Vec<Real>, y: Vec<Real>) -> QlResult<CubicInterpolation> {
monotonic(CubicDerivativeApprox::Spline, x, y)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn q(x: Real) -> Real {
1.0 + 2.0 * x + 3.0 * x * x
}
fn cubic(x: Real) -> Real {
1.0 + 2.0 * x + 3.0 * x * x + 4.0 * x * x * x
}
fn parabolic_sample() -> CubicInterpolation {
let x = vec![0.0, 1.0, 3.0, 4.0];
let y = x.iter().map(|&xi| q(xi)).collect();
ParabolicInterpolation::new(x, y).unwrap()
}
fn assert_close(got: Real, expected: Real) {
let tol = 1e-11 * (1.0 + expected.abs());
assert!(
(got - expected).abs() <= tol,
"got {got}, expected {expected}"
);
}
#[test]
fn parabolic_reproduces_quadratic() {
let f = parabolic_sample();
for &x in &[0.0, 0.5, 1.0, 2.0, 3.0, 3.7, 4.0_f64] {
assert_close(f.value(x).unwrap(), q(x));
assert_close(f.derivative(x).unwrap(), 2.0 + 6.0 * x);
assert_close(f.second_derivative(x).unwrap(), 6.0);
assert_close(f.primitive(x).unwrap(), x + x * x + x * x * x);
}
}
#[test]
fn two_points_is_linear() {
let f = ParabolicInterpolation::new(vec![0.0, 2.0], vec![1.0, 5.0]).unwrap();
assert_close(f.value(1.0).unwrap(), 3.0);
assert_close(f.derivative(1.5).unwrap(), 2.0);
assert_close(f.second_derivative(0.7).unwrap(), 0.0);
}
#[test]
fn kruger_node_derivatives() {
let f = KrugerCubicInterpolation::new(vec![0.0, 1.0, 2.0], vec![0.0, 1.0, 0.0]).unwrap();
assert_close(f.value(1.0).unwrap(), 1.0);
assert_close(f.derivative(1.0).unwrap(), 0.0);
let g = KrugerCubicInterpolation::new(vec![0.0, 1.0, 2.0], vec![0.0, 0.5, 2.0]).unwrap();
assert_close(g.derivative(1.0).unwrap(), 0.75);
}
#[test]
fn kruger_signed_zero_slopes_do_not_produce_nan() {
let f = KrugerCubicInterpolation::new(vec![0.0, 1.0, 2.0], vec![0.0, -0.0, 0.0]).unwrap();
for x in [0.0, 0.3, 1.0, 1.5, 2.0] {
let v = f.value(x).unwrap();
assert!(v.is_finite(), "value({x}) = {v}");
assert_close(v, 0.0);
assert!(
f.derivative(x).unwrap().is_finite(),
"derivative({x}) is NaN"
);
}
}
#[test]
fn harmonic_and_fritsch_butland_node_derivatives() {
let h = HarmonicCubicInterpolation::new(vec![0.0, 1.0, 3.0], vec![0.0, 1.0, 4.0]).unwrap();
assert_close(h.value(1.0).unwrap(), 1.0);
assert_close(h.derivative(1.0).unwrap(), 27.0 / 23.0);
let fb = CubicInterpolation::new(
vec![0.0, 1.0, 3.0],
vec![0.0, 1.0, 4.0],
CubicDerivativeApprox::FritschButland,
)
.unwrap();
assert_close(fb.value(1.0).unwrap(), 1.0);
assert_close(fb.derivative(1.0).unwrap(), 9.0 / 7.0);
}
#[test]
fn harmonic_zeroes_derivative_at_sign_change() {
let h = HarmonicCubicInterpolation::new(vec![0.0, 1.0, 2.0], vec![0.0, 1.0, 0.0]).unwrap();
assert_close(h.value(1.0).unwrap(), 1.0);
assert_close(h.derivative(1.0).unwrap(), 0.0);
}
#[test]
fn akima_reproduces_reference_node_derivatives() {
let x = vec![0.0, 1.0, 2.0, 4.0, 7.0];
let y = vec![0.0, 1.0, 0.0, 2.0, 3.0];
let f = AkimaCubicInterpolation::new(x.clone(), y.clone()).unwrap();
let oracle = [-0.5, -0.2, 0.5, 3.0 / 7.0, 7.0 / 12.0];
for i in 0..x.len() {
assert_close(f.value(x[i]).unwrap(), y[i]);
assert_close(f.derivative(x[i]).unwrap(), oracle[i]);
}
}
#[test]
fn akima_interior_special_case_branches() {
let cfg = CubicConfig::defaults(CubicDerivativeApprox::Akima);
let dx = vec![1.0; 6];
let s = vec![1.0, 2.0, 3.0, 3.0, 1.0, 0.5];
let d = node_derivatives(&cfg, &dx, &s);
let oracle = [1.6, 1.75, 3.0, 3.0, 3.0, 0.6, 1.0];
for i in 0..d.len() {
assert_close(d[i], oracle[i]);
}
let s2 = vec![1.0, 1.0, 1.0, 2.0, 2.0, -1.0];
let d2 = node_derivatives(&cfg, &dx, &s2);
assert_close(d2[3], 1.5);
}
#[test]
fn akima_handles_zero_denominator_endpoints() {
let flat = AkimaCubicInterpolation::new(vec![0.0, 1.0, 2.0, 3.0], vec![0.0; 4]).unwrap();
for &x in &[0.0, 0.5, 1.7, 3.0_f64] {
assert_close(flat.value(x).unwrap(), 0.0);
assert_close(flat.derivative(x).unwrap(), 0.0);
}
let line = AkimaCubicInterpolation::new(vec![0.0, 1.0, 2.0, 3.0], vec![0.0, 0.5, 1.0, 1.5])
.unwrap();
for &x in &[0.0, 0.75, 2.4, 3.0_f64] {
assert_close(line.value(x).unwrap(), 0.5 * x);
assert_close(line.derivative(x).unwrap(), 0.5);
}
}
#[test]
fn akima_requires_at_least_4_points() {
assert!(AkimaCubicInterpolation::new(vec![0.0, 1.0, 2.0], vec![0.0, 1.0, 0.0]).is_err());
assert!(
AkimaCubicInterpolation::new(vec![0.0, 1.0, 2.0, 3.0], vec![0.0, 1.0, 0.0, 2.0])
.is_ok()
);
}
#[test]
fn domain_range_and_extrapolation() {
let f = parabolic_sample();
assert_eq!(f.x_min(), 0.0);
assert_eq!(f.x_max(), 4.0);
assert!(f.is_in_range(2.0));
assert!(!f.is_in_range(-0.1));
assert!(f.value(-1.0).is_err());
assert!(f.value(5.0).is_err());
let g = parabolic_sample().with_extrapolation(true);
assert!(g.allows_extrapolation());
assert_close(g.value(5.0).unwrap(), q(5.0));
assert_close(g.value(-1.0).unwrap(), q(-1.0));
}
#[test]
fn rejects_nan_eval_and_bad_construction() {
let f = parabolic_sample().with_extrapolation(true);
assert!(f.value(Real::NAN).is_err());
assert!(f.derivative(Real::NAN).is_err());
assert!(f.second_derivative(Real::NAN).is_err());
let da = CubicDerivativeApprox::Parabolic;
assert!(CubicInterpolation::new(vec![0.0], vec![1.0], da).is_err());
assert!(CubicInterpolation::new(vec![0.0, 1.0], vec![1.0], da).is_err());
assert!(CubicInterpolation::new(vec![1.0, 1.0], vec![1.0, 2.0], da).is_err());
assert!(CubicInterpolation::new(vec![0.0, Real::NAN], vec![1.0, 2.0], da).is_err());
}
#[test]
fn natural_spline_matches_reference() {
let f =
CubicNaturalSpline::new(vec![0.0, 1.0, 2.0, 3.0], vec![0.0, 1.0, 0.0, 1.0]).unwrap();
for (x, y) in [(0.0, 0.0), (1.0, 1.0), (2.0, 0.0), (3.0, 1.0)] {
assert_close(f.value(x).unwrap(), y);
}
assert_close(f.value(0.5).unwrap(), 0.75);
assert_close(f.value(1.5).unwrap(), 0.5);
assert_close(f.value(2.5).unwrap(), 0.25);
assert_close(f.derivative(0.0).unwrap(), 5.0 / 3.0);
}
#[test]
fn natural_spline_is_c2_with_zero_end_curvature() {
let f =
CubicNaturalSpline::new(vec![0.0, 1.0, 3.0, 4.0], vec![0.0, 2.0, 1.0, 3.0]).unwrap();
assert_close(f.second_derivative(0.0).unwrap(), 0.0);
assert_close(f.second_derivative(4.0).unwrap(), 0.0);
let below = f.second_derivative(1.0 - 1e-7).unwrap();
let above = f.second_derivative(1.0 + 1e-7).unwrap();
assert!(
(below - above).abs() < 1e-5,
"2nd derivative jumps: {below} vs {above}"
);
}
#[test]
fn spline_reproduces_linear_data() {
let f =
CubicNaturalSpline::new(vec![0.0, 1.0, 2.0, 4.0], vec![1.0, 3.0, 5.0, 9.0]).unwrap();
for &x in &[0.3, 1.7, 3.5_f64] {
assert_close(f.value(x).unwrap(), 1.0 + 2.0 * x);
assert_close(f.derivative(x).unwrap(), 2.0);
}
}
#[test]
fn notaknot_spline_reproduces_cubic() {
let x = vec![0.0, 1.0, 2.5, 4.0, 6.0];
let y: Vec<Real> = x.iter().map(|&xi| cubic(xi)).collect();
let f = CubicNaturalSpline::new(x, y)
.unwrap()
.with_boundary_conditions(
CubicBoundaryCondition::NotAKnot,
0.0,
CubicBoundaryCondition::NotAKnot,
0.0,
)
.unwrap();
for &xx in &[0.0, 0.7, 2.5, 3.3, 5.1, 6.0_f64] {
assert_close(f.value(xx).unwrap(), cubic(xx));
assert_close(f.derivative(xx).unwrap(), 2.0 + 6.0 * xx + 12.0 * xx * xx);
}
}
#[test]
fn lagrange_spline_reproduces_cubic() {
let x = vec![0.0, 1.0, 2.5, 4.0, 6.0];
let y: Vec<Real> = x.iter().map(|&xi| cubic(xi)).collect();
let f = CubicNaturalSpline::new(x, y)
.unwrap()
.with_boundary_conditions(
CubicBoundaryCondition::Lagrange,
0.0,
CubicBoundaryCondition::Lagrange,
0.0,
)
.unwrap();
for &xx in &[0.0, 0.7, 2.5, 3.3, 5.1, 6.0_f64] {
assert_close(f.value(xx).unwrap(), cubic(xx));
assert_close(f.derivative(xx).unwrap(), 2.0 + 6.0 * xx + 12.0 * xx * xx);
}
}
#[test]
fn cubic_interpolating_polynomial_derivative_matches_cubic() {
let (a, b, c, d) = (0.0, 1.0, 2.5, 4.0);
let (u, v, w, z) = (cubic(a), cubic(b), cubic(c), cubic(d));
for &xx in &[0.0, 1.3, 2.5, 4.0, -0.5_f64] {
assert_close(
cubic_interpolating_polynomial_derivative([a, b, c, d], [u, v, w, z], xx),
2.0 + 6.0 * xx + 12.0 * xx * xx,
);
}
}
#[test]
fn lagrange_requires_at_least_4_points() {
let three = || CubicNaturalSpline::new(vec![0.0, 1.0, 2.0], vec![0.0, 1.0, 0.5]).unwrap();
assert!(
three()
.with_boundary_conditions(
CubicBoundaryCondition::Lagrange,
0.0,
CubicBoundaryCondition::SecondDerivative,
0.0,
)
.is_err()
);
}
#[test]
fn notaknot_point_count_guards() {
let three = || CubicNaturalSpline::new(vec![0.0, 1.0, 2.0], vec![0.0, 1.0, 0.5]).unwrap();
assert!(
three()
.with_boundary_conditions(
CubicBoundaryCondition::NotAKnot,
0.0,
CubicBoundaryCondition::NotAKnot,
0.0,
)
.is_err()
);
let mixed = three()
.with_boundary_conditions(
CubicBoundaryCondition::NotAKnot,
0.0,
CubicBoundaryCondition::SecondDerivative,
0.0,
)
.unwrap();
assert_close(mixed.value(0.0).unwrap(), 0.0);
assert_close(mixed.value(1.0).unwrap(), 1.0);
assert_close(mixed.value(2.0).unwrap(), 0.5);
let two = || {
CubicInterpolation::new(
vec![0.0, 1.0],
vec![0.0, 1.0],
CubicDerivativeApprox::Spline,
)
.unwrap()
};
assert!(
two()
.with_boundary_conditions(
CubicBoundaryCondition::NotAKnot,
0.0,
CubicBoundaryCondition::SecondDerivative,
0.0,
)
.is_err()
);
}
#[test]
fn solve_tridiagonal_known_system_and_singular() {
let x = solve_tridiagonal(&[1.0, 1.0], &[2.0, 4.0, 2.0], &[1.0, 1.0], &[3.0, 0.0, 3.0])
.unwrap();
assert_close(x[0], 2.0);
assert_close(x[1], -1.0);
assert_close(x[2], 2.0);
assert!(solve_tridiagonal(&[1.0], &[0.0, 1.0], &[1.0], &[1.0, 1.0]).is_err());
assert!(solve_tridiagonal(&[1.0], &[1.0, 1.0], &[1.0], &[1.0, 1.0]).is_err());
}
#[test]
fn clamped_spline_matches_end_slopes_and_reference() {
let f = CubicNaturalSpline::new(vec![0.0, 1.0, 2.0, 3.0], vec![0.0, 1.0, 0.0, 1.0])
.unwrap()
.with_boundary_conditions(
CubicBoundaryCondition::FirstDerivative,
1.0,
CubicBoundaryCondition::FirstDerivative,
-1.0,
)
.unwrap();
for (x, y) in [(0.0, 0.0), (1.0, 1.0), (2.0, 0.0), (3.0, 1.0)] {
assert_close(f.value(x).unwrap(), y);
}
assert_close(f.derivative(0.0).unwrap(), 1.0);
assert_close(f.derivative(3.0).unwrap(), -1.0);
assert_close(f.value(0.5).unwrap(), 2.0 / 3.0);
}
#[test]
fn clamped_spline_two_points() {
let f = CubicInterpolation::new(
vec![0.0, 2.0],
vec![1.0, 5.0],
CubicDerivativeApprox::Spline,
)
.unwrap()
.with_boundary_conditions(
CubicBoundaryCondition::FirstDerivative,
0.5,
CubicBoundaryCondition::FirstDerivative,
3.0,
)
.unwrap();
assert_close(f.derivative(0.0).unwrap(), 0.5);
assert_close(f.derivative(2.0).unwrap(), 3.0);
assert_close(f.value(0.0).unwrap(), 1.0);
assert_close(f.value(2.0).unwrap(), 5.0);
}
#[test]
fn boundary_conditions_reject_non_finite_values() {
let spline =
|| CubicNaturalSpline::new(vec![0.0, 1.0, 2.0, 3.0], vec![0.0, 1.0, 0.0, 1.0]).unwrap();
assert!(
spline()
.with_boundary_conditions(
CubicBoundaryCondition::FirstDerivative,
Real::NAN,
CubicBoundaryCondition::FirstDerivative,
-1.0,
)
.is_err()
);
assert!(
spline()
.with_boundary_conditions(
CubicBoundaryCondition::SecondDerivative,
0.0,
CubicBoundaryCondition::SecondDerivative,
Real::INFINITY,
)
.is_err()
);
}
#[test]
fn boundary_conditions_are_noop_for_local_schemes() {
let base =
ParabolicInterpolation::new(vec![0.0, 1.0, 3.0, 4.0], vec![1.0, 6.0, 34.0, 57.0])
.unwrap();
let before = base.value(2.0).unwrap();
let rebuilt =
ParabolicInterpolation::new(vec![0.0, 1.0, 3.0, 4.0], vec![1.0, 6.0, 34.0, 57.0])
.unwrap()
.with_boundary_conditions(
CubicBoundaryCondition::FirstDerivative,
99.0,
CubicBoundaryCondition::SecondDerivative,
-7.0,
)
.unwrap();
assert_close(rebuilt.value(2.0).unwrap(), before);
}
#[test]
fn hyman_clamps_fritsch_butland_peak_derivative() {
let f = FritschButlandCubic::new(vec![0.0, 1.0, 2.0], vec![0.0, 1.0, 0.0]).unwrap();
assert_close(f.value(1.0).unwrap(), 1.0);
assert_close(f.derivative(1.0).unwrap(), 0.0);
}
#[test]
fn monotonic_spline_removes_overshoot() {
let x = vec![0.0, 1.0, 2.0, 3.0, 4.0];
let y = vec![0.0, 0.0, 0.0, 1.0, 1.0];
let raw = CubicNaturalSpline::new(x.clone(), y.clone()).unwrap();
let mono = MonotonicCubicNaturalSpline::new(x, y).unwrap();
let (mut raw_dips, mut prev_raw, mut prev_mono) =
(false, raw.value(0.0).unwrap(), mono.value(0.0).unwrap());
for k in 1..=200 {
let xx = k as Real / 50.0;
let (r, m) = (raw.value(xx).unwrap(), mono.value(xx).unwrap());
if r < prev_raw - 1e-9 {
raw_dips = true;
}
assert!(
m >= prev_mono - 1e-12,
"monotone spline decreased at x={xx}"
);
prev_raw = r;
prev_mono = m;
}
assert!(raw_dips, "expected the raw natural spline to overshoot");
}
#[test]
fn monotonicity_filter_is_noop_on_linear_data() {
let f = MonotonicParabolicInterpolation::new(
vec![0.0, 1.0, 2.0, 4.0],
vec![1.0, 3.0, 5.0, 9.0],
)
.unwrap();
for &x in &[0.3, 1.7, 3.5_f64] {
assert_close(f.value(x).unwrap(), 1.0 + 2.0 * x);
assert_close(f.derivative(x).unwrap(), 2.0);
}
}
#[test]
fn with_monotonicity_builder_matches_convenience_type() {
let x = vec![0.0, 1.0, 2.0, 3.0, 4.0];
let y = vec![0.0, 0.0, 0.0, 1.0, 1.0];
let via_builder = CubicNaturalSpline::new(x.clone(), y.clone())
.unwrap()
.with_monotonicity(true)
.unwrap();
let via_factory = MonotonicCubicNaturalSpline::new(x, y).unwrap();
for &xx in &[0.5, 1.5, 2.5, 3.5_f64] {
assert_close(
via_builder.value(xx).unwrap(),
via_factory.value(xx).unwrap(),
);
}
}
#[test]
fn non_restrictive_hyman_filter() {
let x = vec![-2.0, -2.0 / 3.0, 2.0 / 3.0, 2.0];
let y: Vec<Real> = x.iter().map(|&xi| -xi * xi).collect();
let base = || CubicNaturalSpline::new(x.clone(), y.clone()).unwrap();
let not_a_knot = base()
.with_boundary_conditions(
CubicBoundaryCondition::NotAKnot,
0.0,
CubicBoundaryCondition::NotAKnot,
0.0,
)
.unwrap()
.with_monotonicity(true)
.unwrap();
let clamped = base()
.with_boundary_conditions(
CubicBoundaryCondition::FirstDerivative,
4.0,
CubicBoundaryCondition::FirstDerivative,
-4.0,
)
.unwrap()
.with_monotonicity(true)
.unwrap();
let second = base()
.with_boundary_conditions(
CubicBoundaryCondition::SecondDerivative,
-2.0,
CubicBoundaryCondition::SecondDerivative,
-2.0,
)
.unwrap()
.with_monotonicity(true)
.unwrap();
for f in [not_a_knot, clamped, second] {
assert!(f.value(0.0).unwrap().abs() < 1e-14);
}
}
}