use super::Result;
#[cfg(feature="plot")]
use super::plot::plot_base;
const DE_BOOR_SIZE: usize = 6;
#[derive(Debug, Clone)]
pub struct SplineCurve<const K: usize, const N: usize> {
pub t: Vec<f64>, pub c: Vec<f64>, i: Option<usize>, }
impl<const K: usize, const N: usize> SplineCurve<K, N> {
pub fn new(t: Vec<f64>, c: Vec<f64>) -> Self {
assert!(K < DE_BOOR_SIZE, "Spline degree K={} exceeds maximum supported degree {}", K, DE_BOOR_SIZE - 1);
Self { t, c, i: None }
}
pub fn try_new(t: Vec<f64>, c: Vec<f64>) -> std::result::Result<Self, Box<dyn std::error::Error>> {
assert!(K < DE_BOOR_SIZE, "Spline degree K={} exceeds maximum supported degree {}", K, DE_BOOR_SIZE - 1);
let nc = c.len() / N;
if nc < (K+1) {
return Err(format!("Need at least {} coefficients for a degree-{} spline", N*(K+1), K).into());
}
Ok(Self { t, c, i: None })
}
#[cfg(feature="plot")]
pub fn plot(&self, filepath: &str, wxh: (u32, u32)) -> Result<()> {
assert!(N <= 2, "Plotting is only supported for 1D and 2D curves (N=1 or N=2), got N={}", N);
Ok(plot_base(self.clone(), filepath, wxh, None, None, false)?)
}
#[cfg(feature="plot")]
pub fn plot_with_parameter(&self, filepath: &str, wxh: (u32, u32), u:Option<&[f64]>) -> Result<()> {
assert!(N <= 2, "Plotting is only supported for 1D and 2D curves (N=1 or N=2), got N={}", N);
Ok(plot_base(self.clone(), filepath, wxh, u, None, false)?)
}
#[cfg(feature="plot")]
pub fn plot_with_control_points(&self, filepath: &str, wxh: (u32, u32)) -> Result<()> {
assert!(N <= 2, "Plotting is only supported for 1D and 2D curves (N=1 or N=2), got N={}", N);
Ok(plot_base(self.clone(), filepath, wxh, None, None, true)?)
}
#[cfg(feature="plot")]
pub fn plot_with_data(&self, filepath: &str, wxh: (u32, u32), xy: &[f64]) -> Result<()> {
assert!(N <= 2, "Plotting is only supported for 1D and 2D curves (N=1 or N=2), got N={}", N);
Ok(plot_base(self.clone(), filepath, wxh, None, Some(xy), false)?)
}
#[cfg(feature="plot")]
pub fn plot_with_control_points_and_data(&self, filepath: &str, wxh: (u32, u32), xy: &[f64]) -> Result<()> {
assert!(N <= 2, "Plotting is only supported for 1D and 2D curves (N=1 or N=2), got N={}", N);
Ok(plot_base(self.clone(), filepath, wxh, None, Some(xy), true)?)
}
pub fn evaluate(&self, u: &[f64]) -> Result<Vec<f64>> {
let n = self.t.len();
let nc = self.c.len() / N;
if nc < (K+1) {
return Err(format!("Need at least {} coefficients for a degree-{} spline", N*(K+1), K).into());
}
if nc != n-(K+1) {
return Err(format!("Expected {} coefficient values, got {}", N*(n - K - 1), N*nc).into());
}
let mut v: Vec<f64> = Vec::with_capacity(u.len() * N);
let mut i = K;
let mut u_prev = f64::NEG_INFINITY;
let mut d = [0.0; DE_BOOR_SIZE];
for &t in u {
if t <= u_prev {
return Err("parameter values must be in strictly increasing order".into());
} else {
u_prev = t;
};
let arg = if t < self.t[K] || t > self.t[n - K - 1] {
t.clamp(self.t[K], self.t[n - K - 1])
} else {
t
};
while !(arg >= self.t[i] && arg <= self.t[i + 1]) {
i += 1
}
for dim in 0..N {
for (j, dm) in d.iter_mut().enumerate().take(K + 1) {
*dm = self.c[dim * nc + j + i - K];
}
v.push(self.deboor(i, arg, &mut d))
}
}
Ok(v)
}
pub fn eval(&mut self, t: f64) -> std::result::Result<[f64; DE_BOOR_SIZE], f64> {
let n = self.t.len();
let nc = self.c.len() / N;
if t < self.t[K] {
Err(t - self.t[K])
} else if t > self.t[n - K - 1] {
Err(t - self.t[n - K - 1])
} else {
let mut i = if let Some(i_prev) = self.i {
if t > self.t[i_prev] {
i_prev
} else {
K
}
} else {
K
};
while !(t >= self.t[i] && t <= self.t[i + 1]) {
i += 1
}
self.i = Some(i);
let mut result = [0.0; DE_BOOR_SIZE];
let mut d = [0.0; DE_BOOR_SIZE];
for dim in 0..N {
for (j, dm) in d.iter_mut().enumerate().take(K + 1) {
*dm = self.c[dim * nc + j + i - K];
}
result[dim] = self.deboor(i, t, &mut d);
}
Ok(result)
}
}
pub(crate) fn deboor(&self, i: usize, x: f64, d: &mut [f64; DE_BOOR_SIZE]) -> f64 {
for r in 1..K + 1 {
for j in (r..=K).into_iter().rev() {
let alpha =
(x - self.t[j + i - K]) / (self.t[j + 1 + i - r] - self.t[j + i - K]);
d[j] = (1.0 - alpha) * d[j - 1] + alpha * d[j]
}
}
d[K]
}
}
pub fn transpose(xyn: &[f64], n: usize) -> Vec<Vec<f64>>{
let m = xyn.len()/n;
let mut vn: Vec<Vec<f64>> = std::iter::repeat(Vec::with_capacity(m)).take(n).collect();
for v in xyn.chunks(n) {
for (i,x) in v.iter().enumerate() {
vn[i].push(*x);
}
}
vn
}
#[cfg(test)]
mod tests {
use super::{transpose, SplineCurve};
use approx::assert_abs_diff_eq;
#[test]
fn try_new_rejects_too_few_coefficients() {
let result: std::result::Result<SplineCurve<3, 1>, _> = SplineCurve::try_new(
vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0],
vec![0.0, 0.0, 0.0], );
assert!(result.is_err());
}
#[test]
fn evaluate_rejects_non_monotonic_params() {
let s: SplineCurve<1, 1> = SplineCurve::new(vec![0.0, 0.0, 1.0, 1.0], vec![0.0, 1.0]);
assert!(s.evaluate(&[0.2, 0.5, 0.4]).is_err()); }
#[test]
fn evaluate_rejects_coefficient_mismatch() {
let s: SplineCurve<1, 1> = SplineCurve::new(
vec![0.0, 0.0, 1.0, 2.0, 3.0, 3.0],
vec![0.0, 1.0, 2.0],
);
assert!(s.evaluate(&[0.5]).is_err());
}
#[test]
fn evaluate_clamps_out_of_range_params() {
let s: SplineCurve<1, 1> = SplineCurve::new(vec![0.0, 0.0, 1.0, 1.0], vec![0.0, 1.0]);
let yt = s.evaluate(&[-0.5, 1.5]).unwrap();
assert_abs_diff_eq!(yt[0], 0.0, epsilon = 1E-10); assert_abs_diff_eq!(yt[1], 1.0, epsilon = 1E-10); }
#[test]
fn eval_returns_err_with_signed_distance_when_out_of_range() {
let mut s: SplineCurve<3, 1> = SplineCurve::new(
vec![-2.0, -2.0, -2.0, -2.0, -1.0, 0.0, 1.0, 2.0, 2.0, 2.0, 2.0],
vec![0.0, 0.0, 0.0, 6.0, 0.0, 0.0, 0.0],
);
assert_abs_diff_eq!(s.eval(-3.0).unwrap_err(), -1.0, epsilon = 1E-10);
assert_abs_diff_eq!(s.eval(3.0).unwrap_err(), 1.0, epsilon = 1E-10);
}
#[test]
fn eval_cache_resets_correctly_on_backward_step() {
let mut s: SplineCurve<3, 1> = SplineCurve::new(
vec![-2.0, -2.0, -2.0, -2.0, -1.0, 0.0, 1.0, 2.0, 2.0, 2.0, 2.0],
vec![0.0, 0.0, 0.0, 6.0, 0.0, 0.0, 0.0],
);
assert_abs_diff_eq!(s.eval(0.5).unwrap()[0], 2.875, epsilon = 1E-7);
assert_abs_diff_eq!(s.eval(-1.0).unwrap()[0], 1.0, epsilon = 1E-7);
assert_abs_diff_eq!(s.eval(0.5).unwrap()[0], 2.875, epsilon = 1E-7);
}
#[test]
fn linear_2d_spline() {
let s: SplineCurve<1, 2> = SplineCurve::new(
vec![0.0, 0.0, 1.0, 1.0],
vec![0.0, 1.0, 0.0, 1.0],
);
let r = s.evaluate(&[0.0, 0.5, 1.0]).unwrap();
assert_abs_diff_eq!(r[0], 0.0, epsilon = 1E-10); assert_abs_diff_eq!(r[1], 0.0, epsilon = 1E-10); assert_abs_diff_eq!(r[2], 0.5, epsilon = 1E-10); assert_abs_diff_eq!(r[3], 0.5, epsilon = 1E-10); assert_abs_diff_eq!(r[4], 1.0, epsilon = 1E-10); assert_abs_diff_eq!(r[5], 1.0, epsilon = 1E-10); }
#[test]
fn transpose_splits_2d_coordinates() {
let flat = vec![1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0]; let vecs = transpose(&flat, 2);
assert_eq!(vecs[0], vec![1.0, 3.0, 5.0]); assert_eq!(vecs[1], vec![2.0, 4.0, 6.0]); }
#[test]
fn transpose_splits_3d_coordinates() {
let flat = vec![1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0]; let vecs = transpose(&flat, 3);
assert_eq!(vecs[0], vec![1.0, 4.0]);
assert_eq!(vecs[1], vec![2.0, 5.0]);
assert_eq!(vecs[2], vec![3.0, 6.0]);
}
#[test]
fn linear_bspline() {
let x = vec![0.0, 0.2, 0.4, 0.6, 0.8, 1.0];
let y = vec![0.0, 0.2, 0.4, 0.6, 0.8, 1.0];
let s: SplineCurve<1, 1> = SplineCurve::new(vec![0.0, 0.0, 1.0, 1.0], vec![0.0, 1.0]);
let yt = s.evaluate(&x).unwrap();
y.iter()
.zip(yt.iter())
.for_each(|(&a, &b)| assert_abs_diff_eq!(a, b, epsilon = 1E-8));
#[cfg(feature = "plot")]
s.plot("test.png", (1500,1000)).unwrap();
}
#[test]
fn quadratic_bspline() {
let x = [0.0, 0.5, 1.0, 1.4, 1.5, 1.6, 2.0, 2.5, 3.0];
let y = [0.0, 0.125, 0.5, 0.74, 0.75, 0.74, 0.5, 0.125, 0.0];
let s: SplineCurve<2, 1> = SplineCurve::new(
vec![0.0, 0.0, 0.0, 1.0, 2.0, 3.0, 3.0, 3.0],
vec![0.0, 0.0, 1.0, 0.0, 0.0],
);
let yt = s.evaluate(&x).unwrap();
y.iter()
.zip(yt.iter())
.for_each(|(&a, &b)| assert_abs_diff_eq!(a, b, epsilon = 1E-8));
#[cfg(feature = "plot")]
s.plot("test.png", (1500,1000)).unwrap();
}
#[test]
fn cubic_bspline() {
let x = vec![-2.0, -1.5, -1.0, -0.6, 0.0, 0.5, 1.5, 2.0];
let y = vec![0.0, 0.125, 1.0, 2.488, 4.0, 2.875, 0.12500001, 0.0];
let s: SplineCurve<3, 1> = SplineCurve::new(
vec![-2.0, -2.0, -2.0, -2.0, -1.0, 0.0, 1.0, 2.0, 2.0, 2.0, 2.0],
vec![0.0, 0.0, 0.0, 6.0, 0.0, 0.0, 0.0],
);
let yt = s.evaluate(&x).unwrap();
y.iter()
.zip(yt.iter())
.for_each(|(&a, &b)| assert_abs_diff_eq!(a, b, epsilon = 1E-7));
#[cfg(feature = "plot")]
s.plot("test.png", (1000,1000)).unwrap();
}
#[test]
fn cubic_bspline_single_values() {
let x = vec![-2.0, -1.5, -1.0, -0.6, 0.0, 0.5, 1.5, 2.0];
let y = vec![0.0, 0.125, 1.0, 2.488, 4.0, 2.875, 0.12500001, 0.0];
let mut s: SplineCurve<3, 1> = SplineCurve::new(
vec![-2.0, -2.0, -2.0, -2.0, -1.0, 0.0, 1.0, 2.0, 2.0, 2.0, 2.0],
vec![0.0, 0.0, 0.0, 6.0, 0.0, 0.0, 0.0],
);
let yt: Vec<f64> = x.into_iter().map(|x|s.eval(x).unwrap()[0]).collect();
y.iter()
.zip(yt.iter())
.for_each(|(&a, &b)| assert_abs_diff_eq!(a, b, epsilon = 1E-7));
#[cfg(feature = "plot")]
s.plot("test.png", (1000,1000)).unwrap();
}
#[test]
fn quartic_bspline() {
let x = vec![0.0, 0.4, 1.0, 1.5, 2.0, 2.5, 3.0, 3.2, 4.1, 4.5, 5.0];
let y = vec![
0.0,
0.0010666668,
0.041666668,
0.19791667,
0.4583333,
0.5989583,
0.4583333,
0.35206667,
0.02733751,
0.002604167,
0.0,
];
let s: SplineCurve<4, 1> = SplineCurve::new(
vec![
0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 5.0, 5.0, 5.0, 5.0,
],
vec![0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0],
);
let yt = s.evaluate(&x).unwrap();
y.iter()
.zip(yt.iter())
.for_each(|(&a, &b)| assert_abs_diff_eq!(a, b, epsilon = 1E-7));
#[cfg(feature = "plot")]
s.plot("test.png", (2000,1000)).unwrap();
}
}