pub trait JetField: Clone {
fn value(&self) -> f64;
fn add(&self, o: &Self) -> Self;
fn sub(&self, o: &Self) -> Self;
fn mul(&self, o: &Self) -> Self;
fn neg(&self) -> Self;
fn scale(&self, s: f64) -> Self;
fn compose_unary(&self, d: [f64; 5]) -> Self;
fn constant_like(&self, v: f64) -> Self {
self.compose_unary([v, 0.0, 0.0, 0.0, 0.0])
}
fn with_value(&self, v: f64) -> Self {
self.sub(&self.constant_like(self.value()))
.add(&self.constant_like(v))
}
}
pub trait JetFieldConst: JetField + Copy {
fn from_f64(x: f64) -> Self;
}
impl JetField for f64 {
#[inline]
fn value(&self) -> f64 {
*self
}
#[inline]
fn add(&self, o: &Self) -> Self {
*self + *o
}
#[inline]
fn sub(&self, o: &Self) -> Self {
*self - *o
}
#[inline]
fn mul(&self, o: &Self) -> Self {
*self * *o
}
#[inline]
fn neg(&self) -> Self {
-*self
}
#[inline]
fn scale(&self, s: f64) -> Self {
*self * s
}
#[inline]
fn compose_unary(&self, d: [f64; 5]) -> Self {
d[0]
}
#[inline]
fn constant_like(&self, v: f64) -> Self {
v
}
#[inline]
fn with_value(&self, v: f64) -> Self {
v
}
}
impl JetFieldConst for f64 {
#[inline]
fn from_f64(x: f64) -> Self {
x
}
}
#[derive(Clone, Copy, Debug)]
pub struct Dual2<S: JetField> {
pub v: S,
pub g: S,
pub h: S,
}
impl<S: JetFieldConst> Dual2<S> {
#[inline]
pub fn constant(v: S) -> Self {
Self {
v,
g: S::from_f64(0.0),
h: S::from_f64(0.0),
}
}
#[inline]
pub fn variable(v: S) -> Self {
Self {
v,
g: S::from_f64(1.0),
h: S::from_f64(0.0),
}
}
}
impl<S: JetField> JetField for Dual2<S> {
#[inline]
fn value(&self) -> f64 {
self.v.value()
}
#[inline]
fn add(&self, o: &Self) -> Self {
Self {
v: self.v.add(&o.v),
g: self.g.add(&o.g),
h: self.h.add(&o.h),
}
}
#[inline]
fn sub(&self, o: &Self) -> Self {
Self {
v: self.v.sub(&o.v),
g: self.g.sub(&o.g),
h: self.h.sub(&o.h),
}
}
#[inline]
fn mul(&self, o: &Self) -> Self {
Self {
v: self.v.mul(&o.v),
g: self.v.mul(&o.g).add(&self.g.mul(&o.v)),
h: self
.v
.mul(&o.h)
.add(&self.g.mul(&o.g).scale(2.0))
.add(&self.h.mul(&o.v)),
}
}
#[inline]
fn neg(&self) -> Self {
Self {
v: self.v.neg(),
g: self.g.neg(),
h: self.h.neg(),
}
}
#[inline]
fn scale(&self, s: f64) -> Self {
Self {
v: self.v.scale(s),
g: self.g.scale(s),
h: self.h.scale(s),
}
}
#[inline]
fn compose_unary(&self, d: [f64; 5]) -> Self {
let f0 = self.v.compose_unary([d[0], d[1], d[2], d[3], d[4]]);
let f1 = self.v.compose_unary([d[1], d[2], d[3], d[4], 0.0]);
let f2 = self.v.compose_unary([d[2], d[3], d[4], 0.0, 0.0]);
Self {
v: f0,
g: f1.mul(&self.g),
h: f1.mul(&self.h).add(&f2.mul(&self.g).mul(&self.g)),
}
}
#[inline]
fn constant_like(&self, v: f64) -> Self {
Self {
v: self.v.constant_like(v),
g: self.v.constant_like(0.0),
h: self.v.constant_like(0.0),
}
}
#[inline]
fn with_value(&self, v: f64) -> Self {
Self {
v: self.v.with_value(v),
g: self.g.clone(),
h: self.h.clone(),
}
}
}
impl<S: JetFieldConst> JetFieldConst for Dual2<S> {
#[inline]
fn from_f64(x: f64) -> Self {
Self::constant(S::from_f64(x))
}
}
pub type Dual22 = Dual2<Dual2<f64>>;
impl Dual22 {
#[inline]
pub fn seed_outer(x: f64) -> Self {
Dual2::variable(Dual2::<f64>::constant(x))
}
#[inline]
pub fn seed_inner(x: f64) -> Self {
Dual2::constant(Dual2::<f64>::variable(x))
}
#[inline]
pub fn seed_directional(base: f64, d1: f64, d2: f64) -> Self {
Dual2 {
v: Dual2::<f64> {
v: base,
g: d2,
h: 0.0,
},
g: Dual2::<f64>::constant(d1),
h: Dual2::<f64>::constant(0.0),
}
}
#[inline]
pub fn from_channels(c: [f64; 9]) -> Self {
Dual2 {
v: Dual2::<f64> {
v: c[0],
g: c[2],
h: c[5],
},
g: Dual2::<f64> {
v: c[1],
g: c[4],
h: c[7],
},
h: Dual2::<f64> {
v: c[3],
g: c[6],
h: c[8],
},
}
}
#[inline]
pub fn channels(&self) -> [f64; 9] {
[
self.v.v, self.g.v, self.v.g, self.h.v, self.g.g, self.v.h, self.h.g, self.g.h,
self.h.h,
]
}
}
#[cfg(test)]
mod nested_dual_tower4_oracle_tests {
use super::*;
use crate::jet_tower::Tower4;
impl<const K: usize> JetFieldConst for Tower4<K> {
fn from_f64(x: f64) -> Self {
Tower4::constant(x)
}
}
fn exp_stack(u: f64) -> [f64; 5] {
let e = u.exp();
[e, e, e, e, e]
}
fn ln_stack(u: f64) -> [f64; 5] {
let r = 1.0 / u;
[u.ln(), r, -r * r, 2.0 * r * r * r, -6.0 * r * r * r * r]
}
fn program<J: JetFieldConst>(p0: &J, p1: &J) -> J {
let one = J::from_f64(1.0);
let arg_e = p0.mul(p1).add(&p0.scale(0.3));
let term_exp = arg_e.compose_unary(exp_stack(arg_e.value()));
let arg_l = one
.add(&p0.mul(p0))
.add(&p1.mul(p1).scale(0.5))
.add(&p0.mul(p1).scale(0.2));
let term_ln = arg_l.compose_unary(ln_stack(arg_l.value()));
let diff = p0.sub(p1);
let term_quad = diff.mul(&diff).scale(-0.7);
term_exp.add(&term_ln).add(&term_quad)
}
#[test]
fn nested_dual2_reproduces_tower4_channels_932() {
let points = [
(0.31_f64, -0.42_f64),
(-0.85, 0.17),
(0.05, 0.93),
(1.2, -0.6),
];
let mut max_rel = 0.0_f64;
for &(x0, x1) in &points {
let t0 = Tower4::<2>::variable(x0, 0);
let t1 = Tower4::<2>::variable(x1, 1);
let tower = program(&t0, &t1);
let d0 = Dual22::seed_outer(x0);
let d1 = Dual22::seed_inner(x1);
let nested = program(&d0, &d1);
let ch = nested.channels();
let cmp = [
("value", ch[0], tower.v),
("d_a", ch[1], tower.g[0]),
("d_b", ch[2], tower.g[1]),
("d_aa", ch[3], tower.h[0][0]),
("d_ab", ch[4], tower.h[0][1]),
("d_bb", ch[5], tower.h[1][1]),
("d_aab", ch[6], tower.t3[0][0][1]),
("d_abb", ch[7], tower.t3[0][1][1]),
("d_aabb", ch[8], tower.t4[0][0][1][1]),
];
for (label, got, want) in cmp {
let rel = (got - want).abs() / want.abs().max(1.0);
max_rel = max_rel.max(rel);
assert!(
rel <= 1e-12,
"point ({x0},{x1}) channel {label}: nested {got:.16e} != tower {want:.16e} (rel {rel:.3e})"
);
}
}
eprintln!(
"[nested-dual #932] Dual2<Dual2> vs Tower4<2> max_rel over 4 points = {max_rel:.3e}"
);
}
#[test]
fn nested_dual2_directional_matches_tower4_contraction_932() {
let d1 = [0.7_f64, -0.3_f64];
let d2 = [0.4_f64, 0.9_f64];
let points = [(0.31_f64, -0.42_f64), (-0.85, 0.17), (1.2, -0.6)];
let mut max_rel = 0.0_f64;
for &(x0, x1) in &points {
let t0 = Tower4::<2>::variable(x0, 0);
let t1 = Tower4::<2>::variable(x1, 1);
let tower = program(&t0, &t1);
let mut c_s = 0.0;
let mut c_t = 0.0;
let mut c_st = 0.0;
let mut c_sstt = 0.0;
for a in 0..2 {
c_s += tower.g[a] * d1[a];
c_t += tower.g[a] * d2[a];
for b in 0..2 {
c_st += tower.h[a][b] * d1[a] * d2[b];
for cc in 0..2 {
for dd in 0..2 {
c_sstt += tower.t4[a][b][cc][dd] * d1[a] * d1[b] * d2[cc] * d2[dd];
}
}
}
}
let p0 = Dual22::seed_directional(x0, d1[0], d2[0]);
let p1 = Dual22::seed_directional(x1, d1[1], d2[1]);
let ch = program(&p0, &p1).channels();
for (label, got, want) in [
("d_s", ch[1], c_s),
("d_t", ch[2], c_t),
("d_st", ch[4], c_st),
("d_sstt", ch[8], c_sstt),
] {
let rel = (got - want).abs() / want.abs().max(1.0);
max_rel = max_rel.max(rel);
assert!(
rel <= 1e-12,
"point ({x0},{x1}) {label}: nested {got:.16e} != tower-contraction {want:.16e} (rel {rel:.3e})"
);
}
}
eprintln!(
"[nested-dual #932] directional Dual2<Dual2> vs Tower4<2> contraction max_rel = {max_rel:.3e}"
);
}
#[test]
fn nested_dual2_channels_from_channels_roundtrip_932() {
let c = [1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0];
let round = Dual22::from_channels(c).channels();
assert_eq!(round, c, "channels∘from_channels must be identity");
}
#[test]
fn nested_dual2_seed_swap_symmetry_932() {
let (x0, x1) = (0.4_f64, -0.55_f64);
let d0 = Dual22::seed_outer(x0);
let d1 = Dual22::seed_inner(x1);
let ab = program(&d0, &d1).channels();
let e0 = Dual22::seed_inner(x0);
let e1 = Dual22::seed_outer(x1);
let ba = program(&e0, &e1).channels();
let close = |a: f64, b: f64| (a - b).abs() <= 1e-12 * (1.0 + b.abs());
assert!(close(ab[0], ba[0]), "value not seed-order invariant");
assert!(close(ab[8], ba[8]), "d_aabb not seed-order invariant");
assert!(close(ab[1], ba[2]), "d_a (ab) != d_b (ba)");
assert!(close(ab[6], ba[7]), "d_aab (ab) != d_abb (ba)");
}
}