use std::sync::Arc;
use echidna::bytecode_tape::{BtapeGuard, BytecodeTape, CustomOp, CustomOpHandle};
use echidna::{BReverse, Dual};
use echidna_optim::{implicit_hessian, implicit_hvp, implicit_jacobian, piggyback_tangent_step};
struct Cube;
impl CustomOp<f64> for Cube {
fn eval(&self, a: f64, _b: f64) -> f64 {
a * a * a
}
fn partials(&self, a: f64, _b: f64, _r: f64) -> (f64, f64) {
(3.0 * a * a, 0.0)
}
fn eval_dual(&self, a: Dual<f64>, _b: Dual<f64>) -> Dual<f64> {
a * a * a
}
fn partials_dual(&self, a: Dual<f64>, _b: Dual<f64>, _r: Dual<f64>) -> (Dual<f64>, Dual<f64>) {
(Dual::constant(3.0) * a * a, Dual::constant(0.0))
}
}
fn record_residual(
inputs_primal: &[f64],
ops: Vec<Arc<dyn CustomOp<f64>>>,
f: impl FnOnce(&[BReverse<f64>], &[CustomOpHandle], &[f64]) -> BReverse<f64>,
) -> BytecodeTape<f64> {
let mut tape = BytecodeTape::with_capacity(inputs_primal.len() * 10);
let handles: Vec<CustomOpHandle> = ops.into_iter().map(|op| tape.register_custom(op)).collect();
let inputs: Vec<BReverse<f64>> = inputs_primal
.iter()
.map(|&val| {
let idx = tape.new_input(val);
BReverse::from_tape(val, idx)
})
.collect();
let output = {
let _guard = BtapeGuard::new(&mut tape);
f(&inputs, &handles, inputs_primal)
};
tape.set_output(output.index());
tape
}
fn cube_residual_tape() -> BytecodeTape<f64> {
record_residual(&[2.0, 8.0], vec![Arc::new(Cube)], |v, h, xv| {
let c = v[0].custom_unary(h[0], xv[0] * xv[0] * xv[0]);
c - v[1]
})
}
#[test]
fn implicit_jacobian_through_custom_op() {
let mut tape = cube_residual_tape();
let jac = implicit_jacobian(&mut tape, &[2.0], &[8.0], 1).unwrap();
assert!(
(jac[0][0] - 1.0 / 12.0).abs() < 1e-12,
"dz/dx = {}, expected 1/12",
jac[0][0]
);
}
#[test]
fn implicit_hvp_through_custom_op() {
let mut tape = cube_residual_tape();
let h = implicit_hvp(&mut tape, &[2.0], &[8.0], &[1.0], &[1.0], 1).unwrap();
assert!(
(h[0] - (-1.0 / 144.0)).abs() < 1e-12,
"h = {}, expected -1/144",
h[0]
);
}
#[test]
fn implicit_hessian_through_custom_op() {
let mut tape = cube_residual_tape();
let hess = implicit_hessian(&mut tape, &[2.0], &[8.0], 1).unwrap();
assert!(
(hess[0][0][0] - (-1.0 / 144.0)).abs() < 1e-12,
"d²z/dx² = {}, expected -1/144",
hess[0][0][0]
);
}
#[test]
fn implicit_hvp_custom_op_away_from_recording_point() {
let mut tape = cube_residual_tape();
let h = implicit_hvp(&mut tape, &[3.0], &[27.0], &[1.0], &[1.0], 1).unwrap();
assert!(
(h[0] - (-2.0 / 2187.0)).abs() < 1e-12,
"h = {}, expected -2/2187",
h[0]
);
}
#[test]
fn implicit_hvp_composed_custom_op_away_from_recording_point() {
let mut tape = record_residual(&[2.0, 8.0], vec![Arc::new(Cube)], |v, h, xv| {
use num_traits::Float as _;
let c = v[0].custom_unary(h[0], xv[0] * xv[0] * xv[0]);
c.sin() - v[1].sin()
});
let h = implicit_hvp(&mut tape, &[3.0], &[27.0], &[1.0], &[1.0], 1).unwrap();
assert!(
(h[0] - (-2.0 / 2187.0)).abs() < 1e-12,
"h = {}, expected -2/2187",
h[0]
);
}
#[test]
fn piggyback_tangent_through_custom_op_away_from_recording_point() {
let tape = record_residual(&[2.0, 8.0], vec![Arc::new(Cube)], |v, h, xv| {
let c = v[0].custom_unary(h[0], xv[0] * xv[0] * xv[0]);
c + v[1]
});
let (z_new, z_dot_new) = piggyback_tangent_step(&tape, &[3.0], &[5.0], &[1.0], &[0.0], 1);
assert!(
(z_new[0] - 32.0).abs() < 1e-12,
"z_new = {}, expected 32",
z_new[0]
);
assert!(
(z_dot_new[0] - 27.0).abs() < 1e-12,
"ż_new = {}, expected 27",
z_dot_new[0]
);
}