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_divided_difference_transform_matrix(
greville: &Array1<f64>,
sign: f64,
) -> Result<Array2<f64>, BasisError> {
let p = greville.len();
if p < 3 {
crate::bail_invalid_basis!(
"convex/concave box reparameterization requires at least 3 basis functions; found {p}"
);
}
let mut first_span = Array1::<f64>::zeros(p - 1);
for i in 0..p - 1 {
let s = greville[i + 1] - greville[i];
if !(s > 1e-12 * greville[p - 1].abs().max(1.0)) {
crate::bail_invalid_basis!(
"convex/concave box reparameterization requires strictly increasing Greville abscissae; span ξ[{}]−ξ[{i}] = {s:.3e} is non-positive",
i + 1
);
}
first_span[i] = s;
}
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 = greville[i + 2] - greville[i];
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_divided_difference_transform_matrix, cumulative_sum_transform_matrix};
use ndarray::Array1;
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_divided_difference_transform_matrix(&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 t = convex_divided_difference_transform_matrix(&g, 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"
);
assert!(
(v - gamma[i + 2]).abs() < 1e-9,
"cone coordinate mismatch at {i}: d2 = {v:.3e}, gamma = {:.3e}",
gamma[i + 2]
);
}
}
#[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"
);
}
}