use super::basis::basis_funs_into;
pub struct PowerBasis1D {
degree: usize,
n_spans: usize,
coeffs: Vec<f64>,
span_starts: Vec<f64>,
}
impl PowerBasis1D {
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn from_knots(knots: &[f64], degree: usize) -> Self {
let p = degree;
let n_ctrl = knots.len() - degree - 1; let n_spans = n_ctrl - degree; let block = (p + 1) * (p + 1);
let mut coeffs = vec![0.0; n_spans * block];
let mut span_starts = Vec::with_capacity(n_spans);
let mut basis_vals = vec![0.0; p + 1];
let mut samples = vec![0.0; p + 1];
let mut t_pts = vec![0.0; p + 1];
for si in 0..n_spans {
let span = si + p; let t0 = knots[span];
let t1 = knots[span + 1];
span_starts.push(t0);
let h = t1 - t0;
if h <= 0.0 {
continue;
}
for k in 0..=p {
t_pts[k] = k as f64 * h / p as f64;
}
let mut all_samples = vec![0.0; (p + 1) * (p + 1)];
for k in 0..=p {
let u = t0 + t_pts[k];
basis_funs_into(span, u, degree, knots, &mut basis_vals);
for j in 0..=p {
all_samples[j * (p + 1) + k] = basis_vals[j];
}
}
for j in 0..=p {
for k in 0..=p {
samples[k] = all_samples[j * (p + 1) + k];
}
for k in 1..=p {
for i in (k..=p).rev() {
samples[i] = (samples[i] - samples[i - 1]) / (t_pts[i] - t_pts[i - k]);
}
}
let base = si * block + j * (p + 1);
let c = &mut coeffs[base..=base + p];
c[0] = samples[p];
for m in (0..p).rev() {
for k in (1..=p - m).rev() {
c[k] = (-t_pts[m]).mul_add(c[k], c[k - 1]);
}
c[0] = (-t_pts[m]).mul_add(c[0], samples[m]);
}
}
}
Self {
degree,
n_spans,
coeffs,
span_starts,
}
}
pub fn horner(&self, span: usize, u: f64, out: &mut [f64]) {
let p = self.degree;
let span_idx = span - p;
let t = u - self.span_starts[span_idx];
let base = span_idx * (p + 1) * (p + 1);
for j in 0..=p {
let coeff_base = base + j * (p + 1);
let mut val = self.coeffs[coeff_base + p];
for k in (0..p).rev() {
val = val.mul_add(t, self.coeffs[coeff_base + k]);
}
out[j] = val;
}
}
#[allow(clippy::cast_precision_loss)]
pub fn horner_with_derivs(&self, span: usize, u: f64, vals: &mut [f64], derivs: &mut [f64]) {
let p = self.degree;
let span_idx = span - p;
let t = u - self.span_starts[span_idx];
let base = span_idx * (p + 1) * (p + 1);
for j in 0..=p {
let coeff_base = base + j * (p + 1);
if p == 0 {
vals[j] = self.coeffs[coeff_base];
derivs[j] = 0.0;
continue;
}
let mut val = self.coeffs[coeff_base + p];
let mut dval = self.coeffs[coeff_base + p] * p as f64;
for k in (1..p).rev() {
val = val.mul_add(t, self.coeffs[coeff_base + k]);
dval = dval.mul_add(t, self.coeffs[coeff_base + k] * k as f64);
}
val = val.mul_add(t, self.coeffs[coeff_base]);
vals[j] = val;
derivs[j] = dval;
}
}
#[must_use]
pub const fn n_spans(&self) -> usize {
self.n_spans
}
#[must_use]
pub const fn degree(&self) -> usize {
self.degree
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::super::basis::{basis_funs, ders_basis_funs, find_span};
use super::*;
fn cubic_knots() -> Vec<f64> {
vec![0.0, 0.0, 0.0, 0.0, 1.0, 2.0, 3.0, 3.0, 3.0, 3.0]
}
#[test]
fn power_basis_matches_cox_de_boor() {
let knots = cubic_knots();
let degree = 3;
let pb = PowerBasis1D::from_knots(&knots, degree);
for &u in &[0.0, 0.25, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0] {
let span = find_span(6, degree, u, &knots);
let expected = basis_funs(span, u, degree, &knots);
let mut got = [0.0_f64; 4];
pb.horner(span, u, &mut got[..=degree]);
for j in 0..=degree {
assert!(
(expected[j] - got[j]).abs() < 1e-12,
"u={u}, span={span}, j={j}: {:.15} vs {:.15}",
expected[j],
got[j]
);
}
}
}
#[test]
fn horner_derivs_match_ders_basis_funs() {
let knots = cubic_knots();
let degree = 3;
let pb = PowerBasis1D::from_knots(&knots, degree);
for &u in &[0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0] {
let span = find_span(6, degree, u, &knots);
let expected = ders_basis_funs(span, u, degree, 1, &knots);
let mut vals = [0.0_f64; 4];
let mut derivs = [0.0_f64; 4];
pb.horner_with_derivs(span, u, &mut vals[..=degree], &mut derivs[..=degree]);
for j in 0..=degree {
assert!(
(expected[0][j] - vals[j]).abs() < 1e-12,
"u={u}, j={j}: val {:.15} vs {:.15}",
expected[0][j],
vals[j]
);
assert!(
(expected[1][j] - derivs[j]).abs() < 1e-10,
"u={u}, j={j}: deriv {:.15} vs {:.15}",
expected[1][j],
derivs[j]
);
}
}
}
#[test]
fn power_basis_partition_of_unity() {
let knots = cubic_knots();
let degree = 3;
let pb = PowerBasis1D::from_knots(&knots, degree);
for i in 0..=30 {
#[allow(clippy::cast_precision_loss)]
let u = i as f64 / 10.0;
let span = find_span(6, degree, u, &knots);
let mut vals = [0.0_f64; 4];
pb.horner(span, u, &mut vals[..=degree]);
let sum: f64 = vals.iter().sum();
assert!(
(sum - 1.0).abs() < 1e-12,
"u={u}: partition of unity sum = {sum}"
);
}
}
#[test]
fn quadratic_knots() {
let knots = vec![0.0, 0.0, 0.0, 0.5, 1.5, 3.0, 3.0, 3.0];
let degree = 2;
let n = knots.len() - degree - 1; let pb = PowerBasis1D::from_knots(&knots, degree);
for i in 0..=60 {
#[allow(clippy::cast_precision_loss)]
let u = i as f64 / 20.0;
let span = find_span(n, degree, u, &knots);
let expected = basis_funs(span, u, degree, &knots);
let mut got = [0.0_f64; 3];
pb.horner(span, u, &mut got[..=degree]);
for j in 0..=degree {
assert!(
(expected[j] - got[j]).abs() < 1e-12,
"u={u}, j={j}: {:.15} vs {:.15}",
expected[j],
got[j]
);
}
}
}
#[test]
fn linear_basis() {
let knots = vec![0.0, 0.0, 1.0, 2.0, 3.0, 3.0];
let degree = 1;
let n = knots.len() - degree - 1; let pb = PowerBasis1D::from_knots(&knots, degree);
for i in 0..=30 {
#[allow(clippy::cast_precision_loss)]
let u = i as f64 / 10.0;
let span = find_span(n, degree, u, &knots);
let expected = basis_funs(span, u, degree, &knots);
let mut got = [0.0_f64; 2];
pb.horner(span, u, &mut got[..=degree]);
for j in 0..=degree {
assert!(
(expected[j] - got[j]).abs() < 1e-14,
"u={u}, j={j}: {:.15} vs {:.15}",
expected[j],
got[j]
);
}
}
}
use proptest::prelude::*;
proptest! {
#[test]
fn prop_power_basis_matches_cox_de_boor(u in 0.0_f64..=3.0) {
let knots = cubic_knots();
let degree = 3;
let pb = PowerBasis1D::from_knots(&knots, degree);
let span = find_span(6, degree, u, &knots);
let expected = basis_funs(span, u, degree, &knots);
let mut got = [0.0_f64; 4];
pb.horner(span, u, &mut got[..=degree]);
for j in 0..=degree {
prop_assert!(
(expected[j] - got[j]).abs() < 1e-10,
"u={}, j={}: {} vs {}",
u,
j,
expected[j],
got[j]
);
}
}
#[test]
fn prop_derivs_sum_to_zero(u in 0.0_f64..=3.0) {
let knots = cubic_knots();
let degree = 3;
let pb = PowerBasis1D::from_knots(&knots, degree);
let span = find_span(6, degree, u, &knots);
let mut vals = [0.0_f64; 4];
let mut derivs = [0.0_f64; 4];
pb.horner_with_derivs(span, u, &mut vals[..=degree], &mut derivs[..=degree]);
let sum: f64 = derivs.iter().sum();
prop_assert!(
sum.abs() < 1e-8,
"u={}: derivative sum = {}",
u,
sum
);
}
}
}