Skip to main content

thermite_dual/
lib.rs

1#![doc = include_str!("../README.md")]
2#![no_std]
3#![cfg_attr(docsrs, feature(doc_cfg))]
4
5use core::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Rem, RemAssign, Sub, SubAssign};
6
7use thermite::vector::ops::{MulAddAssignExt, MulAddExt, Square};
8
9pub mod ad;
10pub mod math;
11pub mod vector;
12
13#[cfg(feature = "special")]
14pub mod special;
15
16pub use ad::AutoDiff;
17pub use vector::DualFloatVector;
18
19/// A value usable as the primal/derivative storage of a [`Dual`].
20///
21/// Implemented for the scalar float elements (`f32`, `f64`) and for every
22/// Thermite float [`Vector`](thermite::prelude::Vector). This lets the core
23/// arithmetic be written once and reused both at the element level
24/// (`Dual<f32, N>`, the [`Element`](thermite::element::Element) of a dual
25/// vector) and at the vector level (`Dual<Vector<R>, N>`).
26///
27/// The scalar element impls only provide arithmetic,
28/// the transcendental math library requires a real
29/// [`FloatVector`](thermite::prelude::FloatVector) inner type.
30pub trait DualValue:
31    Copy
32    + Add<Output = Self>
33    + Sub<Output = Self>
34    + Mul<Output = Self>
35    + Div<Output = Self>
36    + Neg<Output = Self>
37    + MulAddExt<Self, Self, Output = Self>
38{
39    /// The additive identity in this value type.
40    const VAL_ZERO: Self;
41    /// The multiplicative identity in this value type.
42    const VAL_ONE: Self;
43
44    /// Truncate towards zero. Used to give [`Dual`] a (locally-correct) `Rem`.
45    fn val_trunc(self) -> Self;
46}
47
48impl DualValue for f32 {
49    const VAL_ZERO: Self = 0.0;
50    const VAL_ONE: Self = 1.0;
51
52    #[inline(always)]
53    fn val_trunc(self) -> Self {
54        thermite::register::FloatElement::trunc(self)
55    }
56}
57
58impl DualValue for f64 {
59    const VAL_ZERO: Self = 0.0;
60    const VAL_ONE: Self = 1.0;
61
62    #[inline(always)]
63    fn val_trunc(self) -> Self {
64        thermite::register::FloatElement::trunc(self)
65    }
66}
67
68impl<R: thermite::register::FloatRegister> DualValue for thermite::prelude::Vector<R> {
69    const VAL_ZERO: Self = <Self as thermite::prelude::NumericVector>::ZERO;
70    const VAL_ONE: Self = <Self as thermite::prelude::NumericVector>::ONE;
71
72    #[inline(always)]
73    fn val_trunc(self) -> Self {
74        thermite::prelude::FloatVector::trunc(self)
75    }
76}
77
78/// A multidual number: a primal value plus `N` first-order derivative parts.
79///
80/// See the [crate docs](crate) for the high-level idea. The derivative parts are
81/// indexed `0..N` and correspond to the `N` independent directions that were
82/// seeded into the computation.
83#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
84#[repr(C)]
85pub struct Dual<V, const N: usize> {
86    /// The primal value (the "real" part).
87    pub re: V,
88    /// The `N` first-order derivative components.
89    pub dual: [V; N],
90}
91
92impl<V: DualValue, const N: usize> Default for Dual<V, N> {
93    #[inline(always)]
94    fn default() -> Self {
95        Self::ZERO
96    }
97}
98
99impl<V: DualValue, const N: usize> Dual<V, N> {
100    /// A dual whose value and all derivatives are zero.
101    pub const ZERO: Self = Self {
102        re: V::VAL_ZERO,
103        dual: [V::VAL_ZERO; N],
104    };
105
106    /// A dual with value one and all-zero derivatives (a constant `1`).
107    pub const ONE: Self = Self {
108        re: V::VAL_ONE,
109        dual: [V::VAL_ZERO; N],
110    };
111
112    /// Create a dual from a primal value with all derivatives zero.
113    ///
114    /// Use this for *constants* in a differentiated computation -- values that do
115    /// not depend on any seeded variable.
116    #[inline(always)]
117    pub const fn constant(re: V) -> Self {
118        Self {
119            re,
120            dual: [V::VAL_ZERO; N],
121        }
122    }
123
124    /// Create a dual from a primal value and its full derivative vector.
125    #[inline(always)]
126    pub const fn new(re: V, dual: [V; N]) -> Self {
127        Self { re, dual }
128    }
129
130    /// Seed an independent variable: value `re`, with the `i`th derivative set to
131    /// one and the rest zero.
132    ///
133    /// This is how you introduce the `i`th input of an `N`-variable function so
134    /// that the result's `i`th dual part is the partial derivative with respect
135    /// to it.
136    ///
137    /// # Panics
138    /// If `i >= N`.
139    #[inline(always)]
140    pub fn variable(re: V, i: usize) -> Self {
141        assert!(i < N, "dual variable index {i} out of range for N = {N}");
142        let mut dual = [V::VAL_ZERO; N];
143        dual[i] = V::VAL_ONE;
144        Self { re, dual }
145    }
146
147    /// The primal value.
148    #[inline(always)]
149    pub const fn value(self) -> V {
150        self.re
151    }
152
153    /// The `N` derivative components.
154    #[inline(always)]
155    pub const fn gradient(self) -> [V; N] {
156        self.dual
157    }
158
159    /// Apply the chain rule for a unary function `f`: given the new primal
160    /// `f(re)` and the scalar derivative `factor = f'(re)`, propagate the
161    /// derivative parts as `factor * dual[i]`.
162    #[inline(always)]
163    pub fn chain(self, new_re: V, factor: V) -> Self {
164        let mut dual = self.dual;
165        let mut i = 0;
166        while i < N {
167            dual[i] = factor * dual[i];
168            i += 1;
169        }
170        Self { re: new_re, dual }
171    }
172}
173
174// --- Arithmetic: Dual op Dual ---
175
176impl<V: DualValue, const N: usize> Neg for Dual<V, N> {
177    type Output = Self;
178
179    #[inline(always)]
180    fn neg(self) -> Self {
181        let mut dual = self.dual;
182        let mut i = 0;
183        while i < N {
184            dual[i] = -dual[i];
185            i += 1;
186        }
187        Self { re: -self.re, dual }
188    }
189}
190
191impl<V: DualValue, const N: usize> Add for Dual<V, N> {
192    type Output = Self;
193
194    #[inline(always)]
195    fn add(self, rhs: Self) -> Self {
196        let mut dual = self.dual;
197        let mut i = 0;
198        while i < N {
199            dual[i] = dual[i] + rhs.dual[i];
200            i += 1;
201        }
202        Self {
203            re: self.re + rhs.re,
204            dual,
205        }
206    }
207}
208
209impl<V: DualValue, const N: usize> Sub for Dual<V, N> {
210    type Output = Self;
211
212    #[inline(always)]
213    fn sub(self, rhs: Self) -> Self {
214        let mut dual = self.dual;
215        let mut i = 0;
216        while i < N {
217            dual[i] = dual[i] - rhs.dual[i];
218            i += 1;
219        }
220        Self {
221            re: self.re - rhs.re,
222            dual,
223        }
224    }
225}
226
227impl<V: DualValue, const N: usize> Mul for Dual<V, N> {
228    type Output = Self;
229
230    // product rule: (a*b)' = a'b + ab'
231    #[allow(clippy::suspicious_arithmetic_impl)]
232    #[inline(always)]
233    fn mul(self, rhs: Self) -> Self {
234        let mut dual = self.dual;
235        let mut i = 0;
236        while i < N {
237            // a'b + ab'  =  fma(a, b', a'*b)
238            dual[i] = self.re.mul_adde(rhs.dual[i], self.dual[i] * rhs.re);
239            i += 1;
240        }
241        Self {
242            re: self.re * rhs.re,
243            dual,
244        }
245    }
246}
247
248impl<V: DualValue, const N: usize> Div for Dual<V, N> {
249    type Output = Self;
250
251    // quotient rule: (a/b)' = (a' - (a/b) b') / b
252    #[allow(clippy::suspicious_arithmetic_impl)]
253    #[inline(always)]
254    fn div(self, rhs: Self) -> Self {
255        let q = self.re / rhs.re;
256        // reciprocal of the denominator computed once: N divisions -> 1 div + N muls
257        let inv = V::VAL_ONE / rhs.re;
258        let mut dual = self.dual;
259        let mut i = 0;
260        while i < N {
261            // (a' - q*b') / b  =  fnma(q, b', a') * (1/b)
262            dual[i] = q.nmul_adde(rhs.dual[i], self.dual[i]) * inv;
263            i += 1;
264        }
265        Self { re: q, dual }
266    }
267}
268
269// --- Arithmetic: Dual op scalar value (constant, no derivative) ---
270
271impl<V: DualValue, const N: usize> Add<V> for Dual<V, N> {
272    type Output = Self;
273
274    #[inline(always)]
275    fn add(self, rhs: V) -> Self {
276        Self {
277            re: self.re + rhs,
278            dual: self.dual,
279        }
280    }
281}
282
283impl<V: DualValue, const N: usize> Sub<V> for Dual<V, N> {
284    type Output = Self;
285
286    #[inline(always)]
287    fn sub(self, rhs: V) -> Self {
288        Self {
289            re: self.re - rhs,
290            dual: self.dual,
291        }
292    }
293}
294
295impl<V: DualValue, const N: usize> Mul<V> for Dual<V, N> {
296    type Output = Self;
297
298    #[inline(always)]
299    fn mul(self, rhs: V) -> Self {
300        let mut dual = self.dual;
301        let mut i = 0;
302        while i < N {
303            dual[i] = dual[i] * rhs;
304            i += 1;
305        }
306        Self {
307            re: self.re * rhs,
308            dual,
309        }
310    }
311}
312
313impl<V: DualValue, const N: usize> Div<V> for Dual<V, N> {
314    type Output = Self;
315
316    #[inline(always)]
317    fn div(self, rhs: V) -> Self {
318        // single reciprocal, then multiply through
319        let inv = V::VAL_ONE / rhs;
320        let mut dual = self.dual;
321        let mut i = 0;
322        while i < N {
323            dual[i] = dual[i] * inv;
324            i += 1;
325        }
326        Self {
327            re: self.re / rhs,
328            dual,
329        }
330    }
331}
332
333// --- Assignment variants ---
334
335impl<V: DualValue, const N: usize, T> AddAssign<T> for Dual<V, N>
336where
337    Self: Add<T, Output = Self>,
338{
339    #[inline(always)]
340    fn add_assign(&mut self, rhs: T) {
341        *self = *self + rhs;
342    }
343}
344
345impl<V: DualValue, const N: usize, T> SubAssign<T> for Dual<V, N>
346where
347    Self: Sub<T, Output = Self>,
348{
349    #[inline(always)]
350    fn sub_assign(&mut self, rhs: T) {
351        *self = *self - rhs;
352    }
353}
354
355impl<V: DualValue, const N: usize, T> MulAssign<T> for Dual<V, N>
356where
357    Self: Mul<T, Output = Self>,
358{
359    #[inline(always)]
360    fn mul_assign(&mut self, rhs: T) {
361        *self = *self * rhs;
362    }
363}
364
365impl<V: DualValue, const N: usize, T> DivAssign<T> for Dual<V, N>
366where
367    Self: Div<T, Output = Self>,
368{
369    #[inline(always)]
370    fn div_assign(&mut self, rhs: T) {
371        *self = *self / rhs;
372    }
373}
374
375// --- Remainder ---
376//
377// `x % y = x - trunc(x/y) * y`. Treating the integer quotient `k = trunc(x/y)`
378// as locally constant gives the correct one-sided derivatives away from the
379// jump points: d/dx (x % y) = 1, d/dy (x % y) = -k.
380
381impl<V: DualValue, const N: usize> Rem for Dual<V, N> {
382    type Output = Self;
383
384    // x - k*y with k = trunc(x/y) constant. Single fused pass over the components
385    // instead of `self - rhs * k` (a scalar-mul pass followed by a subtract pass).
386    #[inline(always)]
387    fn rem(self, rhs: Self) -> Self {
388        let k = (self.re / rhs.re).val_trunc();
389        let mut dual = self.dual;
390        let mut i = 0;
391        while i < N {
392            dual[i] = k.nmul_adde(rhs.dual[i], self.dual[i]); // self.dual - k*rhs.dual
393            i += 1;
394        }
395        Self {
396            re: k.nmul_adde(rhs.re, self.re), // self.re - k*rhs.re
397            dual,
398        }
399    }
400}
401
402#[allow(clippy::suspicious_arithmetic_impl)]
403impl<V: DualValue, const N: usize> Rem<V> for Dual<V, N> {
404    type Output = Self;
405
406    #[inline(always)]
407    fn rem(self, rhs: V) -> Self {
408        let k = (self.re / rhs).val_trunc();
409        Self {
410            re: k.nmul_adde(rhs, self.re), // self.re - k*rhs; dual unchanged (derivative 1)
411            dual: self.dual,
412        }
413    }
414}
415
416impl<V: DualValue, const N: usize, T> RemAssign<T> for Dual<V, N>
417where
418    Self: Rem<T, Output = Self>,
419{
420    #[inline(always)]
421    fn rem_assign(&mut self, rhs: T) {
422        *self = *self % rhs;
423    }
424}
425
426// --- Fused multiply-add ---
427//
428// Each variant computes `self*a (+/-) b` on the primal and every derivative part
429// using the inner value type's fused multiply-add directly, rather than
430// composing the dual `Mul`/`Add` (which would round twice per component). The
431// derivative of `self*a` is `self.re*a' + self.dual*a.re` by the product rule,
432// so each part folds into two nested FMAs.
433//
434// There is no "true" hardware FMA for a multidual (each derivative part rounds
435// independently), so `HAS_TRUE_FMA` is false; the `_e` variants use the inner
436// estimating FMA while the exact variants use the inner exact FMA.
437
438macro_rules! dual_fma {
439    ($($name:ident => $re_op:ident, $outer:ident, $inner:ident);* $(;)?) => {
440        $(
441            #[inline(always)]
442            fn $name(self, a: Self, b: Self) -> Self {
443                let re = self.re.$re_op(a.re, b.re);
444                let mut dual = self.dual;
445                let mut i = 0;
446                while i < N {
447                    dual[i] = self.re.$outer(a.dual[i], self.dual[i].$inner(a.re, b.dual[i]));
448                    i += 1;
449                }
450                Self { re, dual }
451            }
452        )*
453    };
454}
455
456#[rustfmt::skip]
457impl<V: DualValue, const N: usize> MulAddExt<Self, Self> for Dual<V, N> {
458    type Output = Self;
459
460    const HAS_TRUE_FMA: bool = false;
461
462    dual_fma! {
463        mul_add   => mul_add,   mul_add,   mul_add;
464        mul_sub   => mul_sub,   mul_add,   mul_sub;
465        nmul_add  => nmul_add,  nmul_add,  nmul_add;
466        nmul_sub  => nmul_sub,  nmul_sub,  mul_add;
467        mul_adde  => mul_adde,  mul_adde,  mul_adde;
468        mul_sube  => mul_sube,  mul_adde,  mul_sube;
469        nmul_adde => nmul_adde, nmul_adde, nmul_adde;
470        nmul_sube => nmul_sube, nmul_sube, mul_adde;
471    }
472}
473
474#[rustfmt::skip]
475impl<V: DualValue, const N: usize, A, B> MulAddAssignExt<A, B> for Dual<V, N>
476where
477    Self: MulAddExt<A, B, Output = Self>,
478{
479    #[inline(always)] fn mul_add_assign(&mut self, a: A, b: B) { *self = self.mul_add(a, b); }
480    #[inline(always)] fn mul_sub_assign(&mut self, a: A, b: B) { *self = self.mul_sub(a, b); }
481    #[inline(always)] fn nmul_add_assign(&mut self, a: A, b: B) { *self = self.nmul_add(a, b); }
482    #[inline(always)] fn nmul_sub_assign(&mut self, a: A, b: B) { *self = self.nmul_sub(a, b); }
483    #[inline(always)] fn mul_adde_assign(&mut self, a: A, b: B) { *self = self.mul_adde(a, b); }
484    #[inline(always)] fn mul_sube_assign(&mut self, a: A, b: B) { *self = self.mul_sube(a, b); }
485    #[inline(always)] fn nmul_adde_assign(&mut self, a: A, b: B) { *self = self.nmul_adde(a, b); }
486    #[inline(always)] fn nmul_sube_assign(&mut self, a: A, b: B) { *self = self.nmul_sube(a, b); }
487}
488
489impl<V: DualValue, const N: usize> Square for Dual<V, N> {
490    type Output = Self;
491
492    // (x^2)' = 2 x x'. Cheaper than the general product rule `self * self`: one add + N muls
493    // instead of N (mul, fma) pairs, and no dependence on a splatted intermediate.
494    #[inline(always)]
495    fn square(self) -> Self {
496        let two_re = self.re + self.re;
497        let mut dual = self.dual;
498        let mut i = 0;
499        while i < N {
500            dual[i] = two_re * dual[i];
501            i += 1;
502        }
503        Self {
504            re: self.re * self.re,
505            dual,
506        }
507    }
508}
509
510// --- Nesting: a Dual can itself be the storage type of another Dual ---
511
512impl<V: DualValue, const N: usize> DualValue for Dual<V, N> {
513    const VAL_ZERO: Self = Self::ZERO;
514    const VAL_ONE: Self = Self::ONE;
515
516    #[inline(always)]
517    fn val_trunc(self) -> Self {
518        Self {
519            re: self.re.val_trunc(),
520            dual: self.dual,
521        }
522    }
523}