use core::fmt;
use core::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign};
use tang::Scalar;
#[derive(Clone, Copy, Debug)]
pub struct MultiDual<const N: usize> {
pub real: f64,
pub dual: [f64; N],
}
impl<const N: usize> MultiDual<N> {
#[inline]
pub fn constant(real: f64) -> Self {
Self {
real,
dual: [0.0; N],
}
}
#[inline]
pub fn var(real: f64, k: usize) -> Self {
assert!(k < N, "lane {k} out of range for MultiDual<{N}>");
let mut dual = [0.0; N];
dual[k] = 1.0;
Self { real, dual }
}
#[inline]
pub fn new(real: f64, dual: [f64; N]) -> Self {
Self { real, dual }
}
#[inline]
fn map(&self, f: impl Fn(f64) -> f64) -> [f64; N] {
core::array::from_fn(|k| f(self.dual[k]))
}
#[inline]
fn zip(&self, other: &Self, f: impl Fn(f64, f64) -> f64) -> [f64; N] {
core::array::from_fn(|k| f(self.dual[k], other.dual[k]))
}
}
impl<const N: usize> Default for MultiDual<N> {
fn default() -> Self {
Self::constant(0.0)
}
}
impl<const N: usize> fmt::Display for MultiDual<N> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.real)
}
}
impl<const N: usize> PartialEq for MultiDual<N> {
fn eq(&self, other: &Self) -> bool {
self.real == other.real
}
}
impl<const N: usize> PartialOrd for MultiDual<N> {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
self.real.partial_cmp(&other.real)
}
}
impl<const N: usize> Add for MultiDual<N> {
type Output = Self;
#[inline]
fn add(self, o: Self) -> Self {
Self {
real: self.real + o.real,
dual: self.zip(&o, |a, b| a + b),
}
}
}
impl<const N: usize> Sub for MultiDual<N> {
type Output = Self;
#[inline]
fn sub(self, o: Self) -> Self {
Self {
real: self.real - o.real,
dual: self.zip(&o, |a, b| a - b),
}
}
}
impl<const N: usize> Mul for MultiDual<N> {
type Output = Self;
#[inline]
#[allow(clippy::suspicious_arithmetic_impl)]
fn mul(self, o: Self) -> Self {
Self {
real: self.real * o.real,
dual: self.zip(&o, |a, b| a * o.real + self.real * b),
}
}
}
impl<const N: usize> Div for MultiDual<N> {
type Output = Self;
#[inline]
fn div(self, o: Self) -> Self {
let inv = 1.0 / o.real;
Self {
real: self.real * inv,
dual: self.zip(&o, |a, b| (a * o.real - self.real * b) * (inv * inv)),
}
}
}
impl<const N: usize> Neg for MultiDual<N> {
type Output = Self;
#[inline]
fn neg(self) -> Self {
Self {
real: -self.real,
dual: self.map(|a| -a),
}
}
}
impl<const N: usize> AddAssign for MultiDual<N> {
#[inline]
fn add_assign(&mut self, o: Self) {
*self = *self + o;
}
}
impl<const N: usize> SubAssign for MultiDual<N> {
#[inline]
fn sub_assign(&mut self, o: Self) {
*self = *self - o;
}
}
impl<const N: usize> MulAssign for MultiDual<N> {
#[inline]
fn mul_assign(&mut self, o: Self) {
*self = *self * o;
}
}
impl<const N: usize> DivAssign for MultiDual<N> {
#[inline]
fn div_assign(&mut self, o: Self) {
*self = *self / o;
}
}
impl<const N: usize> Scalar for MultiDual<N> {
const ZERO: Self = Self {
real: 0.0,
dual: [0.0; N],
};
const ONE: Self = Self {
real: 1.0,
dual: [0.0; N],
};
const TWO: Self = Self {
real: 2.0,
dual: [0.0; N],
};
const HALF: Self = Self {
real: 0.5,
dual: [0.0; N],
};
const PI: Self = Self {
real: core::f64::consts::PI,
dual: [0.0; N],
};
const TAU: Self = Self {
real: core::f64::consts::TAU,
dual: [0.0; N],
};
const FRAC_PI_2: Self = Self {
real: core::f64::consts::FRAC_PI_2,
dual: [0.0; N],
};
const EPSILON: Self = Self {
real: f64::EPSILON,
dual: [0.0; N],
};
const INFINITY: Self = Self {
real: f64::INFINITY,
dual: [0.0; N],
};
const NEG_INFINITY: Self = Self {
real: f64::NEG_INFINITY,
dual: [0.0; N],
};
#[inline]
fn sqrt(self) -> Self {
let r = self.real.sqrt();
Self {
real: r,
dual: self.map(|a| a / (2.0 * r)),
}
}
#[inline]
fn abs(self) -> Self {
let s = self.real.signum();
Self {
real: self.real.abs(),
dual: self.map(|a| a * s),
}
}
#[inline]
fn sin(self) -> Self {
let c = self.real.cos();
Self {
real: self.real.sin(),
dual: self.map(|a| a * c),
}
}
#[inline]
fn cos(self) -> Self {
let s = self.real.sin();
Self {
real: self.real.cos(),
dual: self.map(|a| -a * s),
}
}
#[inline]
fn tan(self) -> Self {
let c = self.real.cos();
Self {
real: self.real.tan(),
dual: self.map(|a| a / (c * c)),
}
}
#[inline]
fn asin(self) -> Self {
let d = (1.0 - self.real * self.real).sqrt();
Self {
real: self.real.asin(),
dual: self.map(|a| a / d),
}
}
#[inline]
fn acos(self) -> Self {
let d = (1.0 - self.real * self.real).sqrt();
Self {
real: self.real.acos(),
dual: self.map(|a| -a / d),
}
}
#[inline]
fn atan2(self, other: Self) -> Self {
let denom = self.real * self.real + other.real * other.real;
Self {
real: self.real.atan2(other.real),
dual: self.zip(&other, |a, b| (a * other.real - self.real * b) / denom),
}
}
#[inline]
fn sin_cos(self) -> (Self, Self) {
let (s, c) = self.real.sin_cos();
(
Self {
real: s,
dual: self.map(|a| a * c),
},
Self {
real: c,
dual: self.map(|a| -a * s),
},
)
}
#[inline]
fn min(self, other: Self) -> Self {
if self.real < other.real { self } else { other }
}
#[inline]
fn max(self, other: Self) -> Self {
if self.real > other.real { self } else { other }
}
#[inline]
fn clamp(self, lo: Self, hi: Self) -> Self {
self.max(lo).min(hi)
}
#[inline]
fn recip(self) -> Self {
let inv = self.real.recip();
Self {
real: inv,
dual: self.map(|a| -a * inv * inv),
}
}
#[inline]
fn powi(self, n: i32) -> Self {
let r = self.real.powi(n);
let d = n as f64 * self.real.powi(n - 1);
Self {
real: r,
dual: self.map(|a| a * d),
}
}
#[inline]
fn copysign(self, sign: Self) -> Self {
let flipped = self.real.signum() != sign.real.signum();
Self {
real: self.real.copysign(sign.real),
dual: self.map(|a| if flipped { -a } else { a }),
}
}
#[inline]
fn signum(self) -> Self {
Self::constant(self.real.signum())
}
#[inline]
fn floor(self) -> Self {
Self::constant(self.real.floor())
}
#[inline]
fn ceil(self) -> Self {
Self::constant(self.real.ceil())
}
#[inline]
fn round(self) -> Self {
Self::constant(self.real.round())
}
#[inline]
fn exp(self) -> Self {
let e = self.real.exp();
Self {
real: e,
dual: self.map(|a| a * e),
}
}
#[inline]
fn ln(self) -> Self {
Self {
real: self.real.ln(),
dual: self.map(|a| a / self.real),
}
}
#[inline]
fn powf(self, p: Self) -> Self {
let val = self.real.powf(p.real);
let ln_x = self.real.ln();
Self {
real: val,
dual: self.zip(&p, |a, b| val * (b * ln_x + p.real * a / self.real)),
}
}
#[inline]
fn tanh(self) -> Self {
let t = self.real.tanh();
Self {
real: t,
dual: self.map(|a| a * (1.0 - t * t)),
}
}
#[inline]
fn sinh(self) -> Self {
let c = self.real.cosh();
Self {
real: self.real.sinh(),
dual: self.map(|a| a * c),
}
}
#[inline]
fn cosh(self) -> Self {
let s = self.real.sinh();
Self {
real: self.real.cosh(),
dual: self.map(|a| a * s),
}
}
#[inline]
fn acosh(self) -> Self {
let d = (self.real * self.real - 1.0).sqrt();
Self {
real: self.real.acosh(),
dual: self.map(|a| a / d),
}
}
#[inline]
fn asinh(self) -> Self {
let d = (self.real * self.real + 1.0).sqrt();
Self {
real: self.real.asinh(),
dual: self.map(|a| a / d),
}
}
#[inline]
fn atanh(self) -> Self {
let d = 1.0 - self.real * self.real;
Self {
real: self.real.atanh(),
dual: self.map(|a| a / d),
}
}
#[inline]
fn from_f64(v: f64) -> Self {
Self::constant(v)
}
#[inline]
fn to_f64(self) -> f64 {
self.real
}
#[inline]
fn from_i32(v: i32) -> Self {
Self::constant(v as f64)
}
#[inline]
fn select(cond: Self, a: Self, b: Self) -> Self {
if cond.real > 0.0 { a } else { b }
}
}
pub const SUPPORTED_WIDTHS: [usize; 5] = [1, 2, 4, 8, 16];
pub fn width_for(lanes: usize) -> usize {
*SUPPORTED_WIDTHS
.iter()
.find(|&&w| w >= lanes)
.unwrap_or(SUPPORTED_WIDTHS.last().expect("non-empty"))
}
pub fn for_lanes<Op: LaneOp>(lanes: usize, op: Op) -> Op::Out {
match width_for(lanes) {
1 => op.call::<1>(),
2 => op.call::<2>(),
4 => op.call::<4>(),
8 => op.call::<8>(),
_ => op.call::<16>(),
}
}
pub trait LaneOp {
type Out;
fn call<const N: usize>(self) -> Self::Out;
}
#[cfg(test)]
mod tests {
use super::*;
use tang::Dual;
fn workload<S: Scalar>(x: S, y: S) -> S {
let a = x * y + x / y - y;
let b = a.sin() * a.cos() + a.tan();
let c = (b * b + S::ONE).sqrt().ln();
let d = c.exp() * x.powi(3) + y.powf(S::from_f64(1.7));
let e = d.recip() + d.abs() - d.tanh() + d.sinh() * d.cosh();
let f = e.atan2(x) + x.max(y) - x.min(y) + e.signum() * e;
f + x.asin() + y.acos() + (x * S::TWO).asinh() + (y + S::TWO).acosh() + x.atanh()
}
#[test]
fn lanes_match_scalar_dual_bitwise() {
let (x0, y0) = (0.37, 0.61);
let dx = workload(Dual::var(x0), Dual::constant(y0));
let dy = workload(Dual::constant(x0), Dual::var(y0));
let m = workload(MultiDual::<2>::var(x0, 0), MultiDual::<2>::var(y0, 1));
assert_eq!(m.real.to_bits(), dx.real.to_bits(), "value");
assert_eq!(m.dual[0].to_bits(), dx.dual.to_bits(), "lane 0 vs ∂/∂x");
assert_eq!(m.dual[1].to_bits(), dy.dual.to_bits(), "lane 1 vs ∂/∂y");
}
#[test]
fn width_does_not_change_the_answer() {
let (x0, y0) = (0.37, 0.61);
let w1 = workload(MultiDual::<1>::var(x0, 0), MultiDual::<1>::constant(y0));
let w4 = workload(MultiDual::<4>::var(x0, 0), MultiDual::<4>::constant(y0));
let w16 = workload(MultiDual::<16>::var(x0, 0), MultiDual::<16>::constant(y0));
assert_eq!(w1.dual[0].to_bits(), w4.dual[0].to_bits());
assert_eq!(w1.dual[0].to_bits(), w16.dual[0].to_bits());
}
#[test]
fn unseeded_lanes_stay_zero() {
let m = workload(MultiDual::<8>::var(0.37, 0), MultiDual::<8>::constant(0.61));
for (k, d) in m.dual.iter().enumerate().skip(1) {
assert_eq!(*d, 0.0, "lane {k} picked up a nonzero derivative");
}
}
#[test]
fn width_dispatch_picks_the_narrowest_cover() {
assert_eq!(width_for(0), 1);
assert_eq!(width_for(1), 1);
assert_eq!(width_for(2), 2);
assert_eq!(width_for(3), 4);
assert_eq!(width_for(8), 8);
assert_eq!(width_for(9), 16);
assert_eq!(width_for(17), 16);
assert_eq!(width_for(1000), 16);
}
#[test]
fn for_lanes_reaches_the_expected_width() {
struct Width;
impl LaneOp for Width {
type Out = usize;
fn call<const N: usize>(self) -> usize {
N
}
}
assert_eq!(for_lanes(1, Width), 1);
assert_eq!(for_lanes(5, Width), 8);
assert_eq!(for_lanes(40, Width), 16);
}
#[test]
fn derivatives_are_correct_not_merely_consistent() {
let (x, y) = (1.3_f64, 0.7_f64);
let f = MultiDual::<2>::var(x, 0).powi(3) * MultiDual::<2>::var(y, 1).sin();
assert!((f.real - x.powi(3) * y.sin()).abs() < 1e-14);
assert!((f.dual[0] - 3.0 * x * x * y.sin()).abs() < 1e-14);
assert!((f.dual[1] - x.powi(3) * y.cos()).abs() < 1e-14);
}
}