1use core::fmt;
52use core::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign};
53use tang::Scalar;
54
55#[derive(Clone, Copy, Debug)]
60pub struct MultiDual<const N: usize> {
61 pub real: f64,
63 pub dual: [f64; N],
65}
66
67impl<const N: usize> MultiDual<N> {
68 #[inline]
70 pub fn constant(real: f64) -> Self {
71 Self {
72 real,
73 dual: [0.0; N],
74 }
75 }
76
77 #[inline]
82 pub fn var(real: f64, k: usize) -> Self {
83 assert!(k < N, "lane {k} out of range for MultiDual<{N}>");
84 let mut dual = [0.0; N];
85 dual[k] = 1.0;
86 Self { real, dual }
87 }
88
89 #[inline]
91 pub fn new(real: f64, dual: [f64; N]) -> Self {
92 Self { real, dual }
93 }
94
95 #[inline]
97 fn map(&self, f: impl Fn(f64) -> f64) -> [f64; N] {
98 core::array::from_fn(|k| f(self.dual[k]))
99 }
100
101 #[inline]
103 fn zip(&self, other: &Self, f: impl Fn(f64, f64) -> f64) -> [f64; N] {
104 core::array::from_fn(|k| f(self.dual[k], other.dual[k]))
105 }
106}
107
108impl<const N: usize> Default for MultiDual<N> {
109 fn default() -> Self {
110 Self::constant(0.0)
111 }
112}
113
114impl<const N: usize> fmt::Display for MultiDual<N> {
115 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116 write!(f, "{}", self.real)
117 }
118}
119
120impl<const N: usize> PartialEq for MultiDual<N> {
124 fn eq(&self, other: &Self) -> bool {
125 self.real == other.real
126 }
127}
128
129impl<const N: usize> PartialOrd for MultiDual<N> {
130 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
131 self.real.partial_cmp(&other.real)
132 }
133}
134
135impl<const N: usize> Add for MultiDual<N> {
136 type Output = Self;
137 #[inline]
138 fn add(self, o: Self) -> Self {
139 Self {
140 real: self.real + o.real,
141 dual: self.zip(&o, |a, b| a + b),
142 }
143 }
144}
145
146impl<const N: usize> Sub for MultiDual<N> {
147 type Output = Self;
148 #[inline]
149 fn sub(self, o: Self) -> Self {
150 Self {
151 real: self.real - o.real,
152 dual: self.zip(&o, |a, b| a - b),
153 }
154 }
155}
156
157impl<const N: usize> Mul for MultiDual<N> {
158 type Output = Self;
159 #[inline]
160 #[allow(clippy::suspicious_arithmetic_impl)]
163 fn mul(self, o: Self) -> Self {
164 Self {
167 real: self.real * o.real,
168 dual: self.zip(&o, |a, b| a * o.real + self.real * b),
169 }
170 }
171}
172
173impl<const N: usize> Div for MultiDual<N> {
174 type Output = Self;
175 #[inline]
176 fn div(self, o: Self) -> Self {
177 let inv = 1.0 / o.real;
178 Self {
179 real: self.real * inv,
180 dual: self.zip(&o, |a, b| (a * o.real - self.real * b) * (inv * inv)),
181 }
182 }
183}
184
185impl<const N: usize> Neg for MultiDual<N> {
186 type Output = Self;
187 #[inline]
188 fn neg(self) -> Self {
189 Self {
190 real: -self.real,
191 dual: self.map(|a| -a),
192 }
193 }
194}
195
196impl<const N: usize> AddAssign for MultiDual<N> {
197 #[inline]
198 fn add_assign(&mut self, o: Self) {
199 *self = *self + o;
200 }
201}
202impl<const N: usize> SubAssign for MultiDual<N> {
203 #[inline]
204 fn sub_assign(&mut self, o: Self) {
205 *self = *self - o;
206 }
207}
208impl<const N: usize> MulAssign for MultiDual<N> {
209 #[inline]
210 fn mul_assign(&mut self, o: Self) {
211 *self = *self * o;
212 }
213}
214impl<const N: usize> DivAssign for MultiDual<N> {
215 #[inline]
216 fn div_assign(&mut self, o: Self) {
217 *self = *self / o;
218 }
219}
220
221impl<const N: usize> Scalar for MultiDual<N> {
222 const ZERO: Self = Self {
223 real: 0.0,
224 dual: [0.0; N],
225 };
226 const ONE: Self = Self {
227 real: 1.0,
228 dual: [0.0; N],
229 };
230 const TWO: Self = Self {
231 real: 2.0,
232 dual: [0.0; N],
233 };
234 const HALF: Self = Self {
235 real: 0.5,
236 dual: [0.0; N],
237 };
238 const PI: Self = Self {
239 real: core::f64::consts::PI,
240 dual: [0.0; N],
241 };
242 const TAU: Self = Self {
243 real: core::f64::consts::TAU,
244 dual: [0.0; N],
245 };
246 const FRAC_PI_2: Self = Self {
247 real: core::f64::consts::FRAC_PI_2,
248 dual: [0.0; N],
249 };
250 const EPSILON: Self = Self {
251 real: f64::EPSILON,
252 dual: [0.0; N],
253 };
254 const INFINITY: Self = Self {
255 real: f64::INFINITY,
256 dual: [0.0; N],
257 };
258 const NEG_INFINITY: Self = Self {
259 real: f64::NEG_INFINITY,
260 dual: [0.0; N],
261 };
262
263 #[inline]
264 fn sqrt(self) -> Self {
265 let r = self.real.sqrt();
266 Self {
267 real: r,
268 dual: self.map(|a| a / (2.0 * r)),
269 }
270 }
271
272 #[inline]
273 fn abs(self) -> Self {
274 let s = self.real.signum();
275 Self {
276 real: self.real.abs(),
277 dual: self.map(|a| a * s),
278 }
279 }
280
281 #[inline]
282 fn sin(self) -> Self {
283 let c = self.real.cos();
284 Self {
285 real: self.real.sin(),
286 dual: self.map(|a| a * c),
287 }
288 }
289
290 #[inline]
291 fn cos(self) -> Self {
292 let s = self.real.sin();
293 Self {
294 real: self.real.cos(),
295 dual: self.map(|a| -a * s),
296 }
297 }
298
299 #[inline]
300 fn tan(self) -> Self {
301 let c = self.real.cos();
302 Self {
303 real: self.real.tan(),
304 dual: self.map(|a| a / (c * c)),
305 }
306 }
307
308 #[inline]
309 fn asin(self) -> Self {
310 let d = (1.0 - self.real * self.real).sqrt();
311 Self {
312 real: self.real.asin(),
313 dual: self.map(|a| a / d),
314 }
315 }
316
317 #[inline]
318 fn acos(self) -> Self {
319 let d = (1.0 - self.real * self.real).sqrt();
320 Self {
321 real: self.real.acos(),
322 dual: self.map(|a| -a / d),
323 }
324 }
325
326 #[inline]
327 fn atan2(self, other: Self) -> Self {
328 let denom = self.real * self.real + other.real * other.real;
329 Self {
330 real: self.real.atan2(other.real),
331 dual: self.zip(&other, |a, b| (a * other.real - self.real * b) / denom),
332 }
333 }
334
335 #[inline]
336 fn sin_cos(self) -> (Self, Self) {
337 let (s, c) = self.real.sin_cos();
338 (
339 Self {
340 real: s,
341 dual: self.map(|a| a * c),
342 },
343 Self {
344 real: c,
345 dual: self.map(|a| -a * s),
346 },
347 )
348 }
349
350 #[inline]
351 fn min(self, other: Self) -> Self {
352 if self.real < other.real { self } else { other }
353 }
354
355 #[inline]
356 fn max(self, other: Self) -> Self {
357 if self.real > other.real { self } else { other }
358 }
359
360 #[inline]
361 fn clamp(self, lo: Self, hi: Self) -> Self {
362 self.max(lo).min(hi)
363 }
364
365 #[inline]
366 fn recip(self) -> Self {
367 let inv = self.real.recip();
368 Self {
369 real: inv,
370 dual: self.map(|a| -a * inv * inv),
371 }
372 }
373
374 #[inline]
375 fn powi(self, n: i32) -> Self {
376 let r = self.real.powi(n);
377 let d = n as f64 * self.real.powi(n - 1);
378 Self {
379 real: r,
380 dual: self.map(|a| a * d),
381 }
382 }
383
384 #[inline]
385 fn copysign(self, sign: Self) -> Self {
386 let flipped = self.real.signum() != sign.real.signum();
387 Self {
388 real: self.real.copysign(sign.real),
389 dual: self.map(|a| if flipped { -a } else { a }),
390 }
391 }
392
393 #[inline]
394 fn signum(self) -> Self {
395 Self::constant(self.real.signum())
396 }
397
398 #[inline]
399 fn floor(self) -> Self {
400 Self::constant(self.real.floor())
401 }
402
403 #[inline]
404 fn ceil(self) -> Self {
405 Self::constant(self.real.ceil())
406 }
407
408 #[inline]
409 fn round(self) -> Self {
410 Self::constant(self.real.round())
411 }
412
413 #[inline]
414 fn exp(self) -> Self {
415 let e = self.real.exp();
416 Self {
417 real: e,
418 dual: self.map(|a| a * e),
419 }
420 }
421
422 #[inline]
423 fn ln(self) -> Self {
424 Self {
425 real: self.real.ln(),
426 dual: self.map(|a| a / self.real),
427 }
428 }
429
430 #[inline]
431 fn powf(self, p: Self) -> Self {
432 let val = self.real.powf(p.real);
433 let ln_x = self.real.ln();
434 Self {
435 real: val,
436 dual: self.zip(&p, |a, b| val * (b * ln_x + p.real * a / self.real)),
437 }
438 }
439
440 #[inline]
441 fn tanh(self) -> Self {
442 let t = self.real.tanh();
443 Self {
444 real: t,
445 dual: self.map(|a| a * (1.0 - t * t)),
446 }
447 }
448
449 #[inline]
450 fn sinh(self) -> Self {
451 let c = self.real.cosh();
452 Self {
453 real: self.real.sinh(),
454 dual: self.map(|a| a * c),
455 }
456 }
457
458 #[inline]
459 fn cosh(self) -> Self {
460 let s = self.real.sinh();
461 Self {
462 real: self.real.cosh(),
463 dual: self.map(|a| a * s),
464 }
465 }
466
467 #[inline]
468 fn acosh(self) -> Self {
469 let d = (self.real * self.real - 1.0).sqrt();
470 Self {
471 real: self.real.acosh(),
472 dual: self.map(|a| a / d),
473 }
474 }
475
476 #[inline]
477 fn asinh(self) -> Self {
478 let d = (self.real * self.real + 1.0).sqrt();
479 Self {
480 real: self.real.asinh(),
481 dual: self.map(|a| a / d),
482 }
483 }
484
485 #[inline]
486 fn atanh(self) -> Self {
487 let d = 1.0 - self.real * self.real;
488 Self {
489 real: self.real.atanh(),
490 dual: self.map(|a| a / d),
491 }
492 }
493
494 #[inline]
495 fn from_f64(v: f64) -> Self {
496 Self::constant(v)
497 }
498
499 #[inline]
500 fn to_f64(self) -> f64 {
501 self.real
502 }
503
504 #[inline]
505 fn from_i32(v: i32) -> Self {
506 Self::constant(v as f64)
507 }
508
509 #[inline]
510 fn select(cond: Self, a: Self, b: Self) -> Self {
511 if cond.real > 0.0 { a } else { b }
512 }
513}
514
515pub const SUPPORTED_WIDTHS: [usize; 5] = [1, 2, 4, 8, 16];
521
522pub fn width_for(lanes: usize) -> usize {
529 *SUPPORTED_WIDTHS
530 .iter()
531 .find(|&&w| w >= lanes)
532 .unwrap_or(SUPPORTED_WIDTHS.last().expect("non-empty"))
533}
534
535pub fn for_lanes<Op: LaneOp>(lanes: usize, op: Op) -> Op::Out {
568 match width_for(lanes) {
569 1 => op.call::<1>(),
570 2 => op.call::<2>(),
571 4 => op.call::<4>(),
572 8 => op.call::<8>(),
573 _ => op.call::<16>(),
574 }
575}
576
577pub trait LaneOp {
582 type Out;
584 fn call<const N: usize>(self) -> Self::Out;
586}
587
588#[cfg(test)]
589mod tests {
590 use super::*;
591 use tang::Dual;
592
593 fn workload<S: Scalar>(x: S, y: S) -> S {
597 let a = x * y + x / y - y;
598 let b = a.sin() * a.cos() + a.tan();
599 let c = (b * b + S::ONE).sqrt().ln();
600 let d = c.exp() * x.powi(3) + y.powf(S::from_f64(1.7));
601 let e = d.recip() + d.abs() - d.tanh() + d.sinh() * d.cosh();
602 let f = e.atan2(x) + x.max(y) - x.min(y) + e.signum() * e;
603 f + x.asin() + y.acos() + (x * S::TWO).asinh() + (y + S::TWO).acosh() + x.atanh()
604 }
605
606 #[test]
613 fn lanes_match_scalar_dual_bitwise() {
614 let (x0, y0) = (0.37, 0.61);
615
616 let dx = workload(Dual::var(x0), Dual::constant(y0));
617 let dy = workload(Dual::constant(x0), Dual::var(y0));
618
619 let m = workload(MultiDual::<2>::var(x0, 0), MultiDual::<2>::var(y0, 1));
620
621 assert_eq!(m.real.to_bits(), dx.real.to_bits(), "value");
622 assert_eq!(m.dual[0].to_bits(), dx.dual.to_bits(), "lane 0 vs ∂/∂x");
623 assert_eq!(m.dual[1].to_bits(), dy.dual.to_bits(), "lane 1 vs ∂/∂y");
624 }
625
626 #[test]
630 fn width_does_not_change_the_answer() {
631 let (x0, y0) = (0.37, 0.61);
632 let w1 = workload(MultiDual::<1>::var(x0, 0), MultiDual::<1>::constant(y0));
633 let w4 = workload(MultiDual::<4>::var(x0, 0), MultiDual::<4>::constant(y0));
634 let w16 = workload(MultiDual::<16>::var(x0, 0), MultiDual::<16>::constant(y0));
635
636 assert_eq!(w1.dual[0].to_bits(), w4.dual[0].to_bits());
637 assert_eq!(w1.dual[0].to_bits(), w16.dual[0].to_bits());
638 }
639
640 #[test]
643 fn unseeded_lanes_stay_zero() {
644 let m = workload(MultiDual::<8>::var(0.37, 0), MultiDual::<8>::constant(0.61));
645 for (k, d) in m.dual.iter().enumerate().skip(1) {
646 assert_eq!(*d, 0.0, "lane {k} picked up a nonzero derivative");
647 }
648 }
649
650 #[test]
651 fn width_dispatch_picks_the_narrowest_cover() {
652 assert_eq!(width_for(0), 1);
653 assert_eq!(width_for(1), 1);
654 assert_eq!(width_for(2), 2);
655 assert_eq!(width_for(3), 4);
656 assert_eq!(width_for(8), 8);
657 assert_eq!(width_for(9), 16);
658 assert_eq!(width_for(17), 16);
660 assert_eq!(width_for(1000), 16);
661 }
662
663 #[test]
664 fn for_lanes_reaches_the_expected_width() {
665 struct Width;
666 impl LaneOp for Width {
667 type Out = usize;
668 fn call<const N: usize>(self) -> usize {
669 N
670 }
671 }
672 assert_eq!(for_lanes(1, Width), 1);
673 assert_eq!(for_lanes(5, Width), 8);
674 assert_eq!(for_lanes(40, Width), 16);
675 }
676
677 #[test]
681 fn derivatives_are_correct_not_merely_consistent() {
682 let (x, y) = (1.3_f64, 0.7_f64);
684 let f = MultiDual::<2>::var(x, 0).powi(3) * MultiDual::<2>::var(y, 1).sin();
685
686 assert!((f.real - x.powi(3) * y.sin()).abs() < 1e-14);
687 assert!((f.dual[0] - 3.0 * x * x * y.sin()).abs() < 1e-14);
688 assert!((f.dual[1] - x.powi(3) * y.cos()).abs() < 1e-14);
689 }
690}