use echidna::{Dual, Scalar};
#[cfg(feature = "bytecode")]
use echidna::record;
#[cfg(feature = "bytecode")]
mod forward_over_reverse {
use super::*;
use num_traits::Float;
#[test]
fn hessian_sphere() {
let (tape, _) = record(|x| x[0] * x[0] + x[1] * x[1], &[3.0_f64, 4.0]);
let (val, grad, hess) = tape.hessian(&[3.0, 4.0]);
assert!((val - 25.0_f64).abs() < 1e-10);
assert!((grad[0] - 6.0_f64).abs() < 1e-10);
assert!((grad[1] - 8.0_f64).abs() < 1e-10);
assert!((hess[0][0] - 2.0_f64).abs() < 1e-10);
assert!(hess[0][1].abs() < 1e-10);
assert!(hess[1][0].abs() < 1e-10);
assert!((hess[1][1] - 2.0_f64).abs() < 1e-10);
}
#[test]
fn hessian_cross_term() {
let (tape, _) = record(|x| x[0] * x[1], &[2.0_f64, 3.0]);
let (val, grad, hess) = tape.hessian(&[2.0, 3.0]);
assert!((val - 6.0_f64).abs() < 1e-10);
assert!((grad[0] - 3.0_f64).abs() < 1e-10);
assert!((grad[1] - 2.0_f64).abs() < 1e-10);
assert!(hess[0][0].abs() < 1e-10);
assert!((hess[0][1] - 1.0_f64).abs() < 1e-10);
assert!((hess[1][0] - 1.0_f64).abs() < 1e-10);
assert!(hess[1][1].abs() < 1e-10);
}
#[test]
fn hessian_cubic_mixed() {
let x = 1.5_f64;
let y = 2.0_f64;
let (tape, _) = record(|v| v[0] * v[0] * v[1] + v[1] * v[1] * v[1], &[x, y]);
let (val, grad, hess) = tape.hessian(&[x, y]);
let expected_val = x * x * y + y * y * y;
assert!((val - expected_val).abs() < 1e-10);
assert!((grad[0] - 2.0 * x * y).abs() < 1e-10);
assert!((grad[1] - (x * x + 3.0 * y * y)).abs() < 1e-10);
assert!((hess[0][0] - 2.0 * y).abs() < 1e-10);
assert!((hess[0][1] - 2.0 * x).abs() < 1e-10);
assert!((hess[1][0] - 2.0 * x).abs() < 1e-10);
assert!((hess[1][1] - 6.0 * y).abs() < 1e-10);
}
#[test]
fn hessian_sin_1d() {
let x = 1.0_f64;
let (tape, _) = record(|v| v[0].sin(), &[x]);
let (val, grad, hess) = tape.hessian(&[x]);
assert!((val - x.sin()).abs() < 1e-10);
assert!((grad[0] - x.cos()).abs() < 1e-10);
assert!((hess[0][0] - (-x.sin())).abs() < 1e-10);
}
#[test]
fn hessian_sin_exp() {
let x = 0.7_f64;
let y = 0.3_f64;
let (tape, _) = record(|v| v[0].sin() * v[1].exp(), &[x, y]);
let (val, grad, hess) = tape.hessian(&[x, y]);
let ey = y.exp();
assert!((val - x.sin() * ey).abs() < 1e-10);
assert!((grad[0] - x.cos() * ey).abs() < 1e-10);
assert!((grad[1] - x.sin() * ey).abs() < 1e-10);
assert!((hess[0][0] - (-x.sin() * ey)).abs() < 1e-10);
assert!((hess[0][1] - x.cos() * ey).abs() < 1e-10);
assert!((hess[1][0] - x.cos() * ey).abs() < 1e-10);
assert!((hess[1][1] - x.sin() * ey).abs() < 1e-10);
}
#[test]
fn api_hessian_sphere() {
let (val, grad, hess) = echidna::hessian(|x| x[0] * x[0] + x[1] * x[1], &[3.0_f64, 4.0]);
assert!((val - 25.0_f64).abs() < 1e-10);
assert!((grad[0] - 6.0_f64).abs() < 1e-10);
assert!((grad[1] - 8.0_f64).abs() < 1e-10);
assert!((hess[0][0] - 2.0_f64).abs() < 1e-10);
assert!((hess[1][1] - 2.0_f64).abs() < 1e-10);
}
#[test]
fn api_hvp_cross_term() {
let (grad, hv) = echidna::hvp(|x| x[0] * x[1], &[2.0_f64, 3.0], &[1.0_f64, 0.0]);
assert!((grad[0] - 3.0_f64).abs() < 1e-10);
assert!((grad[1] - 2.0_f64).abs() < 1e-10);
assert!(hv[0].abs() < 1e-10);
assert!((hv[1] - 1.0_f64).abs() < 1e-10);
}
fn finite_diff_hvp(
tape: &mut echidna::BytecodeTape<f64>,
x: &[f64],
v: &[f64],
h: f64,
) -> Vec<f64> {
let n = x.len();
let mut xp = x.to_vec();
let mut xm = x.to_vec();
for i in 0..n {
xp[i] = x[i] + h * v[i];
xm[i] = x[i] - h * v[i];
}
let gp = tape.gradient(&xp);
let gm = tape.gradient(&xm);
(0..n).map(|i| (gp[i] - gm[i]) / (2.0 * h)).collect()
}
#[test]
fn hvp_vs_finite_diff_cubic() {
let x = [1.5_f64, 2.0];
let v = [0.7_f64, -0.3];
let (mut tape, _) = record(|v| v[0] * v[0] * v[1] + v[1] * v[1] * v[1], &x);
let (_, analytic_hv) = tape.hvp(&x, &v);
let fd_hv = finite_diff_hvp(&mut tape, &x, &v, 1e-5);
for i in 0..x.len() {
assert!(
(analytic_hv[i] - fd_hv[i]).abs() < 1e-4,
"hvp vs fd, component {}: analytic={}, fd={}",
i,
analytic_hv[i],
fd_hv[i]
);
}
}
#[test]
fn hvp_vs_finite_diff_sin_exp() {
let x = [0.7_f64, 0.3];
let v = [1.0_f64, 1.0];
let (mut tape, _) = record(|v| v[0].sin() * v[1].exp(), &x);
let (_, analytic_hv) = tape.hvp(&x, &v);
let fd_hv = finite_diff_hvp(&mut tape, &x, &v, 1e-5);
for i in 0..x.len() {
assert!(
(analytic_hv[i] - fd_hv[i]).abs() < 1e-4,
"hvp vs fd, component {}: analytic={}, fd={}",
i,
analytic_hv[i],
fd_hv[i]
);
}
}
fn rosenbrock<T: Scalar>(x: &[T]) -> T {
let one = T::from_f(<T::Float as num_traits::FromPrimitive>::from_f64(1.0).unwrap());
let hundred = T::from_f(<T::Float as num_traits::FromPrimitive>::from_f64(100.0).unwrap());
let mut sum = T::zero();
for i in 0..x.len() - 1 {
let t1 = one - x[i];
let t2 = x[i + 1] - x[i] * x[i];
sum = sum + t1 * t1 + hundred * t2 * t2;
}
sum
}
fn check_symmetry(hess: &[Vec<f64>], label: &str) {
let n = hess.len();
for i in 0..n {
for j in i + 1..n {
assert!(
(hess[i][j] - hess[j][i]).abs() < 1e-10,
"{} symmetry: H[{}][{}]={}, H[{}][{}]={}",
label,
i,
j,
hess[i][j],
j,
i,
hess[j][i]
);
}
}
}
#[test]
fn hessian_symmetry_cubic() {
let x = [1.5_f64, 2.0];
let (tape, _) = record(|v| v[0] * v[0] * v[1] + v[1] * v[1] * v[1], &x);
let (_, _, hess) = tape.hessian(&x);
check_symmetry(&hess, "cubic");
}
#[test]
fn hessian_symmetry_sin_exp() {
let x = [0.7_f64, 0.3];
let (tape, _) = record(|v| v[0].sin() * v[1].exp(), &x);
let (_, _, hess) = tape.hessian(&x);
check_symmetry(&hess, "sin*exp");
}
#[test]
fn hessian_symmetry_rosenbrock() {
let x = [1.5_f64, 2.0];
let (tape, _) = record(|v| rosenbrock(v), &x);
let (_, _, hess) = tape.hessian(&x);
check_symmetry(&hess, "rosenbrock");
}
#[test]
fn hessian_rosenbrock_analytic() {
let x = 1.5_f64;
let y = 2.0_f64;
let (tape, _) = record(|v| rosenbrock(v), &[x, y]);
let (_, _, hess) = tape.hessian(&[x, y]);
let h00 = 2.0 - 400.0 * y + 1200.0 * x * x;
let h01 = -400.0 * x;
let h11 = 200.0;
assert!(
(hess[0][0] - h00).abs() < 1e-10,
"H[0][0]: {} vs {}",
hess[0][0],
h00
);
assert!(
(hess[0][1] - h01).abs() < 1e-10,
"H[0][1]: {} vs {}",
hess[0][1],
h01
);
assert!(
(hess[1][0] - h01).abs() < 1e-10,
"H[1][0]: {} vs {}",
hess[1][0],
h01
);
assert!(
(hess[1][1] - h11).abs() < 1e-10,
"H[1][1]: {} vs {}",
hess[1][1],
h11
);
}
#[test]
fn tape_reuse_hvp() {
let (tape, _) = record(|v| v[0] * v[0] * v[1] + v[1] * v[1] * v[1], &[1.0_f64, 1.0]);
let (g1, hv1) = tape.hvp(&[1.5_f64, 2.0], &[1.0_f64, 0.0]);
let (g2, hv2) = tape.hvp(&[1.5_f64, 2.0], &[0.0_f64, 1.0]);
for i in 0..2 {
assert!(
(g1[i] - g2[i]).abs() < 1e-10,
"gradient mismatch at {}: {} vs {}",
i,
g1[i],
g2[i]
);
}
assert!((hv1[0] - 4.0_f64).abs() < 1e-10); assert!((hv1[1] - 3.0_f64).abs() < 1e-10); assert!((hv2[0] - 3.0_f64).abs() < 1e-10); assert!((hv2[1] - 12.0_f64).abs() < 1e-10); }
#[test]
fn gradient_from_hvp_matches_tape_gradient() {
let x = [1.5_f64, 2.0];
let (mut tape, _) = record(|v| rosenbrock(v), &x);
let tape_grad = tape.gradient(&x);
let (hvp_grad, _) = tape.hvp(&x, &[1.0_f64, 0.0]);
for i in 0..x.len() {
assert!(
(tape_grad[i] - hvp_grad[i]).abs() < 1e-10,
"gradient mismatch at {}: tape={}, hvp={}",
i,
tape_grad[i],
hvp_grad[i]
);
}
}
#[test]
fn hvp_with_buf_matches_hvp() {
let x = [1.5_f64, 2.0];
let v = [0.7_f64, -0.3];
let (tape, _) = record(|v| rosenbrock(v), &x);
let (grad1, hv1) = tape.hvp(&x, &v);
let mut dual_vals_buf = Vec::new();
let mut adjoint_buf = Vec::new();
let (grad2, hv2) = tape.hvp_with_buf(&x, &v, &mut dual_vals_buf, &mut adjoint_buf);
for i in 0..x.len() {
assert!((grad1[i] - grad2[i]).abs() < 1e-15);
assert!((hv1[i] - hv2[i]).abs() < 1e-15);
}
}
}
mod forward_over_forward {
use super::*;
#[test]
fn second_derivative_cubic() {
let x = Dual::<Dual<f64>>::variable(Dual::variable(2.0));
let y = x * x * x;
assert!((y.re.re - 8.0).abs() < 1e-10);
assert!((y.re.eps - 12.0).abs() < 1e-10);
assert!((y.eps.eps - 12.0).abs() < 1e-10);
}
#[test]
fn second_derivative_sin() {
let x_val = 1.0_f64;
let x = Dual::<Dual<f64>>::variable(Dual::variable(x_val));
let y = x.sin();
assert!((y.re.re - x_val.sin()).abs() < 1e-10);
assert!((y.re.eps - x_val.cos()).abs() < 1e-10);
assert!((y.eps.eps - (-x_val.sin())).abs() < 1e-10);
}
#[test]
fn second_derivative_exp() {
let x_val = 0.5_f64;
let x = Dual::<Dual<f64>>::variable(Dual::variable(x_val));
let y = x.exp();
let e = x_val.exp();
assert!((y.re.re - e).abs() < 1e-10);
assert!((y.re.eps - e).abs() < 1e-10);
assert!((y.eps.eps - e).abs() < 1e-10);
}
#[test]
fn second_derivative_ln() {
let x_val = 2.0_f64;
let x = Dual::<Dual<f64>>::variable(Dual::variable(x_val));
let y = x.ln();
assert!((y.re.re - x_val.ln()).abs() < 1e-10);
assert!((y.re.eps - 1.0 / x_val).abs() < 1e-10);
assert!((y.eps.eps - (-1.0 / (x_val * x_val))).abs() < 1e-10);
}
#[test]
fn second_derivative_sqrt() {
let x_val = 4.0_f64;
let x = Dual::<Dual<f64>>::variable(Dual::variable(x_val));
let y = x.sqrt();
assert!((y.re.re - 2.0).abs() < 1e-10);
assert!((y.re.eps - 0.25).abs() < 1e-10);
let expected_f2 = -1.0 / (4.0 * x_val.powf(1.5));
assert!((y.eps.eps - expected_f2).abs() < 1e-10);
}
#[test]
fn nested_dual_composition() {
let x_val = 0.5_f64;
let x = Dual::<Dual<f64>>::variable(Dual::variable(x_val));
let y = x.exp().sin();
let ex = x_val.exp();
let expected_f0 = ex.sin();
let expected_f1 = ex.cos() * ex;
let expected_f2 = ex * (ex.cos() - ex * ex.sin());
assert!((y.re.re - expected_f0).abs() < 1e-10);
assert!((y.re.eps - expected_f1).abs() < 1e-10);
assert!((y.eps.eps - expected_f2).abs() < 1e-8);
}
#[test]
fn scalar_trait_with_nested_dual() {
fn generic_square<T: Scalar>(x: T) -> T {
x * x
}
let x = Dual::<Dual<f64>>::variable(Dual::variable(3.0));
let y = generic_square(x);
assert!((y.re.re - 9.0).abs() < 1e-10);
assert!((y.re.eps - 6.0).abs() < 1e-10);
assert!((y.eps.eps - 2.0).abs() < 1e-10);
}
}