#[inline]
pub(crate) fn leibniz_product<L, R>(positions: &[usize], mut left: L, mut right: R) -> f64
where
L: FnMut(&[usize]) -> f64,
R: FnMut(&[usize]) -> f64,
{
let m = positions.len();
assert!(
m <= usize::BITS as usize,
"too many differentiation slots for subset enumeration"
);
let mut subset = SlotBuf::new();
let mut complement = SlotBuf::new();
let mut total = 0.0;
for sub in 0u32..(1u32 << m) {
subset.clear();
complement.clear();
for (bit, &pos) in positions.iter().enumerate() {
if sub & (1u32 << bit) != 0 {
subset.push(pos);
} else {
complement.push(pos);
}
}
total += left(subset.as_slice()) * right(complement.as_slice());
}
total
}
#[inline]
pub(crate) fn faa_di_bruno<F>(positions: &[usize], derivs: &[f64], mut inner: F) -> f64
where
F: FnMut(&[usize]) -> f64,
{
let m = positions.len();
if m == 0 {
return derivs[0];
}
let mut total = 0.0;
for_each_partition(m, &mut |blocks: &[SlotBuf]| {
let order = blocks.len();
if order >= derivs.len() {
return;
}
let mut prod = derivs[order];
for block in blocks {
let mut labelled = SlotBuf::new();
for &p in block.as_slice() {
labelled.push(positions[p]);
}
prod *= inner(labelled.as_slice());
}
total += prod;
});
total
}
pub(crate) trait JetAlgebra<const DERIVS: usize>: Sized {
fn derivative(&self, positions: &[usize]) -> f64;
fn map_derivatives<F>(&self, f: F) -> Self
where
F: FnMut(&[usize]) -> f64;
fn compose_unary(&self, derivs: [f64; DERIVS]) -> Self {
compose_unary_kernel(self, derivs)
}
}
#[inline]
pub(crate) fn compose_unary_kernel<J, const DERIVS: usize>(inner: &J, derivs: [f64; DERIVS]) -> J
where
J: JetAlgebra<DERIVS>,
{
inner.map_derivatives(|positions| {
faa_di_bruno(positions, &derivs, |block| inner.derivative(block))
})
}
#[derive(Clone, Copy)]
pub(crate) struct SlotBuf {
data: [usize; 8],
len: usize,
}
impl SlotBuf {
#[inline]
pub(crate) fn new() -> Self {
Self {
data: [0; 8],
len: 0,
}
}
#[inline]
fn clear(&mut self) {
self.len = 0;
}
#[inline]
fn push(&mut self, v: usize) {
self.data[self.len] = v;
self.len += 1;
}
#[inline]
pub(crate) fn push_slot(&mut self, v: usize) {
self.push(v);
}
#[inline]
pub(crate) fn as_slice(&self) -> &[usize] {
&self.data[..self.len]
}
}
fn for_each_partition(m: usize, f: &mut dyn FnMut(&[SlotBuf])) {
let mut blocks: [SlotBuf; 8] = [SlotBuf::new(); 8];
recurse(0, m, &mut blocks, 0, f);
}
fn recurse(
elem: usize,
m: usize,
blocks: &mut [SlotBuf; 8],
n_blocks: usize,
f: &mut dyn FnMut(&[SlotBuf]),
) {
if elem == m {
f(&blocks[..n_blocks]);
return;
}
for b in 0..n_blocks {
blocks[b].push(elem);
recurse(elem + 1, m, blocks, n_blocks, f);
blocks[b].len -= 1;
}
blocks[n_blocks].clear();
blocks[n_blocks].push(elem);
recurse(elem + 1, m, blocks, n_blocks + 1, f);
}
#[cfg(test)]
mod tests {
use super::super::jet_partitions::MultiDirJet;
use super::super::jet_tower::Tower4;
#[test]
fn tower_and_dirjet_agree_bit_exact() {
let x = 0.37_f64;
let z = -0.81_f64;
let tx = Tower4::<2>::variable(x, 0);
let tz = Tower4::<2>::variable(z, 1);
let tg = (tx * tz + tx).exp();
let tf = (tg + 2.0).ln() * tg;
let jx = MultiDirJet::linear(2, x, &[1.0, 0.0]);
let jz = MultiDirJet::linear(2, z, &[0.0, 1.0]);
let jg = exp_dirjet(&jx.mul(&jz).add(&jx));
let jf = ln_dirjet(&jg.add(&MultiDirJet::constant(2, 2.0))).mul(&jg);
assert_eq!(jf.coeff(0b00), tf.v, "value");
assert_eq!(jf.coeff(0b01), tf.g[0], "∂x");
assert_eq!(jf.coeff(0b10), tf.g[1], "∂z");
assert_eq!(jf.coeff(0b11), tf.h[0][1], "∂x∂z");
assert_eq!(tf.h[0][1], tf.h[1][0], "tower mixed-partial symmetry");
}
#[test]
fn tower_contractions_match_dirjet_directional_coefficients() {
const K: usize = 3;
let p = [0.37_f64, -0.42_f64, 0.19_f64];
let q = [0.25_f64, -0.7_f64, 1.3_f64];
let u = [-0.4_f64, 0.9_f64, 0.15_f64];
let w = [1.1_f64, -0.2_f64, 0.6_f64];
let tower = nonlinear_tower_program(p);
let third = tower.third_contracted(&q);
let fourth = tower.fourth_contracted(&u, &w);
for a in 0..K {
for b in 0..K {
let mut dirs3 = [[0.0; K]; 3];
dirs3[0][a] = 1.0;
dirs3[1][b] = 1.0;
dirs3[2] = q;
let jet3 = nonlinear_dirjet_program(p, &dirs3);
assert_close(
jet3.coeff(jet3.coeffs.len() - 1),
third[a][b],
&format!("third contraction ({a},{b})"),
);
let mut dirs4 = [[0.0; K]; 4];
dirs4[0][a] = 1.0;
dirs4[1][b] = 1.0;
dirs4[2] = u;
dirs4[3] = w;
let jet4 = nonlinear_dirjet_program(p, &dirs4);
assert_close(
jet4.coeff(jet4.coeffs.len() - 1),
fourth[a][b],
&format!("fourth contraction ({a},{b})"),
);
}
}
}
fn nonlinear_tower_program(p: [f64; 3]) -> Tower4<3> {
let x = Tower4::<3>::variable(p[0], 0);
let y = Tower4::<3>::variable(p[1], 1);
let z = Tower4::<3>::variable(p[2], 2);
let eta = x * y + x * z + z * 0.7;
let g = eta.exp();
(g + 2.0).ln() * g
}
fn nonlinear_dirjet_program(p: [f64; 3], dirs: &[[f64; 3]]) -> MultiDirJet {
let n_dirs = dirs.len();
let x = MultiDirJet::linear(n_dirs, p[0], &direction_components(dirs, 0));
let y = MultiDirJet::linear(n_dirs, p[1], &direction_components(dirs, 1));
let z = MultiDirJet::linear(n_dirs, p[2], &direction_components(dirs, 2));
let eta = x.mul(&y).add(&x.mul(&z)).add(&z.scale(0.7));
let g = exp_dirjet(&eta);
ln_dirjet(&g.add(&MultiDirJet::constant(n_dirs, 2.0))).mul(&g)
}
fn direction_components(dirs: &[[f64; 3]], axis: usize) -> Vec<f64> {
dirs.iter().map(|dir| dir[axis]).collect()
}
fn assert_close(got: f64, want: f64, label: &str) {
let tol = 1.0e-12 * want.abs().max(1.0);
assert!(
(got - want).abs() <= tol,
"{label}: got={got:.17e}, want={want:.17e}, diff={:.3e}, tol={tol:.3e}",
(got - want).abs()
);
}
fn exp_dirjet(j: &MultiDirJet) -> MultiDirJet {
let e = j.coeff(0).exp();
j.compose_unary([e, e, e, e, e])
}
fn ln_dirjet(j: &MultiDirJet) -> MultiDirJet {
let u = j.coeff(0);
let r = 1.0 / u;
j.compose_unary([u.ln(), r, -r * r, 2.0 * r * r * r, -6.0 * r * r * r * r])
}
}