use crate::basis::BasisError;
use ndarray::{Array1, Array2};
pub(crate) fn cumulative_exp(values: &Array1<f64>, sign: f64) -> Array1<f64> {
let mut out = Array1::<f64>::zeros(values.len());
let mut run = 0.0;
for i in 0..values.len() {
run += values[i].exp();
out[i] = sign * run;
}
out
}
pub(crate) fn second_cumulative_exp(values: &Array1<f64>, sign: f64) -> Array1<f64> {
let first = cumulative_exp(values, sign);
let mut out = Array1::<f64>::zeros(values.len());
let mut run = 0.0;
for i in 0..values.len() {
run += first[i];
out[i] = run;
}
out
}
pub(crate) fn cumulative_sum_transform_matrix(dim: usize, order: usize, sign: f64) -> Array2<f64> {
let mut t = Array2::<f64>::zeros((dim, dim));
if order == 0 {
for i in 0..dim {
t[[i, i]] = 1.0;
}
} else {
let k = order - 1;
for i in 0..dim {
for j in 0..=i {
t[[i, j]] = binomial(i - j + k, k) as f64;
}
}
}
if sign < 0.0 {
t.mapv_inplace(|v| -v);
}
t
}
pub(crate) fn convex_derivative_control_transform_matrix(
first_span: &Array1<f64>,
sign: f64,
) -> Result<Array2<f64>, BasisError> {
let p = first_span.len() + 1;
if first_span.is_empty() {
crate::bail_invalid_basis!(
"convex/concave box reparameterization requires at least 2 basis functions; found {p}"
);
}
let span_scale = first_span.iter().copied().fold(0.0_f64, f64::max);
for (i, &span) in first_span.iter().enumerate() {
if !span.is_finite() || span <= 0.0 {
crate::bail_invalid_basis!(
"convex/concave box reparameterization requires positive finite derivative-control spans; span[{i}]={span:.3e}"
);
}
}
let first_span = first_span.mapv(|span| span / span_scale);
let mut t = Array2::<f64>::zeros((p, p));
for c in 0..p {
let gamma1 = if c == 1 { 1.0 } else { 0.0 };
let mut m = Array1::<f64>::zeros(p - 1);
m[0] = gamma1;
for i in 0..p - 2 {
let gamma_ip2 = if c == i + 2 { 1.0 } else { 0.0 };
let second_span = first_span[i] + first_span[i + 1];
m[i + 1] = m[i] + second_span * gamma_ip2;
}
let theta0 = if c == 0 { 1.0 } else { 0.0 };
t[[0, c]] = theta0;
for i in 0..p - 1 {
t[[i + 1, c]] = t[[i, c]] + first_span[i] * m[i];
}
}
if sign < 0.0 {
t.mapv_inplace(|v| -v);
}
Ok(t)
}
fn binomial(n: usize, k: usize) -> u64 {
if k > n {
return 0;
}
let k = k.min(n - k);
let mut num: u64 = 1;
for i in 0..k {
num = num * (n - i) as u64 / (i + 1) as u64;
}
num
}
#[cfg(test)]
mod cumulative_sum_transform_tests {
use super::cumulative_sum_transform_matrix;
use ndarray::Array2;
fn reference(dim: usize, order: usize, sign: f64) -> Array2<f64> {
let mut t = Array2::<f64>::eye(dim);
for _ in 0..order {
let mut next = Array2::<f64>::zeros((dim, dim));
for i in 0..dim {
for j in 0..=i {
next[[i, j]] = 1.0;
}
}
t = t.dot(&next);
}
if sign < 0.0 {
t.mapv_inplace(|v| -v);
}
t
}
#[test]
fn closed_form_matches_loop() {
for &dim in &[5usize, 10] {
for &order in &[1usize, 2, 3, 4] {
for &sign in &[1.0_f64, -1.0] {
let got = cumulative_sum_transform_matrix(dim, order, sign);
let want = reference(dim, order, sign);
for i in 0..dim {
for j in 0..dim {
assert!(
(got[[i, j]] - want[[i, j]]).abs() < 1e-12,
"mismatch at dim={dim} order={order} sign={sign} ({i},{j}): \
got {} want {}",
got[[i, j]],
want[[i, j]],
);
}
}
}
}
}
}
}
#[cfg(test)]
mod convex_divided_difference_transform_tests {
use super::{convex_derivative_control_transform_matrix, cumulative_sum_transform_matrix};
use crate::smooth::{
ShapeConstraint,
shape_constraints::{
bspline_first_derivative_control_spans, bspline_shape_linear_constraints,
},
};
use ndarray::Array1;
fn first_spans(g: &Array1<f64>) -> Array1<f64> {
Array1::from_iter((0..g.len() - 1).map(|i| g[i + 1] - g[i]))
}
fn second_divided_difference(theta: &Array1<f64>, g: &Array1<f64>) -> Array1<f64> {
let p = theta.len();
let mut m = Array1::<f64>::zeros(p - 1);
for i in 0..p - 1 {
m[i] = (theta[i + 1] - theta[i]) / (g[i + 1] - g[i]);
}
let mut d = Array1::<f64>::zeros(p - 2);
for i in 0..p - 2 {
d[i] = (m[i + 1] - m[i]) / (g[i + 2] - g[i]);
}
d
}
#[test]
fn uniform_greville_matches_integer_transform_up_to_column_scale() {
let p = 7;
let g = Array1::from_iter((0..p).map(|i| i as f64));
for &sign in &[1.0_f64, -1.0] {
let t = convex_derivative_control_transform_matrix(&first_spans(&g), sign).unwrap();
let plain = cumulative_sum_transform_matrix(p, 2, sign);
for c in 2..p {
let mut scale: Option<f64> = None;
for i in 0..p {
if plain[[i, c]].abs() > 1e-9 {
scale = Some(t[[i, c]] / plain[[i, c]]);
break;
}
}
let s = scale.expect("plain cone column must have a non-zero entry");
assert!(s > 0.0, "column {c} scale must be positive, got {s}");
for i in 0..p {
assert!(
(t[[i, c]] - s * plain[[i, c]]).abs() < 1e-9,
"uniform reduction mismatch at ({i},{c})"
);
}
}
for c in 0..2usize {
for col in [&t, &plain] {
let n = p as f64;
let sum_g: f64 = g.iter().copied().sum();
let sum_gg: f64 = g.iter().map(|&x| x * x).sum();
let sum_y: f64 = (0..p).map(|i| col[[i, c]]).sum();
let sum_gy: f64 = (0..p).map(|i| g[i] * col[[i, c]]).sum();
let det = n * sum_gg - sum_g * sum_g;
let b = (n * sum_gy - sum_g * sum_y) / det;
let a = (sum_y - b * sum_g) / n;
for i in 0..p {
let fit = a + b * g[i];
assert!(
(col[[i, c]] - fit).abs() < 1e-9,
"affine column {c} entry {i} is not affine in ξ: \
got {}, affine fit {fit}",
col[[i, c]]
);
}
}
}
}
}
#[test]
fn nonneg_gamma_certifies_convexity_for_nonuniform_greville() {
let g = Array1::from(vec![0.0, 0.1, 0.3, 0.7, 1.4, 2.6, 4.5]);
let spans = first_spans(&g);
let span_scale = spans.iter().copied().fold(0.0_f64, f64::max);
let t = convex_derivative_control_transform_matrix(&spans, 1.0).unwrap();
let gamma = Array1::from(vec![-2.0, 1.5, 0.4, 0.0, 1.2, 0.7, 0.9]);
let theta = t.dot(&gamma);
let d2 = second_divided_difference(&theta, &g);
for (i, &v) in d2.iter().enumerate() {
assert!(
v >= -1e-9,
"convex cone violated: second divided difference d2[{i}] = {v:.3e} < 0"
);
let expected = gamma[i + 2] / (span_scale * span_scale);
assert!(
(v - expected).abs() < 1e-9,
"cone coordinate mismatch at {i}: d2 = {v:.3e}, expected = {expected:.3e}",
);
}
}
#[test]
fn affine_coefficients_are_a_cone_boundary() {
let g = Array1::from(vec![0.0, 0.1, 0.3, 0.7, 1.4, 2.6, 4.5]);
let theta = g.mapv(|x| 3.0 - 2.0 * x);
let d2 = second_divided_difference(&theta, &g);
for (i, &v) in d2.iter().enumerate() {
assert!(
v.abs() < 1e-9,
"affine control polygon must have zero second divided difference at {i}, got {v:.3e}"
);
}
let mut any_nonzero_plain = false;
for i in 0..theta.len() - 2 {
let plain = theta[i + 2] - 2.0 * theta[i + 1] + theta[i];
if plain.abs() > 1e-6 {
any_nonzero_plain = true;
}
}
assert!(
any_nonzero_plain,
"non-uniform abscissae should make the plain second difference non-zero on affine data"
);
}
#[test]
fn box_transforms_realize_the_canonical_linear_cones() {
let knots = Array1::from(vec![
0.0, 0.0, 0.0, 0.0, 0.08, 0.37, 0.62, 1.0, 1.0, 1.0, 1.0,
]);
let spans = bspline_first_derivative_control_spans(knots.view(), 3).unwrap();
let p = spans.len() + 1;
for shape in [
ShapeConstraint::MonotoneIncreasing,
ShapeConstraint::MonotoneDecreasing,
ShapeConstraint::Convex,
ShapeConstraint::Concave,
] {
let (order, sign) = match shape {
ShapeConstraint::MonotoneIncreasing => (1, 1.0),
ShapeConstraint::MonotoneDecreasing => (1, -1.0),
ShapeConstraint::Convex => (2, 1.0),
ShapeConstraint::Concave => (2, -1.0),
ShapeConstraint::None => unreachable!(),
};
let transform = if order == 1 {
cumulative_sum_transform_matrix(p, order, sign)
} else {
convex_derivative_control_transform_matrix(&spans, sign).unwrap()
};
let constraints = bspline_shape_linear_constraints(knots.view(), 3, shape)
.unwrap()
.unwrap();
let mapped = constraints.a.dot(&transform);
for row in 0..mapped.nrows() {
for col in 0..mapped.ncols() {
if col == row + order {
assert!(
mapped[[row, col]] > 0.0,
"shape={shape:?} mapped cone diagonal must be positive"
);
} else {
assert!(
mapped[[row, col]].abs() <= 64.0 * f64::EPSILON,
"shape={shape:?} A*T must isolate cone coordinate; ({row},{col})={} ",
mapped[[row, col]],
);
}
}
}
}
}
}