1pub trait SymmetricQuadraticCoefficients {
72 fn dimension(&self) -> usize;
74
75 fn multiply(&self, input: &[f64], output: &mut [f64]);
77
78 fn coefficient(&self, row: usize, column: usize) -> f64;
80
81 fn visit_upper_triangle(
92 &self,
93 direction: &mut [f64],
94 projected: &mut [f64],
95 mut visit: impl FnMut(usize, usize, f64),
96 ) {
97 let dimension = self.dimension();
98 assert_eq!(direction.len(), dimension);
99 assert_eq!(projected.len(), dimension);
100 direction.fill(0.0);
101 for column in 0..dimension {
102 direction[column] = 1.0;
103 self.multiply(direction, projected);
104 direction[column] = 0.0;
105 for row in 0..=column {
106 visit(row, column, projected[row]);
107 }
108 }
109 }
110
111 fn quadratic_value<T, F>(&self, inputs: &[T], value: F) -> f64
116 where
117 F: Fn(&T) -> f64,
118 {
119 assert_eq!(
120 inputs.len(),
121 self.dimension(),
122 "symmetric quadratic-form dimension mismatch"
123 );
124 let mut out = 0.0;
125 for row in 0..inputs.len() {
126 let row_value = value(&inputs[row]);
127 out += self.coefficient(row, row) * row_value * row_value;
128 for column in row + 1..inputs.len() {
129 out += 2.0 * self.coefficient(row, column) * row_value * value(&inputs[column]);
130 }
131 }
132 out
133 }
134}
135
136fn symmetric_quadratic_form_default<T, C>(
137 inputs: &[T],
138 coefficients: &C,
139 constant: impl Fn(f64) -> T,
140 add: impl Fn(&T, &T) -> T,
141 mul: impl Fn(&T, &T) -> T,
142 scale: impl Fn(&T, f64) -> T,
143) -> T
144where
145 C: SymmetricQuadraticCoefficients,
146{
147 assert_eq!(
148 inputs.len(),
149 coefficients.dimension(),
150 "symmetric quadratic-form dimension mismatch"
151 );
152 let mut out = constant(0.0);
153 for row in 0..inputs.len() {
154 let diagonal = mul(&inputs[row], &inputs[row]);
155 out = add(&out, &scale(&diagonal, coefficients.coefficient(row, row)));
156 for column in row + 1..inputs.len() {
157 let cross = mul(&inputs[row], &inputs[column]);
158 out = add(
159 &out,
160 &scale(&cross, 2.0 * coefficients.coefficient(row, column)),
161 );
162 }
163 }
164 out
165}
166
167fn linear_combination_default<T>(
168 inputs: &[T],
169 weights: &[f64],
170 constant: impl Fn(f64) -> T,
171 add: impl Fn(&T, &T) -> T,
172 scale: impl Fn(&T, f64) -> T,
173) -> T {
174 assert_eq!(
175 inputs.len(),
176 weights.len(),
177 "linear-combination dimension mismatch"
178 );
179 inputs
180 .iter()
181 .zip(weights)
182 .fold(constant(0.0), |sum, (input, &weight)| {
183 add(&sum, &scale(input, weight))
184 })
185}
186
187fn multiply_add_default<T>(
188 left: &T,
189 right: &T,
190 addend: &T,
191 mul: impl Fn(&T, &T) -> T,
192 add: impl Fn(&T, &T) -> T,
193) -> T {
194 add(&mul(left, right), addend)
195}
196
197fn composed_sum_default<T>(
198 inputs: &[T],
199 derivative_stacks: &[[f64; 5]],
200 constant: impl Fn(f64) -> T,
201 add: impl Fn(&T, &T) -> T,
202 compose: impl Fn(&T, [f64; 5]) -> T,
203) -> T {
204 assert_eq!(
205 inputs.len(),
206 derivative_stacks.len(),
207 "composed-sum term-count mismatch"
208 );
209 inputs
210 .iter()
211 .zip(derivative_stacks)
212 .fold(constant(0.0), |sum, (input, &stack)| {
213 add(&sum, &compose(input, stack))
214 })
215}
216
217fn affine_compose_default<T>(
218 input: &T,
219 input_scale: f64,
220 input_shift: f64,
221 derivative_stack: [f64; 5],
222 scale: impl Fn(&T, f64) -> T,
223 add_constant: impl Fn(&T, f64) -> T,
224 compose: impl Fn(&T, [f64; 5]) -> T,
225) -> T {
226 compose(
227 &add_constant(&scale(input, input_scale), input_shift),
228 derivative_stack,
229 )
230}
231
232fn affine_composed_sum_default<T>(
233 inputs: &[T],
234 input_scales: &[f64],
235 derivative_stacks: &[[f64; 5]],
236 constant: impl Fn(f64) -> T,
237 add: impl Fn(&T, &T) -> T,
238 scale: impl Fn(&T, f64) -> T,
239 add_constant: impl Fn(&T, f64) -> T,
240 compose: impl Fn(&T, [f64; 5]) -> T,
241) -> T {
242 assert_eq!(inputs.len(), input_scales.len());
243 assert_eq!(inputs.len(), derivative_stacks.len());
244 inputs.iter().zip(input_scales).zip(derivative_stacks).fold(
245 constant(0.0),
246 |sum, ((input, &input_scale), &stack)| {
247 add(
248 &sum,
249 &affine_compose_default(
250 input,
251 input_scale,
252 0.0,
253 stack,
254 &scale,
255 &add_constant,
256 &compose,
257 ),
258 )
259 },
260 )
261}
262
263fn shared_multiply_add_affine_composed_sum_default<T, const N: usize>(
264 lefts: &[&T; N],
265 right: &T,
266 addend: &T,
267 addend_scales: &[f64; N],
268 input_scales: &[f64; N],
269 derivative_stacks: &[[f64; 5]; N],
270 constant: impl Fn(f64) -> T,
271 add: impl Fn(&T, &T) -> T,
272 mul: impl Fn(&T, &T) -> T,
273 scale: impl Fn(&T, f64) -> T,
274 multiply_add: impl Fn(&T, &T, &T) -> T,
275 affine_compose: impl Fn(&T, f64, f64, [f64; 5]) -> T,
276) -> T {
277 let (representatives, term_sources, source_count) =
278 canonical_shared_source_schedule(|term, representative| {
279 std::ptr::eq(lefts[term], lefts[representative])
280 && addend_scales[term] == addend_scales[representative]
281 });
282 let (value, source_derivatives) =
283 aggregate_shared_source_derivatives(&term_sources, input_scales, derivative_stacks);
284 (0..source_count).fold(constant(value), |sum, source| {
285 let term = representatives[source];
286 let inner = if addend_scales[term] == 0.0 {
287 mul(lefts[term], right)
288 } else if addend_scales[term] == 1.0 {
289 multiply_add(lefts[term], right, addend)
290 } else {
291 multiply_add(lefts[term], right, &scale(addend, addend_scales[term]))
292 };
293 let composed = affine_compose(&inner, 1.0, 0.0, source_derivatives[source]);
294 add(&sum, &composed)
295 })
296}
297
298#[inline(always)]
306pub(crate) fn canonical_shared_source_schedule<const N: usize>(
307 mut equivalent: impl FnMut(usize, usize) -> bool,
308) -> ([usize; N], [usize; N], usize) {
309 let mut representatives = [0; N];
310 let mut term_sources = [0; N];
311 let mut source_count = 0;
312 for term in 0..N {
313 let mut source = 0;
314 while source < source_count && !equivalent(term, representatives[source]) {
315 source += 1;
316 }
317 if source == source_count {
318 representatives[source] = term;
319 source_count += 1;
320 }
321 term_sources[term] = source;
322 }
323 (representatives, term_sources, source_count)
324}
325
326#[inline(always)]
333pub(crate) fn aggregate_shared_source_derivatives<const N: usize>(
334 term_sources: &[usize; N],
335 input_scales: &[f64; N],
336 derivative_stacks: &[[f64; 5]; N],
337) -> (f64, [[f64; 5]; N]) {
338 let mut value = 0.0;
339 let mut source_derivatives = [[0.0; 5]; N];
340 for term in 0..N {
341 value += derivative_stacks[term][0];
342 let source = term_sources[term];
343 let mut scale_power = input_scales[term];
344 for order in 1..5 {
345 source_derivatives[source][order] += derivative_stacks[term][order] * scale_power;
346 scale_power *= input_scales[term];
347 }
348 }
349 (value, source_derivatives)
350}
351
352pub trait JetScalar<const K: usize>: crate::nested_dual::JetField + Copy {
360 fn constant(c: f64) -> Self;
362
363 fn variable(x: f64, axis: usize) -> Self;
368
369 fn symmetric_quadratic_form<C: SymmetricQuadraticCoefficients>(
373 inputs: &[Self],
374 coefficients: &C,
375 ) -> Self {
376 symmetric_quadratic_form_default(
377 inputs,
378 coefficients,
379 Self::constant,
380 crate::nested_dual::JetField::add,
381 crate::nested_dual::JetField::mul,
382 crate::nested_dual::JetField::scale,
383 )
384 }
385
386 fn linear_combination(inputs: &[Self], weights: &[f64]) -> Self {
388 linear_combination_default(
389 inputs,
390 weights,
391 Self::constant,
392 crate::nested_dual::JetField::add,
393 crate::nested_dual::JetField::scale,
394 )
395 }
396
397 fn add_constant(&self, constant: f64) -> Self {
399 self.add(&Self::constant(constant))
400 }
401
402 fn multiply_add(&self, right: &Self, addend: &Self) -> Self {
404 multiply_add_default(
405 self,
406 right,
407 addend,
408 crate::nested_dual::JetField::mul,
409 crate::nested_dual::JetField::add,
410 )
411 }
412
413 fn composed_sum(inputs: &[Self], derivative_stacks: &[[f64; 5]]) -> Self {
415 composed_sum_default(
416 inputs,
417 derivative_stacks,
418 Self::constant,
419 crate::nested_dual::JetField::add,
420 crate::nested_dual::JetField::compose_unary,
421 )
422 }
423
424 fn product(&self, right: &Self) -> Self {
426 self.mul(right)
427 }
428
429 fn affine_compose(
432 &self,
433 input_scale: f64,
434 input_shift: f64,
435 derivative_stack: [f64; 5],
436 ) -> Self {
437 affine_compose_default(
438 self,
439 input_scale,
440 input_shift,
441 derivative_stack,
442 crate::nested_dual::JetField::scale,
443 Self::add_constant,
444 crate::nested_dual::JetField::compose_unary,
445 )
446 }
447
448 fn affine_composed_sum(
450 inputs: &[Self],
451 input_scales: &[f64],
452 derivative_stacks: &[[f64; 5]],
453 ) -> Self {
454 affine_composed_sum_default(
455 inputs,
456 input_scales,
457 derivative_stacks,
458 Self::constant,
459 crate::nested_dual::JetField::add,
460 crate::nested_dual::JetField::scale,
461 Self::add_constant,
462 crate::nested_dual::JetField::compose_unary,
463 )
464 }
465
466 fn shared_multiply_add_affine_composed_sum<const N: usize>(
475 lefts: &[&Self; N],
476 right: &Self,
477 addend: &Self,
478 addend_scales: &[f64; N],
479 input_scales: &[f64; N],
480 derivative_stacks: &[[f64; 5]; N],
481 ) -> Self {
482 shared_multiply_add_affine_composed_sum_default(
483 lefts,
484 right,
485 addend,
486 addend_scales,
487 input_scales,
488 derivative_stacks,
489 Self::constant,
490 crate::nested_dual::JetField::add,
491 crate::nested_dual::JetField::mul,
492 crate::nested_dual::JetField::scale,
493 Self::multiply_add,
494 Self::affine_compose,
495 )
496 }
497
498 fn compose_unary_with(&self, stack_fn: impl Fn(f64) -> [f64; 5]) -> Self {
511 self.compose_unary(stack_fn(self.value()))
512 }
513
514 fn exp(&self) -> Self {
516 let e = self.value().exp();
517 self.compose_unary([e, e, e, e, e])
518 }
519
520 fn sqrt(&self) -> Self {
522 let u = self.value();
523 let s = u.sqrt();
524 self.compose_unary([
525 s,
526 0.5 / s,
527 -0.25 / (u * s),
528 0.375 / (u * u * s),
529 -0.9375 / (u * u * u * s),
530 ])
531 }
532
533 fn ln(&self) -> Self {
537 let u = self.value();
538 let r = 1.0 / u;
539 self.compose_unary([u.ln(), r, -r * r, 2.0 * r * r * r, -6.0 * r * r * r * r])
540 }
541
542 fn recip(&self) -> Self {
544 let r = 1.0 / self.value();
545 let r2 = r * r;
546 self.compose_unary([r, -r2, 2.0 * r2 * r, -6.0 * r2 * r2, 24.0 * r2 * r2 * r])
547 }
548
549 fn powf(&self, a: f64) -> Self {
552 let u = self.value();
553 self.compose_unary([
554 u.powf(a),
555 a * u.powf(a - 1.0),
556 a * (a - 1.0) * u.powf(a - 2.0),
557 a * (a - 1.0) * (a - 2.0) * u.powf(a - 3.0),
558 a * (a - 1.0) * (a - 2.0) * (a - 3.0) * u.powf(a - 4.0),
559 ])
560 }
561
562 fn ln_gamma(&self) -> Self {
567 self.compose_unary(crate::jet_tower::ln_gamma_derivative_stack(self.value()))
568 }
569
570 fn digamma(&self) -> Self {
574 self.compose_unary(crate::jet_tower::digamma_derivative_stack(self.value()))
575 }
576}
577
578impl<S, const K: usize> JetScalar<K> for crate::nested_dual::Dual2<S>
583where
584 S: JetScalar<K>,
585{
586 #[inline]
587 fn constant(c: f64) -> Self {
588 Self {
589 v: S::constant(c),
590 g: S::constant(0.0),
591 h: S::constant(0.0),
592 }
593 }
594
595 #[inline]
596 fn variable(x: f64, axis: usize) -> Self {
597 Self {
598 v: S::variable(x, axis),
599 g: S::constant(0.0),
600 h: S::constant(0.0),
601 }
602 }
603}
604
605pub trait RuntimeJetScalar<'arena>: Clone {
613 type Workspace: ?Sized;
616
617 fn constant(c: f64, dimension: usize, workspace: &'arena Self::Workspace) -> Self;
619 fn variable(x: f64, axis: usize, dimension: usize, workspace: &'arena Self::Workspace) -> Self;
621 fn constant_like(&self, c: f64) -> Self;
628 fn with_value(&self, value: f64) -> Self;
630
631 fn symmetric_quadratic_form<C: SymmetricQuadraticCoefficients>(
634 inputs: &[Self],
635 coefficients: &C,
636 dimension: usize,
637 workspace: &'arena Self::Workspace,
638 ) -> Self {
639 symmetric_quadratic_form_default(
640 inputs,
641 coefficients,
642 |value| Self::constant(value, dimension, workspace),
643 Self::add,
644 Self::mul,
645 Self::scale,
646 )
647 }
648
649 fn linear_combination(
651 inputs: &[Self],
652 weights: &[f64],
653 dimension: usize,
654 workspace: &'arena Self::Workspace,
655 ) -> Self {
656 linear_combination_default(
657 inputs,
658 weights,
659 |value| Self::constant(value, dimension, workspace),
660 Self::add,
661 Self::scale,
662 )
663 }
664
665 fn add_constant(&self, constant: f64) -> Self {
667 self.with_value(self.value() + constant)
668 }
669
670 fn multiply_add(&self, right: &Self, addend: &Self) -> Self {
672 multiply_add_default(self, right, addend, Self::mul, Self::add)
673 }
674
675 fn weighted_compose_sum(
691 lefts: &[Self],
692 right: &Self,
693 derivative_stacks: &[[f64; 5]],
694 addend: &Self,
695 ) -> Self {
696 assert_eq!(
697 lefts.len(),
698 derivative_stacks.len(),
699 "weighted compose sum needs one derivative stack per left factor"
700 );
701 let mut sum = addend.clone();
702 for (left, stack) in lefts.iter().zip(derivative_stacks) {
703 let composed = right.compose_unary(*stack);
704 let accumulated = left.multiply_add(&composed, &sum);
705 sum = accumulated;
706 }
707 sum
708 }
709
710 fn composed_sum(
712 inputs: &[Self],
713 derivative_stacks: &[[f64; 5]],
714 dimension: usize,
715 workspace: &'arena Self::Workspace,
716 ) -> Self {
717 composed_sum_default(
718 inputs,
719 derivative_stacks,
720 |value| Self::constant(value, dimension, workspace),
721 Self::add,
722 Self::compose_unary,
723 )
724 }
725
726 fn product(&self, right: &Self) -> Self {
728 self.mul(right)
729 }
730
731 fn affine_compose(
733 &self,
734 input_scale: f64,
735 input_shift: f64,
736 derivative_stack: [f64; 5],
737 ) -> Self {
738 affine_compose_default(
739 self,
740 input_scale,
741 input_shift,
742 derivative_stack,
743 Self::scale,
744 |value, constant| value.add_constant(constant),
745 Self::compose_unary,
746 )
747 }
748
749 fn affine_composed_sum(
751 inputs: &[Self],
752 input_scales: &[f64],
753 derivative_stacks: &[[f64; 5]],
754 dimension: usize,
755 workspace: &'arena Self::Workspace,
756 ) -> Self {
757 affine_composed_sum_default(
758 inputs,
759 input_scales,
760 derivative_stacks,
761 |value| Self::constant(value, dimension, workspace),
762 Self::add,
763 Self::scale,
764 |value, constant| value.add_constant(constant),
765 Self::compose_unary,
766 )
767 }
768
769 fn shared_multiply_add_affine_composed_sum<const N: usize>(
777 lefts: &[&Self; N],
778 right: &Self,
779 addend: &Self,
780 addend_scales: &[f64; N],
781 input_scales: &[f64; N],
782 derivative_stacks: &[[f64; 5]; N],
783 dimension: usize,
784 workspace: &'arena Self::Workspace,
785 ) -> Self {
786 shared_multiply_add_affine_composed_sum_default(
787 lefts,
788 right,
789 addend,
790 addend_scales,
791 input_scales,
792 derivative_stacks,
793 |value| Self::constant(value, dimension, workspace),
794 Self::add,
795 Self::mul,
796 Self::scale,
797 Self::multiply_add,
798 |input, scale, shift, stack| input.affine_compose(scale, shift, stack),
799 )
800 }
801 fn dimension(&self) -> usize;
803 fn value(&self) -> f64;
805 fn add(&self, o: &Self) -> Self;
807 fn sub(&self, o: &Self) -> Self;
809 fn mul(&self, o: &Self) -> Self;
811 fn neg(&self) -> Self;
813 fn scale(&self, s: f64) -> Self;
815 fn compose_unary(&self, d: [f64; 5]) -> Self;
817
818 fn exp(&self) -> Self {
820 let e = self.value().exp();
821 self.compose_unary([e, e, e, e, e])
822 }
823
824 fn ln(&self) -> Self {
828 let u = self.value();
829 let r = 1.0 / u;
830 self.compose_unary([u.ln(), r, -r * r, 2.0 * r * r * r, -6.0 * r * r * r * r])
831 }
832
833 fn recip(&self) -> Self {
835 let r = 1.0 / self.value();
836 let r2 = r * r;
837 self.compose_unary([r, -r2, 2.0 * r2 * r, -6.0 * r2 * r2, 24.0 * r2 * r2 * r])
838 }
839}
840
841#[derive(Clone, Copy, Debug, PartialEq)]
849pub struct RuntimeValue {
850 value: f64,
851 dimension: usize,
852}
853
854impl<'arena> RuntimeJetScalar<'arena> for RuntimeValue {
855 type Workspace = ();
856
857 #[inline(always)]
858 fn constant(c: f64, dimension: usize, &(): &'arena Self::Workspace) -> Self {
859 Self {
860 value: c,
861 dimension,
862 }
863 }
864
865 #[inline(always)]
866 fn variable(x: f64, axis: usize, dimension: usize, &(): &'arena Self::Workspace) -> Self {
867 assert!(
868 axis < dimension,
869 "runtime value variable axis out of bounds"
870 );
871 Self {
872 value: x,
873 dimension,
874 }
875 }
876
877 #[inline(always)]
878 fn constant_like(&self, c: f64) -> Self {
879 Self {
880 value: c,
881 dimension: self.dimension,
882 }
883 }
884
885 #[inline(always)]
886 fn with_value(&self, value: f64) -> Self {
887 Self {
888 value,
889 dimension: self.dimension,
890 }
891 }
892
893 #[inline(always)]
894 fn symmetric_quadratic_form<C: SymmetricQuadraticCoefficients>(
895 inputs: &[Self],
896 coefficients: &C,
897 dimension: usize,
898 &(): &'arena Self::Workspace,
899 ) -> Self {
900 assert_eq!(inputs.len(), coefficients.dimension());
901 assert!(inputs.iter().all(|input| input.dimension == dimension));
902 Self {
903 value: coefficients.quadratic_value(inputs, |input| input.value),
904 dimension,
905 }
906 }
907
908 #[inline(always)]
909 fn linear_combination(
910 inputs: &[Self],
911 weights: &[f64],
912 dimension: usize,
913 &(): &'arena Self::Workspace,
914 ) -> Self {
915 assert_eq!(inputs.len(), weights.len());
916 assert!(inputs.iter().all(|input| input.dimension == dimension));
917 let value = inputs
918 .iter()
919 .zip(weights)
920 .map(|(input, &weight)| input.value * weight)
921 .sum();
922 Self { value, dimension }
923 }
924
925 #[inline(always)]
926 fn multiply_add(&self, right: &Self, addend: &Self) -> Self {
927 self.assert_same_dimension(right);
928 self.assert_same_dimension(addend);
929 Self {
930 value: self.value * right.value + addend.value,
931 dimension: self.dimension,
932 }
933 }
934
935 #[inline(always)]
936 fn composed_sum(
937 inputs: &[Self],
938 derivative_stacks: &[[f64; 5]],
939 dimension: usize,
940 &(): &'arena Self::Workspace,
941 ) -> Self {
942 assert_eq!(inputs.len(), derivative_stacks.len());
943 assert!(inputs.iter().all(|input| input.dimension == dimension));
944 Self {
945 value: derivative_stacks.iter().map(|stack| stack[0]).sum(),
946 dimension,
947 }
948 }
949
950 #[inline(always)]
951 fn product(&self, right: &Self) -> Self {
952 self.mul(right)
953 }
954
955 #[inline(always)]
956 fn affine_compose(
957 &self,
958 input_scale: f64,
959 input_shift: f64,
960 derivative_stack: [f64; 5],
961 ) -> Self {
962 affine_compose_default(
968 self,
969 input_scale,
970 input_shift,
971 derivative_stack,
972 Self::scale,
973 |value, constant| value.add_constant(constant),
974 Self::compose_unary,
975 )
976 }
977
978 #[inline(always)]
979 fn affine_composed_sum(
980 inputs: &[Self],
981 input_scales: &[f64],
982 derivative_stacks: &[[f64; 5]],
983 dimension: usize,
984 &(): &'arena Self::Workspace,
985 ) -> Self {
986 assert_eq!(inputs.len(), input_scales.len());
987 assert_eq!(inputs.len(), derivative_stacks.len());
988 assert!(inputs.iter().all(|input| input.dimension == dimension));
989 Self {
990 value: derivative_stacks.iter().map(|stack| stack[0]).sum(),
991 dimension,
992 }
993 }
994
995 #[inline(always)]
996 fn dimension(&self) -> usize {
997 self.dimension
998 }
999
1000 #[inline(always)]
1001 fn value(&self) -> f64 {
1002 self.value
1003 }
1004
1005 #[inline(always)]
1006 fn add(&self, other: &Self) -> Self {
1007 self.assert_same_dimension(other);
1008 Self {
1009 value: self.value + other.value,
1010 dimension: self.dimension,
1011 }
1012 }
1013
1014 #[inline(always)]
1015 fn sub(&self, other: &Self) -> Self {
1016 self.assert_same_dimension(other);
1017 Self {
1018 value: self.value - other.value,
1019 dimension: self.dimension,
1020 }
1021 }
1022
1023 #[inline(always)]
1024 fn mul(&self, other: &Self) -> Self {
1025 self.assert_same_dimension(other);
1026 Self {
1027 value: self.value * other.value,
1028 dimension: self.dimension,
1029 }
1030 }
1031
1032 #[inline(always)]
1033 fn neg(&self) -> Self {
1034 Self {
1035 value: -self.value,
1036 dimension: self.dimension,
1037 }
1038 }
1039
1040 #[inline(always)]
1041 fn scale(&self, scale: f64) -> Self {
1042 Self {
1043 value: self.value * scale,
1044 dimension: self.dimension,
1045 }
1046 }
1047
1048 #[inline(always)]
1049 fn compose_unary(&self, derivative_stack: [f64; 5]) -> Self {
1050 Self {
1051 value: derivative_stack[0],
1052 dimension: self.dimension,
1053 }
1054 }
1055}
1056
1057impl RuntimeValue {
1058 #[inline(always)]
1059 fn assert_same_dimension(&self, other: &Self) {
1060 assert_eq!(self.dimension, other.dimension);
1061 }
1062}
1063
1064#[derive(Clone, Copy, Debug)]
1069#[repr(transparent)]
1070pub struct FixedRuntimeJet<S, const K: usize> {
1071 inner: S,
1072}
1073
1074impl<S, const K: usize> FixedRuntimeJet<S, K> {
1075 #[inline(always)]
1078 #[must_use]
1079 pub fn from_inner(inner: S) -> Self {
1080 Self { inner }
1081 }
1082
1083 #[inline(always)]
1085 #[must_use]
1086 pub fn into_inner(self) -> S {
1087 self.inner
1088 }
1089}
1090
1091impl<'arena, S: JetScalar<K>, const K: usize> RuntimeJetScalar<'arena> for FixedRuntimeJet<S, K> {
1092 type Workspace = ();
1093
1094 #[inline(always)]
1095 fn constant(c: f64, dimension: usize, &(): &'arena Self::Workspace) -> Self {
1096 assert_eq!(dimension, K, "fixed jet dimension mismatch");
1097 Self {
1098 inner: S::constant(c),
1099 }
1100 }
1101
1102 #[inline(always)]
1103 fn variable(x: f64, axis: usize, dimension: usize, &(): &'arena Self::Workspace) -> Self {
1104 assert_eq!(dimension, K, "fixed jet dimension mismatch");
1105 Self {
1106 inner: S::variable(x, axis),
1107 }
1108 }
1109
1110 #[inline(always)]
1111 fn constant_like(&self, c: f64) -> Self {
1112 Self {
1113 inner: S::constant(c),
1114 }
1115 }
1116
1117 #[inline(always)]
1118 fn with_value(&self, value: f64) -> Self {
1119 Self {
1120 inner: self.inner.compose_unary([value, 1.0, 0.0, 0.0, 0.0]),
1121 }
1122 }
1123
1124 #[inline(always)]
1125 fn symmetric_quadratic_form<C: SymmetricQuadraticCoefficients>(
1126 inputs: &[Self],
1127 coefficients: &C,
1128 dimension: usize,
1129 &(): &'arena Self::Workspace,
1130 ) -> Self {
1131 assert_eq!(dimension, K, "fixed jet dimension mismatch");
1132 assert_eq!(inputs.len(), coefficients.dimension());
1133 let inner =
1139 unsafe { std::slice::from_raw_parts(inputs.as_ptr().cast::<S>(), inputs.len()) };
1140 Self {
1141 inner: S::symmetric_quadratic_form(inner, coefficients),
1142 }
1143 }
1144
1145 #[inline(always)]
1146 fn linear_combination(
1147 inputs: &[Self],
1148 weights: &[f64],
1149 dimension: usize,
1150 &(): &'arena Self::Workspace,
1151 ) -> Self {
1152 assert_eq!(dimension, K, "fixed jet dimension mismatch");
1153 assert_eq!(inputs.len(), weights.len());
1154 let inner =
1158 unsafe { std::slice::from_raw_parts(inputs.as_ptr().cast::<S>(), inputs.len()) };
1159 Self {
1160 inner: S::linear_combination(inner, weights),
1161 }
1162 }
1163
1164 #[inline(always)]
1165 fn add_constant(&self, constant: f64) -> Self {
1166 Self {
1167 inner: self.inner.add_constant(constant),
1168 }
1169 }
1170
1171 #[inline(always)]
1172 fn multiply_add(&self, right: &Self, addend: &Self) -> Self {
1173 Self {
1174 inner: self.inner.multiply_add(&right.inner, &addend.inner),
1175 }
1176 }
1177
1178 #[inline(always)]
1179 fn composed_sum(
1180 inputs: &[Self],
1181 derivative_stacks: &[[f64; 5]],
1182 dimension: usize,
1183 &(): &'arena Self::Workspace,
1184 ) -> Self {
1185 assert_eq!(dimension, K, "fixed jet dimension mismatch");
1186 let inner =
1190 unsafe { std::slice::from_raw_parts(inputs.as_ptr().cast::<S>(), inputs.len()) };
1191 Self {
1192 inner: S::composed_sum(inner, derivative_stacks),
1193 }
1194 }
1195
1196 #[inline(always)]
1197 fn product(&self, right: &Self) -> Self {
1198 Self {
1199 inner: self.inner.product(&right.inner),
1200 }
1201 }
1202
1203 #[inline(always)]
1204 fn affine_compose(
1205 &self,
1206 input_scale: f64,
1207 input_shift: f64,
1208 derivative_stack: [f64; 5],
1209 ) -> Self {
1210 Self {
1211 inner: self
1212 .inner
1213 .affine_compose(input_scale, input_shift, derivative_stack),
1214 }
1215 }
1216
1217 #[inline(always)]
1218 fn affine_composed_sum(
1219 inputs: &[Self],
1220 input_scales: &[f64],
1221 derivative_stacks: &[[f64; 5]],
1222 dimension: usize,
1223 &(): &'arena Self::Workspace,
1224 ) -> Self {
1225 assert_eq!(dimension, K, "fixed jet dimension mismatch");
1226 let inner =
1230 unsafe { std::slice::from_raw_parts(inputs.as_ptr().cast::<S>(), inputs.len()) };
1231 Self {
1232 inner: S::affine_composed_sum(inner, input_scales, derivative_stacks),
1233 }
1234 }
1235
1236 #[inline(always)]
1237 fn shared_multiply_add_affine_composed_sum<const N: usize>(
1238 lefts: &[&Self; N],
1239 right: &Self,
1240 addend: &Self,
1241 addend_scales: &[f64; N],
1242 input_scales: &[f64; N],
1243 derivative_stacks: &[[f64; 5]; N],
1244 dimension: usize,
1245 &(): &'arena Self::Workspace,
1246 ) -> Self {
1247 assert_eq!(dimension, K, "fixed jet dimension mismatch");
1248 let left_inner: [&S; N] = std::array::from_fn(|term| &lefts[term].inner);
1249 Self {
1250 inner: S::shared_multiply_add_affine_composed_sum(
1251 &left_inner,
1252 &right.inner,
1253 &addend.inner,
1254 addend_scales,
1255 input_scales,
1256 derivative_stacks,
1257 ),
1258 }
1259 }
1260
1261 #[inline(always)]
1262 fn dimension(&self) -> usize {
1263 K
1264 }
1265
1266 #[inline(always)]
1267 fn value(&self) -> f64 {
1268 self.inner.value()
1269 }
1270
1271 #[inline(always)]
1272 fn add(&self, o: &Self) -> Self {
1273 Self {
1274 inner: self.inner.add(&o.inner),
1275 }
1276 }
1277
1278 #[inline(always)]
1279 fn sub(&self, o: &Self) -> Self {
1280 Self {
1281 inner: self.inner.sub(&o.inner),
1282 }
1283 }
1284
1285 #[inline(always)]
1286 fn mul(&self, o: &Self) -> Self {
1287 Self {
1288 inner: self.inner.mul(&o.inner),
1289 }
1290 }
1291
1292 #[inline(always)]
1293 fn neg(&self) -> Self {
1294 Self {
1295 inner: self.inner.neg(),
1296 }
1297 }
1298
1299 #[inline(always)]
1300 fn scale(&self, s: f64) -> Self {
1301 Self {
1302 inner: self.inner.scale(s),
1303 }
1304 }
1305
1306 #[inline(always)]
1307 fn compose_unary(&self, d: [f64; 5]) -> Self {
1308 Self {
1309 inner: self.inner.compose_unary(d),
1310 }
1311 }
1312}
1313
1314#[derive(Debug)]
1318pub struct DynamicJetArena {
1319 bump: bumpalo::Bump,
1320}
1321
1322impl DynamicJetArena {
1323 #[must_use]
1325 pub fn new() -> Self {
1326 Self {
1327 bump: bumpalo::Bump::new(),
1328 }
1329 }
1330
1331 #[must_use]
1333 pub fn with_capacity(bytes: usize) -> Self {
1334 Self {
1335 bump: bumpalo::Bump::with_capacity(bytes),
1336 }
1337 }
1338
1339 pub fn reset(&mut self) {
1350 let high_water = self.bump.allocated_bytes();
1351 self.bump.reset();
1352 if self.bump.allocated_bytes() < high_water {
1353 self.bump = bumpalo::Bump::with_capacity(high_water);
1354 }
1355 }
1356
1357 #[must_use]
1360 pub fn allocated_bytes(&self) -> usize {
1361 self.bump.allocated_bytes()
1362 }
1363
1364 #[inline(always)]
1365 fn zeros(&self, len: usize) -> &mut [f64] {
1366 self.bump.alloc_slice_fill_copy(len, 0.0)
1367 }
1368
1369 #[inline(always)]
1373 pub fn alloc_slice_fill_with<T>(&self, len: usize, fill: impl FnMut(usize) -> T) -> &mut [T] {
1374 self.bump.alloc_slice_fill_with(len, fill)
1375 }
1376}
1377
1378impl Default for DynamicJetArena {
1379 fn default() -> Self {
1380 Self::new()
1381 }
1382}
1383
1384#[derive(Clone, Copy, Debug)]
1386pub struct DynamicOrder1<'arena> {
1387 arena: &'arena DynamicJetArena,
1388 pub v: f64,
1390 pub g: &'arena [f64],
1392}
1393
1394impl DynamicOrder1<'_> {
1395 #[inline]
1397 #[must_use]
1398 pub fn g(&self) -> &[f64] {
1399 self.g
1400 }
1401
1402 #[inline]
1403 fn assert_compatible(&self, o: &Self) {
1404 assert_eq!(
1405 self.g.len(),
1406 o.g.len(),
1407 "dynamic first-order jet dimension mismatch"
1408 );
1409 assert!(
1410 std::ptr::eq(self.arena, o.arena),
1411 "dynamic jets belong to different arenas"
1412 );
1413 }
1414}
1415
1416impl<'arena> RuntimeJetScalar<'arena> for DynamicOrder1<'arena> {
1417 type Workspace = DynamicJetArena;
1418
1419 fn constant(c: f64, dimension: usize, arena: &'arena DynamicJetArena) -> Self {
1420 Self {
1421 arena,
1422 v: c,
1423 g: arena.zeros(dimension),
1424 }
1425 }
1426
1427 fn variable(x: f64, axis: usize, dimension: usize, arena: &'arena DynamicJetArena) -> Self {
1428 assert!(
1429 axis < dimension,
1430 "dynamic first-order jet axis out of bounds"
1431 );
1432 let g = arena_vector(arena, dimension, |i| if i == axis { 1.0 } else { 0.0 });
1433 Self { arena, v: x, g }
1434 }
1435
1436 #[inline(always)]
1437 fn constant_like(&self, c: f64) -> Self {
1438 Self {
1439 arena: self.arena,
1440 v: c,
1441 g: self.arena.zeros(self.dimension()),
1442 }
1443 }
1444
1445 #[inline(always)]
1446 fn with_value(&self, value: f64) -> Self {
1447 Self {
1448 arena: self.arena,
1449 v: value,
1450 g: self.g,
1451 }
1452 }
1453
1454 fn dimension(&self) -> usize {
1455 self.g.len()
1456 }
1457 fn value(&self) -> f64 {
1458 self.v
1459 }
1460
1461 fn add(&self, o: &Self) -> Self {
1462 self.assert_compatible(o);
1463 let g = arena_vector(self.arena, self.dimension(), |i| self.g[i] + o.g[i]);
1464 Self {
1465 arena: self.arena,
1466 v: self.v + o.v,
1467 g,
1468 }
1469 }
1470
1471 fn sub(&self, o: &Self) -> Self {
1472 self.assert_compatible(o);
1473 let g = arena_vector(self.arena, self.dimension(), |i| self.g[i] - o.g[i]);
1474 Self {
1475 arena: self.arena,
1476 v: self.v - o.v,
1477 g,
1478 }
1479 }
1480
1481 fn mul(&self, o: &Self) -> Self {
1482 self.assert_compatible(o);
1483 let g = arena_vector(self.arena, self.dimension(), |i| self.v * o.g[i] + self.g[i] * o.v);
1484 Self {
1485 arena: self.arena,
1486 v: self.v * o.v,
1487 g,
1488 }
1489 }
1490
1491 fn neg(&self) -> Self {
1492 self.scale(-1.0)
1493 }
1494
1495 fn scale(&self, s: f64) -> Self {
1496 let g = arena_vector(self.arena, self.dimension(), |i| self.g[i] * s);
1497 Self {
1498 arena: self.arena,
1499 v: self.v * s,
1500 g,
1501 }
1502 }
1503
1504 fn compose_unary(&self, d: [f64; 5]) -> Self {
1505 let g = arena_vector(self.arena, self.dimension(), |i| d[1] * self.g[i]);
1506 Self {
1507 arena: self.arena,
1508 v: d[0],
1509 g,
1510 }
1511 }
1512}
1513
1514#[derive(Clone, Copy, Debug)]
1518pub struct DynamicOrder2<'arena> {
1519 arena: &'arena DynamicJetArena,
1520 pub v: f64,
1522 pub g: &'arena [f64],
1524 pub h: &'arena [f64],
1526}
1527
1528impl DynamicOrder2<'_> {
1529 #[inline]
1539 #[must_use]
1540 pub fn from_channel_functions<'arena>(
1541 value: f64,
1542 dimension: usize,
1543 arena: &'arena DynamicJetArena,
1544 mut gradient: impl FnMut(usize) -> f64,
1545 mut hessian: impl FnMut(usize, usize) -> f64,
1546 ) -> DynamicOrder2<'arena> {
1547 let g = arena.alloc_slice_fill_with(dimension, |axis| gradient(axis));
1548 let h = arena.zeros(dimension * dimension);
1549 for row in 0..dimension {
1550 for column in row..dimension {
1551 let channel = hessian(row, column);
1552 h[row * dimension + column] = channel;
1553 h[column * dimension + row] = channel;
1554 }
1555 }
1556 DynamicOrder2 {
1557 arena,
1558 v: value,
1559 g,
1560 h,
1561 }
1562 }
1563
1564 #[inline]
1566 #[must_use]
1567 pub fn g(&self) -> &[f64] {
1568 self.g
1569 }
1570
1571 #[inline]
1573 #[must_use]
1574 pub fn h(&self) -> &[f64] {
1575 self.h
1576 }
1577
1578 #[inline(always)]
1579 fn assert_compatible(&self, o: &Self) {
1580 assert_eq!(
1581 self.g.len(),
1582 o.g.len(),
1583 "dynamic second-order jet dimension mismatch"
1584 );
1585 assert_eq!(
1586 self.h.len(),
1587 o.h.len(),
1588 "dynamic second-order jet Hessian mismatch"
1589 );
1590 assert!(
1591 std::ptr::eq(self.arena, o.arena),
1592 "dynamic jets belong to different arenas"
1593 );
1594 }
1595}
1596
1597#[inline(always)]
1606fn arena_vector<'arena>(
1607 arena: &'arena DynamicJetArena,
1608 n: usize,
1609 entry: impl FnMut(usize) -> f64,
1610) -> &'arena mut [f64] {
1611 arena.alloc_slice_fill_with(n, entry)
1612}
1613
1614#[inline(always)]
1619fn arena_square<'arena>(
1620 arena: &'arena DynamicJetArena,
1621 n: usize,
1622 mut entry: impl FnMut(usize, usize, usize) -> f64,
1623) -> &'arena mut [f64] {
1624 let mut row = 0usize;
1625 let mut column = 0usize;
1626 arena.alloc_slice_fill_with(n * n, |index| {
1627 let value = entry(row, column, index);
1628 column += 1;
1629 if column == n {
1630 column = 0;
1631 row += 1;
1632 }
1633 value
1634 })
1635}
1636
1637impl<'arena> DynamicOrder2<'arena> {
1638 #[inline(always)]
1645 #[must_use]
1646 pub fn scaled_product_sum(scales: &[f64], lefts: &[Self], rights: &[Self]) -> Self {
1647 assert!(
1648 !lefts.is_empty() && lefts.len() == rights.len() && lefts.len() == scales.len(),
1649 "dynamic product sum needs matching non-empty term lists"
1650 );
1651 let arena = lefts[0].arena;
1652 let n = lefts[0].dimension();
1653 for (left, right) in lefts.iter().zip(rights) {
1654 left.assert_compatible(right);
1655 assert!(
1656 left.dimension() == n && std::ptr::eq(left.arena, arena),
1657 "dynamic product sum jets must share dimension and arena"
1658 );
1659 }
1660 let mut v = 0.0;
1661 for ((left, right), &scale) in lefts.iter().zip(rights).zip(scales) {
1662 v += scale * left.v * right.v;
1663 }
1664 let g = arena_vector(arena, n, |i| {
1665 let mut total = 0.0;
1666 for ((left, right), &scale) in lefts.iter().zip(rights).zip(scales) {
1667 total += scale * (left.v * right.g[i] + left.g[i] * right.v);
1668 }
1669 total
1670 });
1671 let h = arena_square(arena, n, |i, j, ij| {
1672 let mut total = 0.0;
1673 for ((left, right), &scale) in lefts.iter().zip(rights).zip(scales) {
1674 total += scale
1675 * (left.v * right.h[ij]
1676 + left.g[i] * right.g[j]
1677 + left.g[j] * right.g[i]
1678 + left.h[ij] * right.v);
1679 }
1680 total
1681 });
1682 Self { arena, v, g, h }
1683 }
1684
1685 #[inline(always)]
1687 #[must_use]
1688 pub fn product_pair_sum_plus(a: &Self, b: &Self, c: &Self, d: &Self, e: &Self) -> Self {
1689 a.assert_compatible(b);
1690 a.assert_compatible(c);
1691 a.assert_compatible(d);
1692 a.assert_compatible(e);
1693 let arena = a.arena;
1694 let n = a.dimension();
1695 let g = arena_vector(arena, n, |i| {
1696 a.v * b.g[i] + a.g[i] * b.v + c.v * d.g[i] + c.g[i] * d.v + e.g[i]
1697 });
1698 let h = arena_square(arena, n, |i, j, ij| {
1699 a.v * b.h[ij]
1700 + a.g[i] * b.g[j]
1701 + a.g[j] * b.g[i]
1702 + a.h[ij] * b.v
1703 + c.v * d.h[ij]
1704 + c.g[i] * d.g[j]
1705 + c.g[j] * d.g[i]
1706 + c.h[ij] * d.v
1707 + e.h[ij]
1708 });
1709 Self {
1710 arena,
1711 v: a.v * b.v + c.v * d.v + e.v,
1712 g,
1713 h,
1714 }
1715 }
1716}
1717
1718impl<'arena> RuntimeJetScalar<'arena> for DynamicOrder2<'arena> {
1719 type Workspace = DynamicJetArena;
1720
1721 #[inline(always)]
1722 fn constant(c: f64, dimension: usize, arena: &'arena DynamicJetArena) -> Self {
1723 Self {
1724 arena,
1725 v: c,
1726 g: arena.zeros(dimension),
1727 h: arena.zeros(dimension * dimension),
1728 }
1729 }
1730
1731 #[inline(always)]
1732 fn variable(x: f64, axis: usize, dimension: usize, arena: &'arena DynamicJetArena) -> Self {
1733 assert!(
1734 axis < dimension,
1735 "dynamic second-order jet axis out of bounds"
1736 );
1737 let g = arena.zeros(dimension);
1738 g[axis] = 1.0;
1739 Self {
1740 arena,
1741 v: x,
1742 g,
1743 h: arena.zeros(dimension * dimension),
1744 }
1745 }
1746
1747 #[inline(always)]
1748 fn constant_like(&self, c: f64) -> Self {
1749 let dimension = self.dimension();
1750 Self {
1751 arena: self.arena,
1752 v: c,
1753 g: self.arena.zeros(dimension),
1754 h: self.arena.zeros(dimension * dimension),
1755 }
1756 }
1757
1758 #[inline(always)]
1759 fn with_value(&self, value: f64) -> Self {
1760 Self {
1761 arena: self.arena,
1762 v: value,
1763 g: self.g,
1764 h: self.h,
1765 }
1766 }
1767
1768 #[inline(always)]
1769 fn symmetric_quadratic_form<C: SymmetricQuadraticCoefficients>(
1770 inputs: &[Self],
1771 coefficients: &C,
1772 dimension: usize,
1773 arena: &'arena DynamicJetArena,
1774 ) -> Self {
1775 assert_eq!(inputs.len(), coefficients.dimension());
1776 assert!(
1777 inputs.iter().all(|input| {
1778 input.dimension() == dimension && std::ptr::eq(input.arena, arena)
1779 }),
1780 "dynamic quadratic-form jets must share dimension and arena"
1781 );
1782 let input_dimension = inputs.len();
1783 let values = arena.zeros(input_dimension);
1784 for (value, input) in values.iter_mut().zip(inputs) {
1785 *value = input.v;
1786 }
1787 let projected = arena.zeros(input_dimension);
1788 coefficients.multiply(values, projected);
1789
1790 let mut value = 0.0;
1791 for axis in 0..input_dimension {
1792 value += values[axis] * projected[axis];
1793 }
1794 let gradient = arena.zeros(dimension);
1795 for primary in 0..dimension {
1796 let mut channel = 0.0;
1797 for axis in 0..input_dimension {
1798 channel += projected[axis] * inputs[axis].g[primary];
1799 }
1800 gradient[primary] = 2.0 * channel;
1801 }
1802 let hessian = arena.zeros(dimension * dimension);
1803 let input_gradient = arena.zeros(input_dimension);
1804 let projected_gradient = arena.zeros(input_dimension);
1805 for primary_b in 0..dimension {
1806 for row in 0..input_dimension {
1807 input_gradient[row] = inputs[row].g[primary_b];
1808 }
1809 coefficients.multiply(input_gradient, projected_gradient);
1810 for primary_a in 0..=primary_b {
1811 let mut inherited = 0.0;
1812 let mut curvature = 0.0;
1813 for row in 0..input_dimension {
1814 inherited += projected[row] * inputs[row].h[primary_a * dimension + primary_b];
1815 curvature += inputs[row].g[primary_a] * projected_gradient[row];
1816 }
1817 let channel = 2.0 * (inherited + curvature);
1818 hessian[primary_a * dimension + primary_b] = channel;
1819 hessian[primary_b * dimension + primary_a] = channel;
1820 }
1821 }
1822 Self {
1823 arena,
1824 v: value,
1825 g: gradient,
1826 h: hessian,
1827 }
1828 }
1829
1830 #[inline(always)]
1831 fn product(&self, right: &Self) -> Self {
1832 self.mul(right)
1833 }
1834
1835 #[inline(always)]
1836 fn affine_compose(
1837 &self,
1838 input_scale: f64,
1839 input_shift: f64,
1840 derivative_stack: [f64; 5],
1841 ) -> Self {
1842 assert!(input_shift.is_finite(), "affine input shift must be finite");
1843 let arena = self.arena;
1844 let dimension = self.dimension();
1845 let first = derivative_stack[1] * input_scale;
1846 let second = derivative_stack[2] * input_scale * input_scale;
1847 let gradient = arena_vector(arena, dimension, |i| first * self.g[i]);
1848 let hessian = arena_square(arena, dimension, |i, j, ij| {
1849 first * self.h[ij] + second * self.g[i] * self.g[j]
1850 });
1851 Self {
1852 arena,
1853 v: derivative_stack[0],
1854 g: gradient,
1855 h: hessian,
1856 }
1857 }
1858
1859 #[inline(always)]
1860 fn affine_composed_sum(
1861 inputs: &[Self],
1862 input_scales: &[f64],
1863 derivative_stacks: &[[f64; 5]],
1864 dimension: usize,
1865 arena: &'arena DynamicJetArena,
1866 ) -> Self {
1867 assert_eq!(inputs.len(), input_scales.len());
1868 assert_eq!(inputs.len(), derivative_stacks.len());
1869 assert!(
1870 inputs.iter().all(|input| {
1871 input.dimension() == dimension && std::ptr::eq(input.arena, arena)
1872 }),
1873 "dynamic affine-composed-sum jets must share dimension and arena"
1874 );
1875 let mut value = 0.0;
1876 for stack in derivative_stacks {
1877 value += stack[0];
1878 }
1879 let gradient = arena_vector(arena, dimension, |i| {
1880 let mut total = 0.0;
1881 for ((input, &input_scale), stack) in
1882 inputs.iter().zip(input_scales).zip(derivative_stacks)
1883 {
1884 total += stack[1] * input_scale * input.g[i];
1885 }
1886 total
1887 });
1888 let hessian = arena_square(arena, dimension, |i, j, ij| {
1889 let mut total = 0.0;
1890 for ((input, &input_scale), stack) in
1891 inputs.iter().zip(input_scales).zip(derivative_stacks)
1892 {
1893 let first = stack[1] * input_scale;
1894 let second = stack[2] * input_scale * input_scale;
1895 total += first * input.h[ij] + second * input.g[i] * input.g[j];
1896 }
1897 total
1898 });
1899 Self {
1900 arena,
1901 v: value,
1902 g: gradient,
1903 h: hessian,
1904 }
1905 }
1906
1907 #[inline(always)]
1908 fn shared_multiply_add_affine_composed_sum<const N: usize>(
1909 lefts: &[&Self; N],
1910 right: &Self,
1911 addend: &Self,
1912 addend_scales: &[f64; N],
1913 input_scales: &[f64; N],
1914 derivative_stacks: &[[f64; 5]; N],
1915 dimension: usize,
1916 arena: &'arena DynamicJetArena,
1917 ) -> Self {
1918 assert!(
1919 lefts.iter().all(|input| {
1920 input.dimension() == dimension && std::ptr::eq(input.arena, arena)
1921 }) && (N == 0 || (right.dimension() == dimension && std::ptr::eq(right.arena, arena))),
1922 "dynamic fused product-composition jets must share dimension and arena"
1923 );
1924 let addend_live = addend_scales.iter().any(|&scale| scale != 0.0);
1925 assert!(
1926 !addend_live || (addend.dimension() == dimension && std::ptr::eq(addend.arena, arena)),
1927 "live dynamic fused addends must share dimension and arena"
1928 );
1929 let (representatives, term_sources, source_count) =
1930 canonical_shared_source_schedule::<N>(|term, representative| {
1931 std::ptr::eq(lefts[term], lefts[representative])
1932 && addend_scales[term] == addend_scales[representative]
1933 });
1934 let (value, source_derivatives) =
1935 aggregate_shared_source_derivatives(&term_sources, input_scales, derivative_stacks);
1936 let source_gradients = arena.zeros(source_count * dimension);
1937 let gradient = arena.zeros(dimension);
1938 let hessian = arena.zeros(dimension * dimension);
1939 let mut right_first = 0.0;
1940 let mut addend_first = 0.0;
1941 for source in 0..source_count {
1942 let term = representatives[source];
1943 let first = source_derivatives[source][1];
1944 right_first += first * lefts[term].v;
1945 addend_first += first * addend_scales[term];
1946 for primary in 0..dimension {
1947 let product_gradient =
1948 lefts[term].v * right.g[primary] + lefts[term].g[primary] * right.v;
1949 let inner_gradient = if addend_scales[term] == 0.0 {
1950 product_gradient
1951 } else if addend_scales[term] == 1.0 {
1952 product_gradient + addend.g[primary]
1953 } else {
1954 product_gradient + addend_scales[term] * addend.g[primary]
1955 };
1956 source_gradients[source * dimension + primary] = inner_gradient;
1957 gradient[primary] += first * lefts[term].g[primary] * right.v;
1958 }
1959 }
1960 if N != 0 {
1961 for primary in 0..dimension {
1962 gradient[primary] += right_first * right.g[primary];
1963 }
1964 }
1965 if addend_live {
1966 for primary in 0..dimension {
1967 gradient[primary] += addend_first * addend.g[primary];
1968 }
1969 }
1970 for primary in 0..dimension {
1971 for other in primary..dimension {
1972 let index = primary * dimension + other;
1973 let mut channel = if N == 0 {
1974 0.0
1975 } else {
1976 right_first * right.h[index]
1977 };
1978 if addend_live {
1979 channel += addend_first * addend.h[index];
1980 }
1981 for source in 0..source_count {
1982 let term = representatives[source];
1983 let local_product_hessian = lefts[term].g[primary] * right.g[other]
1984 + lefts[term].g[other] * right.g[primary]
1985 + lefts[term].h[index] * right.v;
1986 channel += source_derivatives[source][1] * local_product_hessian
1987 + source_derivatives[source][2]
1988 * source_gradients[source * dimension + primary]
1989 * source_gradients[source * dimension + other];
1990 }
1991 hessian[index] = channel;
1992 hessian[other * dimension + primary] = channel;
1993 }
1994 }
1995 Self {
1996 arena,
1997 v: value,
1998 g: gradient,
1999 h: hessian,
2000 }
2001 }
2002
2003 #[inline(always)]
2004 fn multiply_add(&self, right: &Self, addend: &Self) -> Self {
2005 self.assert_compatible(right);
2006 self.assert_compatible(addend);
2007 let dimension = self.dimension();
2008 let gradient = arena_vector(self.arena, dimension, |i| {
2009 self.v * right.g[i] + self.g[i] * right.v + addend.g[i]
2010 });
2011 let hessian = arena_square(self.arena, dimension, |i, j, ij| {
2012 self.v * right.h[ij]
2013 + self.g[i] * right.g[j]
2014 + self.g[j] * right.g[i]
2015 + self.h[ij] * right.v
2016 + addend.h[ij]
2017 });
2018 Self {
2019 arena: self.arena,
2020 v: self.v * right.v + addend.v,
2021 g: gradient,
2022 h: hessian,
2023 }
2024 }
2025
2026 #[inline(always)]
2027 fn composed_sum(
2028 inputs: &[Self],
2029 derivative_stacks: &[[f64; 5]],
2030 dimension: usize,
2031 arena: &'arena DynamicJetArena,
2032 ) -> Self {
2033 assert_eq!(inputs.len(), derivative_stacks.len());
2034 assert!(
2035 inputs.iter().all(|input| {
2036 input.dimension() == dimension && std::ptr::eq(input.arena, arena)
2037 }),
2038 "dynamic composed-sum jets must share dimension and arena"
2039 );
2040 let mut value = 0.0;
2041 for stack in derivative_stacks {
2042 value += stack[0];
2043 }
2044 let gradient = arena_vector(arena, dimension, |i| {
2045 let mut total = 0.0;
2046 for (input, stack) in inputs.iter().zip(derivative_stacks) {
2047 total += stack[1] * input.g[i];
2048 }
2049 total
2050 });
2051 let hessian = arena_square(arena, dimension, |i, j, ij| {
2052 let mut total = 0.0;
2053 for (input, stack) in inputs.iter().zip(derivative_stacks) {
2054 total += stack[1] * input.h[ij] + stack[2] * input.g[i] * input.g[j];
2055 }
2056 total
2057 });
2058 Self {
2059 arena,
2060 v: value,
2061 g: gradient,
2062 h: hessian,
2063 }
2064 }
2065
2066 #[inline(always)]
2067 fn linear_combination(
2068 inputs: &[Self],
2069 weights: &[f64],
2070 dimension: usize,
2071 arena: &'arena DynamicJetArena,
2072 ) -> Self {
2073 assert_eq!(inputs.len(), weights.len());
2074 assert!(
2075 inputs.iter().all(|input| {
2076 input.dimension() == dimension && std::ptr::eq(input.arena, arena)
2077 }),
2078 "dynamic linear-combination jets must share dimension and arena"
2079 );
2080 let mut value = 0.0;
2081 for (input, &weight) in inputs.iter().zip(weights) {
2082 value += input.v * weight;
2083 }
2084 let gradient = arena_vector(arena, dimension, |i| {
2085 let mut total = 0.0;
2086 for (input, &weight) in inputs.iter().zip(weights) {
2087 total += input.g[i] * weight;
2088 }
2089 total
2090 });
2091 let hessian = arena_square(arena, dimension, |_, _, ij| {
2092 let mut total = 0.0;
2093 for (input, &weight) in inputs.iter().zip(weights) {
2094 total += input.h[ij] * weight;
2095 }
2096 total
2097 });
2098 Self {
2099 arena,
2100 v: value,
2101 g: gradient,
2102 h: hessian,
2103 }
2104 }
2105
2106 #[inline(always)]
2107 fn dimension(&self) -> usize {
2108 self.g.len()
2109 }
2110
2111 #[inline(always)]
2112 fn value(&self) -> f64 {
2113 self.v
2114 }
2115
2116 #[inline(always)]
2117 fn add(&self, o: &Self) -> Self {
2118 self.assert_compatible(o);
2119 let dimension = self.dimension();
2120 let g = arena_vector(self.arena, dimension, |i| self.g[i] + o.g[i]);
2121 let h = arena_square(self.arena, dimension, |_, _, ij| self.h[ij] + o.h[ij]);
2122 Self {
2123 arena: self.arena,
2124 v: self.v + o.v,
2125 g,
2126 h,
2127 }
2128 }
2129
2130 #[inline(always)]
2131 fn sub(&self, o: &Self) -> Self {
2132 self.assert_compatible(o);
2133 let dimension = self.dimension();
2134 let g = arena_vector(self.arena, dimension, |i| self.g[i] - o.g[i]);
2135 let h = arena_square(self.arena, dimension, |_, _, ij| self.h[ij] - o.h[ij]);
2136 Self {
2137 arena: self.arena,
2138 v: self.v - o.v,
2139 g,
2140 h,
2141 }
2142 }
2143
2144 #[inline(always)]
2145 fn mul(&self, o: &Self) -> Self {
2146 self.assert_compatible(o);
2147 let n = self.dimension();
2148 let g = arena_vector(self.arena, n, |i| self.v * o.g[i] + self.g[i] * o.v);
2149 let h = arena_square(self.arena, n, |i, j, ij| {
2150 self.v * o.h[ij] + self.g[i] * o.g[j] + self.g[j] * o.g[i] + self.h[ij] * o.v
2151 });
2152 Self {
2153 arena: self.arena,
2154 v: self.v * o.v,
2155 g,
2156 h,
2157 }
2158 }
2159
2160 #[inline(always)]
2161 fn neg(&self) -> Self {
2162 self.scale(-1.0)
2163 }
2164
2165 #[inline(always)]
2166 fn scale(&self, s: f64) -> Self {
2167 let dimension = self.dimension();
2168 let g = arena_vector(self.arena, dimension, |i| self.g[i] * s);
2169 let h = arena_square(self.arena, dimension, |_, _, ij| self.h[ij] * s);
2170 Self {
2171 arena: self.arena,
2172 v: self.v * s,
2173 g,
2174 h,
2175 }
2176 }
2177
2178 #[inline(always)]
2179 fn compose_unary(&self, d: [f64; 5]) -> Self {
2180 let n = self.dimension();
2181 let g = arena_vector(self.arena, n, |i| d[1] * self.g[i]);
2182 let h = arena_square(self.arena, n, |i, j, ij| {
2183 d[1] * self.h[ij] + d[2] * self.g[i] * self.g[j]
2184 });
2185 Self {
2186 arena: self.arena,
2187 v: d[0],
2188 g,
2189 h,
2190 }
2191 }
2192
2193 #[inline]
2211 fn weighted_compose_sum(
2212 lefts: &[Self],
2213 right: &Self,
2214 derivative_stacks: &[[f64; 5]],
2215 addend: &Self,
2216 ) -> Self {
2217 assert_eq!(
2218 lefts.len(),
2219 derivative_stacks.len(),
2220 "weighted compose sum needs one derivative stack per left factor"
2221 );
2222 let arena = addend.arena;
2223 let n = addend.dimension();
2224 for left in lefts {
2225 addend.assert_compatible(left);
2226 }
2227 addend.assert_compatible(right);
2228 let mut value = addend.v;
2229 let mut right_first = 0.0;
2230 let mut right_second = 0.0;
2231 for (left, stack) in lefts.iter().zip(derivative_stacks) {
2232 value += left.v * stack[0];
2233 right_first += left.v * stack[1];
2234 right_second += left.v * stack[2];
2235 }
2236 let g = arena_vector(arena, n, |a| {
2237 let mut channel = addend.g[a] + right_first * right.g[a];
2238 for (left, stack) in lefts.iter().zip(derivative_stacks) {
2239 channel += stack[0] * left.g[a];
2240 }
2241 channel
2242 });
2243 let h = arena_square(arena, n, |a, b, ab| {
2244 let mut channel = addend.h[ab]
2245 + right_first * right.h[ab]
2246 + right_second * right.g[a] * right.g[b];
2247 for (left, stack) in lefts.iter().zip(derivative_stacks) {
2248 channel += stack[1] * (left.g[a] * right.g[b] + right.g[a] * left.g[b])
2249 + stack[0] * left.h[ab];
2250 }
2251 channel
2252 });
2253 Self {
2254 arena,
2255 v: value,
2256 g,
2257 h,
2258 }
2259 }
2260}
2261
2262#[derive(Clone, Copy, Debug)]
2264pub struct DynamicOneSeed<'arena> {
2265 pub base: DynamicOrder2<'arena>,
2267 pub eps: DynamicOrder2<'arena>,
2269}
2270
2271impl<'arena> DynamicOneSeed<'arena> {
2272 #[inline(always)]
2274 #[must_use]
2275 pub fn seed_direction(
2276 x: f64,
2277 axis: usize,
2278 u_axis: f64,
2279 dimension: usize,
2280 arena: &'arena DynamicJetArena,
2281 ) -> Self {
2282 Self {
2283 base: DynamicOrder2::variable(x, axis, dimension, arena),
2284 eps: DynamicOrder2::constant(u_axis, dimension, arena),
2285 }
2286 }
2287
2288 #[inline(always)]
2290 #[must_use]
2291 pub fn contracted_third(&self) -> &[f64] {
2292 self.eps.h()
2293 }
2294}
2295
2296impl<'arena> RuntimeJetScalar<'arena> for DynamicOneSeed<'arena> {
2297 type Workspace = DynamicJetArena;
2298
2299 #[inline(always)]
2300 fn constant(c: f64, dimension: usize, arena: &'arena DynamicJetArena) -> Self {
2301 Self {
2302 base: DynamicOrder2::constant(c, dimension, arena),
2303 eps: DynamicOrder2::constant(0.0, dimension, arena),
2304 }
2305 }
2306
2307 #[inline(always)]
2308 fn variable(x: f64, axis: usize, dimension: usize, arena: &'arena DynamicJetArena) -> Self {
2309 Self {
2310 base: DynamicOrder2::variable(x, axis, dimension, arena),
2311 eps: DynamicOrder2::constant(0.0, dimension, arena),
2312 }
2313 }
2314
2315 #[inline(always)]
2316 fn constant_like(&self, c: f64) -> Self {
2317 Self {
2318 base: self.base.constant_like(c),
2319 eps: self.eps.constant_like(0.0),
2320 }
2321 }
2322
2323 #[inline(always)]
2324 fn with_value(&self, value: f64) -> Self {
2325 Self {
2326 base: self.base.with_value(value),
2327 eps: self.eps,
2328 }
2329 }
2330
2331 #[inline(always)]
2332 fn dimension(&self) -> usize {
2333 self.base.dimension()
2334 }
2335
2336 #[inline(always)]
2337 fn value(&self) -> f64 {
2338 self.base.value()
2339 }
2340
2341 #[inline(always)]
2342 fn add(&self, o: &Self) -> Self {
2343 Self {
2344 base: self.base.add(&o.base),
2345 eps: self.eps.add(&o.eps),
2346 }
2347 }
2348
2349 #[inline(always)]
2350 fn sub(&self, o: &Self) -> Self {
2351 Self {
2352 base: self.base.sub(&o.base),
2353 eps: self.eps.sub(&o.eps),
2354 }
2355 }
2356
2357 #[inline(always)]
2358 fn mul(&self, o: &Self) -> Self {
2359 self.base.assert_compatible(&o.base);
2360 self.eps.assert_compatible(&o.eps);
2361 Self {
2362 base: self.base.mul(&o.base),
2363 eps: DynamicOrder2::from_channel_functions(
2364 self.base.v * o.eps.v + self.eps.v * o.base.v,
2365 self.dimension(),
2366 self.base.arena,
2367 |i| {
2368 self.base.v * o.eps.g[i]
2369 + self.base.g[i] * o.eps.v
2370 + self.eps.v * o.base.g[i]
2371 + self.eps.g[i] * o.base.v
2372 },
2373 |i, j| {
2374 let ij = i * self.dimension() + j;
2375 self.base.v * o.eps.h[ij]
2376 + self.base.g[i] * o.eps.g[j]
2377 + self.base.g[j] * o.eps.g[i]
2378 + self.base.h[ij] * o.eps.v
2379 + self.eps.v * o.base.h[ij]
2380 + self.eps.g[i] * o.base.g[j]
2381 + self.eps.g[j] * o.base.g[i]
2382 + self.eps.h[ij] * o.base.v
2383 },
2384 ),
2385 }
2386 }
2387
2388 #[inline(always)]
2389 fn neg(&self) -> Self {
2390 Self {
2391 base: self.base.neg(),
2392 eps: self.eps.neg(),
2393 }
2394 }
2395
2396 #[inline(always)]
2397 fn scale(&self, s: f64) -> Self {
2398 Self {
2399 base: self.base.scale(s),
2400 eps: self.eps.scale(s),
2401 }
2402 }
2403
2404 #[inline(always)]
2405 fn compose_unary(&self, d: [f64; 5]) -> Self {
2406 let base = self.base.compose_unary(d);
2407 let dimension = self.dimension();
2408 let eps = DynamicOrder2::from_channel_functions(
2409 d[1] * self.eps.v,
2410 dimension,
2411 self.base.arena,
2412 |i| d[2] * self.base.g[i] * self.eps.v + d[1] * self.eps.g[i],
2413 |i, j| {
2414 let ij = i * dimension + j;
2415 d[1] * self.eps.h[ij]
2416 + d[2]
2417 * (self.base.g[i] * self.eps.g[j]
2418 + self.base.g[j] * self.eps.g[i]
2419 + self.base.h[ij] * self.eps.v)
2420 + d[3] * self.base.g[i] * self.base.g[j] * self.eps.v
2421 },
2422 );
2423 Self { base, eps }
2424 }
2425}
2426
2427#[derive(Debug)]
2434pub struct DynamicJetBatchWorkspace {
2435 arena: DynamicJetArena,
2436 lanes: usize,
2437}
2438
2439impl DynamicJetBatchWorkspace {
2440 #[must_use]
2442 pub fn new(lanes: usize) -> Self {
2443 Self {
2444 arena: DynamicJetArena::new(),
2445 lanes,
2446 }
2447 }
2448
2449 pub fn reset(&mut self, lanes: usize) {
2451 self.arena.reset();
2452 self.lanes = lanes;
2453 }
2454
2455 #[must_use]
2457 pub fn allocated_bytes(&self) -> usize {
2458 self.arena.allocated_bytes()
2459 }
2460
2461 #[inline(always)]
2463 pub fn alloc_slice_fill_with<T>(&self, len: usize, fill: impl FnMut(usize) -> T) -> &mut [T] {
2464 self.arena.alloc_slice_fill_with(len, fill)
2465 }
2466}
2467
2468#[derive(Clone, Copy, Debug)]
2475pub struct DynamicOneSeedBatch<'arena> {
2476 pub base: DynamicOrder2<'arena>,
2478 eps: &'arena [DynamicOrder2<'arena>],
2480}
2481
2482impl<'arena> DynamicOneSeedBatch<'arena> {
2483 #[inline(always)]
2485 #[must_use]
2486 pub fn seed_directions(
2487 x: f64,
2488 axis: usize,
2489 dimension: usize,
2490 workspace: &'arena DynamicJetBatchWorkspace,
2491 mut direction_at: impl FnMut(usize) -> f64,
2492 ) -> Self {
2493 let eps = workspace
2494 .arena
2495 .alloc_slice_fill_with(workspace.lanes, |lane| {
2496 DynamicOrder2::constant(direction_at(lane), dimension, &workspace.arena)
2497 });
2498 Self {
2499 base: DynamicOrder2::variable(x, axis, dimension, &workspace.arena),
2500 eps,
2501 }
2502 }
2503
2504 #[inline(always)]
2506 #[must_use]
2507 pub fn lanes(&self) -> usize {
2508 self.eps.len()
2509 }
2510
2511 #[inline(always)]
2513 #[must_use]
2514 pub fn contracted_third(&self, lane: usize) -> &[f64] {
2515 self.eps[lane].h()
2516 }
2517
2518 #[inline(always)]
2519 fn assert_compatible(&self, other: &Self) {
2520 self.base.assert_compatible(&other.base);
2521 assert_eq!(
2522 self.eps.len(),
2523 other.eps.len(),
2524 "dynamic one-seed batch lane mismatch"
2525 );
2526 }
2527}
2528
2529impl<'arena> RuntimeJetScalar<'arena> for DynamicOneSeedBatch<'arena> {
2530 type Workspace = DynamicJetBatchWorkspace;
2531
2532 #[inline(always)]
2533 fn constant(c: f64, dimension: usize, workspace: &'arena DynamicJetBatchWorkspace) -> Self {
2534 let eps = workspace.arena.alloc_slice_fill_with(workspace.lanes, |_| {
2535 DynamicOrder2::constant(0.0, dimension, &workspace.arena)
2536 });
2537 Self {
2538 base: DynamicOrder2::constant(c, dimension, &workspace.arena),
2539 eps,
2540 }
2541 }
2542
2543 #[inline(always)]
2544 fn variable(
2545 x: f64,
2546 axis: usize,
2547 dimension: usize,
2548 workspace: &'arena DynamicJetBatchWorkspace,
2549 ) -> Self {
2550 let eps = workspace.arena.alloc_slice_fill_with(workspace.lanes, |_| {
2551 DynamicOrder2::constant(0.0, dimension, &workspace.arena)
2552 });
2553 Self {
2554 base: DynamicOrder2::variable(x, axis, dimension, &workspace.arena),
2555 eps,
2556 }
2557 }
2558
2559 #[inline(always)]
2560 fn constant_like(&self, c: f64) -> Self {
2561 let eps = self
2562 .base
2563 .arena
2564 .alloc_slice_fill_with(self.lanes(), |_| self.base.constant_like(0.0));
2565 Self {
2566 base: self.base.constant_like(c),
2567 eps,
2568 }
2569 }
2570
2571 #[inline(always)]
2572 fn with_value(&self, value: f64) -> Self {
2573 Self {
2574 base: self.base.with_value(value),
2575 eps: self.eps,
2576 }
2577 }
2578
2579 #[inline(always)]
2580 fn dimension(&self) -> usize {
2581 self.base.dimension()
2582 }
2583
2584 #[inline(always)]
2585 fn value(&self) -> f64 {
2586 self.base.value()
2587 }
2588
2589 #[inline(always)]
2590 fn add(&self, other: &Self) -> Self {
2591 self.assert_compatible(other);
2592 let eps = self
2593 .base
2594 .arena
2595 .alloc_slice_fill_with(self.eps.len(), |lane| self.eps[lane].add(&other.eps[lane]));
2596 Self {
2597 base: self.base.add(&other.base),
2598 eps,
2599 }
2600 }
2601
2602 #[inline(always)]
2603 fn sub(&self, other: &Self) -> Self {
2604 self.assert_compatible(other);
2605 let eps = self
2606 .base
2607 .arena
2608 .alloc_slice_fill_with(self.eps.len(), |lane| self.eps[lane].sub(&other.eps[lane]));
2609 Self {
2610 base: self.base.sub(&other.base),
2611 eps,
2612 }
2613 }
2614
2615 #[inline(always)]
2616 fn mul(&self, other: &Self) -> Self {
2617 self.assert_compatible(other);
2618 let eps = self
2621 .base
2622 .arena
2623 .alloc_slice_fill_with(self.eps.len(), |lane| {
2624 DynamicOrder2::scaled_product_sum(
2625 &[1.0, 1.0],
2626 &[self.base, self.eps[lane]],
2627 &[other.eps[lane], other.base],
2628 )
2629 });
2630 Self {
2631 base: self.base.mul(&other.base),
2632 eps,
2633 }
2634 }
2635
2636 #[inline]
2653 fn weighted_compose_sum(
2654 lefts: &[Self],
2655 right: &Self,
2656 derivative_stacks: &[[f64; 5]],
2657 addend: &Self,
2658 ) -> Self {
2659 assert_eq!(
2660 lefts.len(),
2661 derivative_stacks.len(),
2662 "weighted compose sum needs one derivative stack per left factor"
2663 );
2664 for left in lefts {
2665 addend.assert_compatible(left);
2666 }
2667 addend.assert_compatible(right);
2668 let arena = addend.base.arena;
2669 let terms = lefts.len();
2670 let left_bases: &[DynamicOrder2<'arena>] =
2671 arena.alloc_slice_fill_with(terms, |term| lefts[term].base);
2672 let shifted: &[[f64; 5]] = arena.alloc_slice_fill_with(terms, |term| {
2675 let stack = derivative_stacks[term];
2676 [stack[1], stack[2], stack[3], stack[4], stack[4]]
2677 });
2678 let zero = addend.base.constant_like(0.0);
2679 let derivative_sum =
2681 DynamicOrder2::weighted_compose_sum(left_bases, &right.base, shifted, &zero);
2682 let eps = arena.alloc_slice_fill_with(addend.eps.len(), |lane| {
2683 let left_eps: &[DynamicOrder2<'arena>] =
2684 arena.alloc_slice_fill_with(terms, |term| lefts[term].eps[lane]);
2685 let carried = DynamicOrder2::weighted_compose_sum(
2686 left_eps,
2687 &right.base,
2688 derivative_stacks,
2689 &addend.eps[lane],
2690 );
2691 derivative_sum.multiply_add(&right.eps[lane], &carried)
2692 });
2693 Self {
2694 base: DynamicOrder2::weighted_compose_sum(
2695 left_bases,
2696 &right.base,
2697 derivative_stacks,
2698 &addend.base,
2699 ),
2700 eps,
2701 }
2702 }
2703
2704 #[inline(always)]
2705 fn multiply_add(&self, right: &Self, addend: &Self) -> Self {
2706 self.assert_compatible(right);
2707 self.assert_compatible(addend);
2708 let eps = self
2709 .base
2710 .arena
2711 .alloc_slice_fill_with(self.eps.len(), |lane| {
2712 DynamicOrder2::product_pair_sum_plus(
2713 &self.base,
2714 &right.eps[lane],
2715 &self.eps[lane],
2716 &right.base,
2717 &addend.eps[lane],
2718 )
2719 });
2720 Self {
2721 base: self.base.multiply_add(&right.base, &addend.base),
2722 eps,
2723 }
2724 }
2725
2726 #[inline(always)]
2727 fn linear_combination(
2728 inputs: &[Self],
2729 weights: &[f64],
2730 dimension: usize,
2731 workspace: &'arena DynamicJetBatchWorkspace,
2732 ) -> Self {
2733 assert_eq!(inputs.len(), weights.len());
2734 if inputs.is_empty() {
2738 return Self::constant(0.0, dimension, workspace);
2739 }
2740 let lanes = workspace.lanes;
2741 assert!(
2742 inputs
2743 .iter()
2744 .all(|input| input.eps.len() == lanes && input.dimension() == dimension),
2745 "dynamic one-seed linear-combination jets must share lanes and dimension"
2746 );
2747 let arena = &workspace.arena;
2748 let bases: &[DynamicOrder2<'arena>] =
2749 arena.alloc_slice_fill_with(inputs.len(), |term| inputs[term].base);
2750 let eps = arena.alloc_slice_fill_with(lanes, |lane| {
2751 let lane_inputs: &[DynamicOrder2<'arena>] =
2752 arena.alloc_slice_fill_with(inputs.len(), |term| inputs[term].eps[lane]);
2753 DynamicOrder2::linear_combination(lane_inputs, weights, dimension, arena)
2754 });
2755 Self {
2756 base: DynamicOrder2::linear_combination(bases, weights, dimension, arena),
2757 eps,
2758 }
2759 }
2760
2761 #[inline(always)]
2762 fn affine_composed_sum(
2763 inputs: &[Self],
2764 input_scales: &[f64],
2765 derivative_stacks: &[[f64; 5]],
2766 dimension: usize,
2767 workspace: &'arena DynamicJetBatchWorkspace,
2768 ) -> Self {
2769 assert_eq!(inputs.len(), input_scales.len());
2770 assert_eq!(inputs.len(), derivative_stacks.len());
2771 if inputs.is_empty() {
2773 return Self::constant(0.0, dimension, workspace);
2774 }
2775 let lanes = workspace.lanes;
2776 assert!(
2777 inputs
2778 .iter()
2779 .all(|input| input.eps.len() == lanes && input.dimension() == dimension),
2780 "dynamic one-seed composed-sum jets must share lanes and dimension"
2781 );
2782 let arena = &workspace.arena;
2783 let bases: &[DynamicOrder2<'arena>] =
2784 arena.alloc_slice_fill_with(inputs.len(), |term| inputs[term].base);
2785 let fprimes: &[DynamicOrder2<'arena>] = arena.alloc_slice_fill_with(inputs.len(), |term| {
2789 let scale = input_scales[term];
2790 let stack = derivative_stacks[term];
2791 inputs[term].base.compose_unary([
2792 stack[1] * scale,
2793 stack[2] * scale * scale,
2794 stack[3] * scale * scale * scale,
2795 stack[4] * scale * scale * scale * scale,
2796 stack[4] * scale * scale * scale * scale,
2797 ])
2798 });
2799 let ones: &[f64] = arena.alloc_slice_fill_with(inputs.len(), |_| 1.0);
2800 let eps = arena.alloc_slice_fill_with(lanes, |lane| {
2801 let lane_inputs: &[DynamicOrder2<'arena>] =
2802 arena.alloc_slice_fill_with(inputs.len(), |term| inputs[term].eps[lane]);
2803 DynamicOrder2::scaled_product_sum(ones, fprimes, lane_inputs)
2804 });
2805 Self {
2806 base: DynamicOrder2::affine_composed_sum(
2807 bases,
2808 input_scales,
2809 derivative_stacks,
2810 dimension,
2811 arena,
2812 ),
2813 eps,
2814 }
2815 }
2816
2817 #[inline(always)]
2818 fn neg(&self) -> Self {
2819 self.scale(-1.0)
2820 }
2821
2822 #[inline(always)]
2823 fn scale(&self, scale: f64) -> Self {
2824 let eps = self
2825 .base
2826 .arena
2827 .alloc_slice_fill_with(self.eps.len(), |lane| self.eps[lane].scale(scale));
2828 Self {
2829 base: self.base.scale(scale),
2830 eps,
2831 }
2832 }
2833
2834 #[inline(always)]
2835 fn compose_unary(&self, derivatives: [f64; 5]) -> Self {
2836 let fprime = self.base.compose_unary([
2837 derivatives[1],
2838 derivatives[2],
2839 derivatives[3],
2840 derivatives[4],
2841 derivatives[4],
2842 ]);
2843 let eps = self
2844 .base
2845 .arena
2846 .alloc_slice_fill_with(self.eps.len(), |lane| fprime.mul(&self.eps[lane]));
2847 Self {
2848 base: self.base.compose_unary(derivatives),
2849 eps,
2850 }
2851 }
2852}
2853
2854#[derive(Clone, Copy, Debug)]
2861pub struct DynamicTwoSeedBatch<'arena> {
2862 pub base: DynamicOrder2<'arena>,
2864 eps: &'arena [DynamicOrder2<'arena>],
2865 del: &'arena [DynamicOrder2<'arena>],
2866 eps_del: &'arena [DynamicOrder2<'arena>],
2867}
2868
2869impl<'arena> DynamicTwoSeedBatch<'arena> {
2870 #[inline(always)]
2872 #[must_use]
2873 pub fn seed_direction_pairs(
2874 x: f64,
2875 axis: usize,
2876 dimension: usize,
2877 workspace: &'arena DynamicJetBatchWorkspace,
2878 mut direction_pair_at: impl FnMut(usize) -> (f64, f64),
2879 ) -> Self {
2880 let directions = workspace
2881 .arena
2882 .alloc_slice_fill_with(workspace.lanes, |lane| direction_pair_at(lane));
2883 let eps = workspace
2884 .arena
2885 .alloc_slice_fill_with(workspace.lanes, |lane| {
2886 DynamicOrder2::constant(directions[lane].0, dimension, &workspace.arena)
2887 });
2888 let del = workspace
2889 .arena
2890 .alloc_slice_fill_with(workspace.lanes, |lane| {
2891 DynamicOrder2::constant(directions[lane].1, dimension, &workspace.arena)
2892 });
2893 let eps_del = workspace.arena.alloc_slice_fill_with(workspace.lanes, |_| {
2894 DynamicOrder2::constant(0.0, dimension, &workspace.arena)
2895 });
2896 Self {
2897 base: DynamicOrder2::variable(x, axis, dimension, &workspace.arena),
2898 eps,
2899 del,
2900 eps_del,
2901 }
2902 }
2903
2904 #[inline(always)]
2906 #[must_use]
2907 pub fn lanes(&self) -> usize {
2908 self.eps.len()
2909 }
2910
2911 #[inline(always)]
2913 #[must_use]
2914 pub fn contracted_fourth(&self, lane: usize) -> &[f64] {
2915 self.eps_del[lane].h()
2916 }
2917
2918 #[inline(always)]
2919 fn assert_compatible(&self, other: &Self) {
2920 self.base.assert_compatible(&other.base);
2921 assert_eq!(
2922 self.eps.len(),
2923 other.eps.len(),
2924 "dynamic two-seed batch lane mismatch"
2925 );
2926 assert_eq!(
2927 self.del.len(),
2928 self.eps.len(),
2929 "dynamic two-seed batch delta mismatch"
2930 );
2931 assert_eq!(
2932 self.eps_del.len(),
2933 self.eps.len(),
2934 "dynamic two-seed batch cross mismatch"
2935 );
2936 }
2937}
2938
2939impl<'arena> RuntimeJetScalar<'arena> for DynamicTwoSeedBatch<'arena> {
2940 type Workspace = DynamicJetBatchWorkspace;
2941
2942 #[inline(always)]
2943 fn constant(c: f64, dimension: usize, workspace: &'arena Self::Workspace) -> Self {
2944 let zero = workspace.arena.alloc_slice_fill_with(workspace.lanes, |_| {
2945 DynamicOrder2::constant(0.0, dimension, &workspace.arena)
2946 });
2947 Self {
2948 base: DynamicOrder2::constant(c, dimension, &workspace.arena),
2949 eps: zero,
2950 del: zero,
2951 eps_del: zero,
2952 }
2953 }
2954
2955 #[inline(always)]
2956 fn variable(x: f64, axis: usize, dimension: usize, workspace: &'arena Self::Workspace) -> Self {
2957 let zero = workspace.arena.alloc_slice_fill_with(workspace.lanes, |_| {
2958 DynamicOrder2::constant(0.0, dimension, &workspace.arena)
2959 });
2960 Self {
2961 base: DynamicOrder2::variable(x, axis, dimension, &workspace.arena),
2962 eps: zero,
2963 del: zero,
2964 eps_del: zero,
2965 }
2966 }
2967
2968 #[inline(always)]
2969 fn constant_like(&self, c: f64) -> Self {
2970 let zero = self
2971 .base
2972 .arena
2973 .alloc_slice_fill_with(self.lanes(), |_| self.base.constant_like(0.0));
2974 Self {
2975 base: self.base.constant_like(c),
2976 eps: zero,
2977 del: zero,
2978 eps_del: zero,
2979 }
2980 }
2981
2982 #[inline(always)]
2983 fn with_value(&self, value: f64) -> Self {
2984 Self {
2985 base: self.base.with_value(value),
2986 eps: self.eps,
2987 del: self.del,
2988 eps_del: self.eps_del,
2989 }
2990 }
2991
2992 #[inline(always)]
2993 fn dimension(&self) -> usize {
2994 self.base.dimension()
2995 }
2996
2997 #[inline(always)]
2998 fn value(&self) -> f64 {
2999 self.base.value()
3000 }
3001
3002 #[inline(always)]
3003 fn add(&self, other: &Self) -> Self {
3004 self.assert_compatible(other);
3005 let arena = self.base.arena;
3006 let eps =
3007 arena.alloc_slice_fill_with(self.lanes(), |lane| self.eps[lane].add(&other.eps[lane]));
3008 let del =
3009 arena.alloc_slice_fill_with(self.lanes(), |lane| self.del[lane].add(&other.del[lane]));
3010 let eps_del = arena.alloc_slice_fill_with(self.lanes(), |lane| {
3011 self.eps_del[lane].add(&other.eps_del[lane])
3012 });
3013 Self {
3014 base: self.base.add(&other.base),
3015 eps,
3016 del,
3017 eps_del,
3018 }
3019 }
3020
3021 #[inline(always)]
3022 fn sub(&self, other: &Self) -> Self {
3023 self.assert_compatible(other);
3024 let arena = self.base.arena;
3025 let eps =
3026 arena.alloc_slice_fill_with(self.lanes(), |lane| self.eps[lane].sub(&other.eps[lane]));
3027 let del =
3028 arena.alloc_slice_fill_with(self.lanes(), |lane| self.del[lane].sub(&other.del[lane]));
3029 let eps_del = arena.alloc_slice_fill_with(self.lanes(), |lane| {
3030 self.eps_del[lane].sub(&other.eps_del[lane])
3031 });
3032 Self {
3033 base: self.base.sub(&other.base),
3034 eps,
3035 del,
3036 eps_del,
3037 }
3038 }
3039
3040 #[inline(always)]
3041 fn mul(&self, other: &Self) -> Self {
3042 self.assert_compatible(other);
3043 let arena = self.base.arena;
3044 let eps = arena.alloc_slice_fill_with(self.lanes(), |lane| {
3045 self.base
3046 .mul(&other.eps[lane])
3047 .add(&self.eps[lane].mul(&other.base))
3048 });
3049 let del = arena.alloc_slice_fill_with(self.lanes(), |lane| {
3050 self.base
3051 .mul(&other.del[lane])
3052 .add(&self.del[lane].mul(&other.base))
3053 });
3054 let eps_del = arena.alloc_slice_fill_with(self.lanes(), |lane| {
3055 self.base
3056 .mul(&other.eps_del[lane])
3057 .add(&self.eps[lane].mul(&other.del[lane]))
3058 .add(&self.del[lane].mul(&other.eps[lane]))
3059 .add(&self.eps_del[lane].mul(&other.base))
3060 });
3061 Self {
3062 base: self.base.mul(&other.base),
3063 eps,
3064 del,
3065 eps_del,
3066 }
3067 }
3068
3069 #[inline(always)]
3070 fn neg(&self) -> Self {
3071 self.scale(-1.0)
3072 }
3073
3074 #[inline(always)]
3075 fn scale(&self, scale: f64) -> Self {
3076 let arena = self.base.arena;
3077 let eps = arena.alloc_slice_fill_with(self.lanes(), |lane| self.eps[lane].scale(scale));
3078 let del = arena.alloc_slice_fill_with(self.lanes(), |lane| self.del[lane].scale(scale));
3079 let eps_del =
3080 arena.alloc_slice_fill_with(self.lanes(), |lane| self.eps_del[lane].scale(scale));
3081 Self {
3082 base: self.base.scale(scale),
3083 eps,
3084 del,
3085 eps_del,
3086 }
3087 }
3088
3089 #[inline(always)]
3090 fn compose_unary(&self, derivatives: [f64; 5]) -> Self {
3091 let arena = self.base.arena;
3092 let fprime = self.base.compose_unary([
3093 derivatives[1],
3094 derivatives[2],
3095 derivatives[3],
3096 derivatives[4],
3097 derivatives[4],
3098 ]);
3099 let fsecond = self.base.compose_unary([
3100 derivatives[2],
3101 derivatives[3],
3102 derivatives[4],
3103 derivatives[4],
3104 derivatives[4],
3105 ]);
3106 let eps = arena.alloc_slice_fill_with(self.lanes(), |lane| fprime.mul(&self.eps[lane]));
3107 let del = arena.alloc_slice_fill_with(self.lanes(), |lane| fprime.mul(&self.del[lane]));
3108 let eps_del = arena.alloc_slice_fill_with(self.lanes(), |lane| {
3109 fsecond
3110 .mul(&self.eps[lane])
3111 .mul(&self.del[lane])
3112 .add(&fprime.mul(&self.eps_del[lane]))
3113 });
3114 Self {
3115 base: self.base.compose_unary(derivatives),
3116 eps,
3117 del,
3118 eps_del,
3119 }
3120 }
3121}
3122
3123#[derive(Clone, Copy, Debug)]
3125pub struct DynamicTwoSeed<'arena> {
3126 pub base: DynamicOrder2<'arena>,
3128 pub eps: DynamicOrder2<'arena>,
3130 pub del: DynamicOrder2<'arena>,
3132 pub eps_del: DynamicOrder2<'arena>,
3134}
3135
3136impl<'arena> DynamicTwoSeed<'arena> {
3137 #[inline(always)]
3139 #[must_use]
3140 pub fn seed(
3141 x: f64,
3142 axis: usize,
3143 u_axis: f64,
3144 v_axis: f64,
3145 dimension: usize,
3146 arena: &'arena DynamicJetArena,
3147 ) -> Self {
3148 Self {
3149 base: DynamicOrder2::variable(x, axis, dimension, arena),
3150 eps: DynamicOrder2::constant(u_axis, dimension, arena),
3151 del: DynamicOrder2::constant(v_axis, dimension, arena),
3152 eps_del: DynamicOrder2::constant(0.0, dimension, arena),
3153 }
3154 }
3155
3156 #[inline(always)]
3158 #[must_use]
3159 pub fn contracted_fourth(&self) -> &[f64] {
3160 self.eps_del.h()
3161 }
3162}
3163
3164impl<'arena> RuntimeJetScalar<'arena> for DynamicTwoSeed<'arena> {
3165 type Workspace = DynamicJetArena;
3166
3167 #[inline(always)]
3168 fn constant(c: f64, dimension: usize, arena: &'arena DynamicJetArena) -> Self {
3169 Self {
3170 base: DynamicOrder2::constant(c, dimension, arena),
3171 eps: DynamicOrder2::constant(0.0, dimension, arena),
3172 del: DynamicOrder2::constant(0.0, dimension, arena),
3173 eps_del: DynamicOrder2::constant(0.0, dimension, arena),
3174 }
3175 }
3176
3177 #[inline(always)]
3178 fn variable(x: f64, axis: usize, dimension: usize, arena: &'arena DynamicJetArena) -> Self {
3179 Self {
3180 base: DynamicOrder2::variable(x, axis, dimension, arena),
3181 eps: DynamicOrder2::constant(0.0, dimension, arena),
3182 del: DynamicOrder2::constant(0.0, dimension, arena),
3183 eps_del: DynamicOrder2::constant(0.0, dimension, arena),
3184 }
3185 }
3186
3187 #[inline(always)]
3188 fn constant_like(&self, c: f64) -> Self {
3189 Self {
3190 base: self.base.constant_like(c),
3191 eps: self.eps.constant_like(0.0),
3192 del: self.del.constant_like(0.0),
3193 eps_del: self.eps_del.constant_like(0.0),
3194 }
3195 }
3196
3197 #[inline(always)]
3198 fn with_value(&self, value: f64) -> Self {
3199 Self {
3200 base: self.base.with_value(value),
3201 eps: self.eps,
3202 del: self.del,
3203 eps_del: self.eps_del,
3204 }
3205 }
3206
3207 #[inline(always)]
3208 fn dimension(&self) -> usize {
3209 self.base.dimension()
3210 }
3211
3212 #[inline(always)]
3213 fn value(&self) -> f64 {
3214 self.base.value()
3215 }
3216
3217 #[inline(always)]
3218 fn add(&self, o: &Self) -> Self {
3219 Self {
3220 base: self.base.add(&o.base),
3221 eps: self.eps.add(&o.eps),
3222 del: self.del.add(&o.del),
3223 eps_del: self.eps_del.add(&o.eps_del),
3224 }
3225 }
3226
3227 #[inline(always)]
3228 fn sub(&self, o: &Self) -> Self {
3229 Self {
3230 base: self.base.sub(&o.base),
3231 eps: self.eps.sub(&o.eps),
3232 del: self.del.sub(&o.del),
3233 eps_del: self.eps_del.sub(&o.eps_del),
3234 }
3235 }
3236
3237 #[inline(always)]
3238 fn mul(&self, o: &Self) -> Self {
3239 let base = self.base.mul(&o.base);
3240 let eps = self.base.mul(&o.eps).add(&self.eps.mul(&o.base));
3241 let del = self.base.mul(&o.del).add(&self.del.mul(&o.base));
3242 let eps_del = self
3243 .base
3244 .mul(&o.eps_del)
3245 .add(&self.eps.mul(&o.del))
3246 .add(&self.del.mul(&o.eps))
3247 .add(&self.eps_del.mul(&o.base));
3248 Self {
3249 base,
3250 eps,
3251 del,
3252 eps_del,
3253 }
3254 }
3255
3256 #[inline(always)]
3257 fn neg(&self) -> Self {
3258 Self {
3259 base: self.base.neg(),
3260 eps: self.eps.neg(),
3261 del: self.del.neg(),
3262 eps_del: self.eps_del.neg(),
3263 }
3264 }
3265
3266 #[inline(always)]
3267 fn scale(&self, s: f64) -> Self {
3268 Self {
3269 base: self.base.scale(s),
3270 eps: self.eps.scale(s),
3271 del: self.del.scale(s),
3272 eps_del: self.eps_del.scale(s),
3273 }
3274 }
3275
3276 #[inline(always)]
3277 fn compose_unary(&self, d: [f64; 5]) -> Self {
3278 let base = self.base.compose_unary(d);
3279 let fprime = self.base.compose_unary([d[1], d[2], d[3], d[4], d[4]]);
3280 let fsecond = self.base.compose_unary([d[2], d[3], d[4], d[4], d[4]]);
3281 let eps = fprime.mul(&self.eps);
3282 let del = fprime.mul(&self.del);
3283 let eps_del = fsecond
3284 .mul(&self.eps)
3285 .mul(&self.del)
3286 .add(&fprime.mul(&self.eps_del));
3287 Self {
3288 base,
3289 eps,
3290 del,
3291 eps_del,
3292 }
3293 }
3294}
3295
3296impl<const K: usize> std::ops::Add for Order2<K> {
3306 type Output = Self;
3307 #[inline]
3308 fn add(self, o: Self) -> Self {
3309 Order2(self.0 + o.0)
3310 }
3311}
3312
3313impl<const K: usize> std::ops::Add<f64> for Order2<K> {
3314 type Output = Self;
3315 #[inline]
3316 fn add(self, c: f64) -> Self {
3317 Order2(self.0 + c)
3318 }
3319}
3320
3321impl<const K: usize> std::ops::Sub for Order2<K> {
3322 type Output = Self;
3323 #[inline]
3324 fn sub(self, o: Self) -> Self {
3325 Order2(self.0 + o.0.scale(-1.0))
3326 }
3327}
3328
3329impl<const K: usize> std::ops::Sub<f64> for Order2<K> {
3330 type Output = Self;
3331 #[inline]
3332 fn sub(self, c: f64) -> Self {
3333 Order2(self.0 + (-c))
3334 }
3335}
3336
3337impl<const K: usize> std::ops::Mul for Order2<K> {
3338 type Output = Self;
3339 #[inline]
3340 fn mul(self, o: Self) -> Self {
3341 Order2(crate::jet_tower::Tower2::mul(&self.0, &o.0))
3342 }
3343}
3344
3345impl<const K: usize> std::ops::Mul<f64> for Order2<K> {
3346 type Output = Self;
3347 #[inline]
3348 fn mul(self, c: f64) -> Self {
3349 Order2(self.0.scale(c))
3350 }
3351}
3352
3353impl<const K: usize> std::ops::Neg for Order2<K> {
3354 type Output = Self;
3355 #[inline]
3356 fn neg(self) -> Self {
3357 Order2(self.0.scale(-1.0))
3358 }
3359}
3360
3361pub fn filtered_implicit_solve_scalar<const K: usize, S: JetScalar<K>>(
3386 a0: f64,
3387 inv_fa: f64,
3388 iters: usize,
3389 f: impl Fn(&S) -> S,
3390) -> S {
3391 let mut a = S::constant(a0);
3392 for _ in 0..iters {
3393 let residual = f(&a);
3394 a = a.sub(&residual.scale(inv_fa));
3395 }
3396 a
3397}
3398
3399pub fn filtered_implicit_solve_runtime_scalar<'arena, S: RuntimeJetScalar<'arena>>(
3411 a0: f64,
3412 inv_fa: f64,
3413 iters: usize,
3414 dimension: usize,
3415 workspace: &'arena S::Workspace,
3416 f: impl Fn(&S) -> S,
3417) -> S {
3418 let mut a = S::constant(a0, dimension, workspace);
3419 for _ in 0..iters {
3420 let residual = f(&a);
3421 a = a.sub(&residual.scale(inv_fa));
3422 }
3423 a
3424}
3425
3426pub trait HessianPattern<const K: usize, const H: usize> {
3434 const PAIRS: [(usize, usize); H];
3435 const PAIR_BITS: [[u128; K]; K];
3436}
3437
3438pub const fn hessian_pair_bits<const K: usize, const H: usize>(
3441 pairs: [(usize, usize); H],
3442) -> [[u128; K]; K] {
3443 let mut table = [[0u128; K]; K];
3444 let mut slot = 0;
3445 while slot < H {
3446 let (i, j) = pairs[slot];
3447 let bit = 1u128 << slot;
3448 table[i][j] = bit;
3449 table[j][i] = bit;
3450 slot += 1;
3451 }
3452 table
3453}
3454
3455#[derive(Debug)]
3464pub struct PatternedOrder2<P, const K: usize, const H: usize> {
3465 v: f64,
3466 g: [f64; K],
3467 h: [f64; H],
3468 gradient_mask: u128,
3469 hessian_mask: u128,
3470 pattern: std::marker::PhantomData<fn() -> P>,
3471}
3472
3473impl<P, const K: usize, const H: usize> Copy for PatternedOrder2<P, K, H> {}
3474
3475impl<P, const K: usize, const H: usize> Clone for PatternedOrder2<P, K, H> {
3476 fn clone(&self) -> Self {
3477 *self
3478 }
3479}
3480
3481impl<P, const K: usize, const H: usize> PatternedOrder2<P, K, H>
3482where
3483 P: HessianPattern<K, H>,
3484{
3485 #[inline]
3486 #[must_use]
3487 pub fn g(&self) -> [f64; K] {
3488 self.g
3489 }
3490
3491 #[inline]
3495 #[must_use]
3496 pub fn h(&self) -> [[f64; K]; K] {
3497 let mut dense = [[0.0; K]; K];
3498 for (slot, &(i, j)) in P::PAIRS.iter().enumerate() {
3499 dense[i][j] = self.h[slot];
3500 dense[j][i] = self.h[slot];
3501 }
3502 dense
3503 }
3504
3505 #[inline]
3506 fn pair_mask_between(left: u128, right: u128) -> u128 {
3507 let mut result = 0u128;
3508 let mut left_axes = left;
3509 while left_axes != 0 {
3510 let i = left_axes.trailing_zeros() as usize;
3511 left_axes &= left_axes - 1;
3512 let mut right_axes = right;
3513 while right_axes != 0 {
3514 let j = right_axes.trailing_zeros() as usize;
3515 right_axes &= right_axes - 1;
3516 result |= P::PAIR_BITS[i][j];
3517 }
3518 }
3519 result
3520 }
3521}
3522
3523impl<P, const K: usize, const H: usize> JetScalar<K> for PatternedOrder2<P, K, H>
3524where
3525 P: HessianPattern<K, H>,
3526{
3527 #[inline]
3528 fn constant(c: f64) -> Self {
3529 Self {
3530 v: c,
3531 g: [0.0; K],
3532 h: [0.0; H],
3533 gradient_mask: 0,
3534 hessian_mask: 0,
3535 pattern: std::marker::PhantomData,
3536 }
3537 }
3538
3539 #[inline]
3540 fn variable(x: f64, axis: usize) -> Self {
3541 let mut out = Self::constant(x);
3542 if axis < K {
3543 out.g[axis] = 1.0;
3544 out.gradient_mask = 1u128 << axis;
3545 }
3546 out
3547 }
3548}
3549
3550impl<P, const K: usize, const H: usize> crate::nested_dual::JetField for PatternedOrder2<P, K, H>
3551where
3552 P: HessianPattern<K, H>,
3553{
3554 #[inline]
3555 fn value(&self) -> f64 {
3556 self.v
3557 }
3558
3559 #[inline]
3560 fn add(&self, other: &Self) -> Self {
3561 let mut out = Self::constant(self.v + other.v);
3562 out.gradient_mask = self.gradient_mask | other.gradient_mask;
3563 let mut gradient_mask = out.gradient_mask;
3564 while gradient_mask != 0 {
3565 let i = gradient_mask.trailing_zeros() as usize;
3566 gradient_mask &= gradient_mask - 1;
3567 out.g[i] = self.g[i] + other.g[i];
3568 }
3569 out.hessian_mask = self.hessian_mask | other.hessian_mask;
3570 let mut hessian_mask = out.hessian_mask;
3571 while hessian_mask != 0 {
3572 let slot = hessian_mask.trailing_zeros() as usize;
3573 hessian_mask &= hessian_mask - 1;
3574 out.h[slot] = self.h[slot] + other.h[slot];
3575 }
3576 out
3577 }
3578
3579 #[inline]
3580 fn sub(&self, other: &Self) -> Self {
3581 let mut out = Self::constant(self.v - other.v);
3582 out.gradient_mask = self.gradient_mask | other.gradient_mask;
3583 let mut gradient_mask = out.gradient_mask;
3584 while gradient_mask != 0 {
3585 let i = gradient_mask.trailing_zeros() as usize;
3586 gradient_mask &= gradient_mask - 1;
3587 out.g[i] = self.g[i] - other.g[i];
3588 }
3589 out.hessian_mask = self.hessian_mask | other.hessian_mask;
3590 let mut hessian_mask = out.hessian_mask;
3591 while hessian_mask != 0 {
3592 let slot = hessian_mask.trailing_zeros() as usize;
3593 hessian_mask &= hessian_mask - 1;
3594 out.h[slot] = self.h[slot] - other.h[slot];
3595 }
3596 out
3597 }
3598
3599 #[inline]
3600 fn mul(&self, other: &Self) -> Self {
3601 let mut out = Self::constant(self.v * other.v);
3602 out.gradient_mask = self.gradient_mask | other.gradient_mask;
3603 let mut gradient_mask = out.gradient_mask;
3604 while gradient_mask != 0 {
3605 let i = gradient_mask.trailing_zeros() as usize;
3606 gradient_mask &= gradient_mask - 1;
3607 out.g[i] = self.v * other.g[i] + self.g[i] * other.v;
3608 }
3609 out.hessian_mask = self.hessian_mask
3610 | other.hessian_mask
3611 | Self::pair_mask_between(self.gradient_mask, other.gradient_mask);
3612 let mut hessian_mask = out.hessian_mask;
3613 while hessian_mask != 0 {
3614 let slot = hessian_mask.trailing_zeros() as usize;
3615 hessian_mask &= hessian_mask - 1;
3616 let (i, j) = P::PAIRS[slot];
3617 out.h[slot] = self.v * other.h[slot]
3618 + self.g[i] * other.g[j]
3619 + self.g[j] * other.g[i]
3620 + self.h[slot] * other.v;
3621 }
3622 out
3623 }
3624
3625 #[inline]
3626 fn neg(&self) -> Self {
3627 self.scale(-1.0)
3628 }
3629
3630 #[inline]
3631 fn scale(&self, scale: f64) -> Self {
3632 let mut out = Self::constant(self.v * scale);
3633 out.gradient_mask = self.gradient_mask;
3634 let mut gradient_mask = out.gradient_mask;
3635 while gradient_mask != 0 {
3636 let i = gradient_mask.trailing_zeros() as usize;
3637 gradient_mask &= gradient_mask - 1;
3638 out.g[i] = self.g[i] * scale;
3639 }
3640 out.hessian_mask = self.hessian_mask;
3641 let mut hessian_mask = out.hessian_mask;
3642 while hessian_mask != 0 {
3643 let slot = hessian_mask.trailing_zeros() as usize;
3644 hessian_mask &= hessian_mask - 1;
3645 out.h[slot] = self.h[slot] * scale;
3646 }
3647 out
3648 }
3649
3650 #[inline]
3651 fn compose_unary(&self, derivatives: [f64; 5]) -> Self {
3652 let mut out = Self::constant(derivatives[0]);
3653 out.gradient_mask = self.gradient_mask;
3654 let mut gradient_mask = out.gradient_mask;
3655 while gradient_mask != 0 {
3656 let i = gradient_mask.trailing_zeros() as usize;
3657 gradient_mask &= gradient_mask - 1;
3658 out.g[i] = derivatives[1] * self.g[i];
3659 }
3660 out.hessian_mask =
3661 self.hessian_mask | Self::pair_mask_between(self.gradient_mask, self.gradient_mask);
3662 let mut hessian_mask = out.hessian_mask;
3663 while hessian_mask != 0 {
3664 let slot = hessian_mask.trailing_zeros() as usize;
3665 hessian_mask &= hessian_mask - 1;
3666 let (i, j) = P::PAIRS[slot];
3667 out.h[slot] = derivatives[2] * self.g[i] * self.g[j] + derivatives[1] * self.h[slot];
3668 }
3669 out
3670 }
3671}
3672
3673#[derive(Clone, Copy, Debug)]
3686pub struct Order2<const K: usize>(pub crate::jet_tower::Tower2<K>);
3687
3688impl<const K: usize> Order2<K> {
3689 #[inline]
3691 #[must_use]
3692 pub fn g(&self) -> &[f64; K] {
3693 &self.0.g
3694 }
3695
3696 #[inline]
3698 #[must_use]
3699 pub fn h(&self) -> &[[f64; K]; K] {
3700 &self.0.h
3701 }
3702
3703 #[inline]
3705 #[must_use]
3706 pub fn into_channels(self) -> (f64, [f64; K], [[f64; K]; K]) {
3707 let crate::jet_tower::Tower2 { v, g, h } = self.0;
3708 (v, g, h)
3709 }
3710}
3711
3712impl<const K: usize> JetScalar<K> for Order2<K> {
3713 fn constant(c: f64) -> Self {
3714 Order2(crate::jet_tower::Tower2::constant(c))
3715 }
3716 fn variable(x: f64, axis: usize) -> Self {
3717 Order2(crate::jet_tower::Tower2::variable(x, axis))
3718 }
3719
3720 #[inline(always)]
3721 fn symmetric_quadratic_form<C: SymmetricQuadraticCoefficients>(
3722 inputs: &[Self],
3723 coefficients: &C,
3724 ) -> Self {
3725 assert_eq!(inputs.len(), coefficients.dimension());
3726 let input_dimension = inputs.len();
3727 assert!(input_dimension <= K);
3728 let mut values = [0.0; K];
3729 for axis in 0..input_dimension {
3730 values[axis] = inputs[axis].0.v;
3731 }
3732 let mut projected = [0.0; K];
3733 coefficients.multiply(
3734 &values[..input_dimension],
3735 &mut projected[..input_dimension],
3736 );
3737
3738 let mut out = crate::jet_tower::Tower2::zero();
3739 for axis in 0..input_dimension {
3740 out.v += values[axis] * projected[axis];
3741 }
3742 for primary in 0..K {
3743 let mut channel = 0.0;
3744 for axis in 0..input_dimension {
3745 channel += projected[axis] * inputs[axis].0.g[primary];
3746 }
3747 out.g[primary] = 2.0 * channel;
3748 }
3749 let mut input_gradient = [0.0; K];
3750 let mut projected_gradient = [0.0; K];
3751 for primary_b in 0..K {
3752 for row in 0..input_dimension {
3753 input_gradient[row] = inputs[row].0.g[primary_b];
3754 }
3755 coefficients.multiply(
3756 &input_gradient[..input_dimension],
3757 &mut projected_gradient[..input_dimension],
3758 );
3759 for primary_a in 0..=primary_b {
3760 let mut inherited = 0.0;
3761 let mut curvature = 0.0;
3762 for row in 0..input_dimension {
3763 inherited += projected[row] * inputs[row].0.h[primary_a][primary_b];
3764 curvature += inputs[row].0.g[primary_a] * projected_gradient[row];
3765 }
3766 let channel = 2.0 * (inherited + curvature);
3767 out.h[primary_a][primary_b] = channel;
3768 out.h[primary_b][primary_a] = channel;
3769 }
3770 }
3771 Order2(out)
3772 }
3773
3774 #[inline(always)]
3775 fn linear_combination(inputs: &[Self], weights: &[f64]) -> Self {
3776 assert_eq!(inputs.len(), weights.len());
3777 let mut out = crate::jet_tower::Tower2::zero();
3778 for (input, &weight) in inputs.iter().zip(weights) {
3779 out.v += input.0.v * weight;
3780 }
3781 for primary in 0..K {
3782 for (input, &weight) in inputs.iter().zip(weights) {
3783 out.g[primary] += input.0.g[primary] * weight;
3784 }
3785 for other in primary..K {
3786 for (input, &weight) in inputs.iter().zip(weights) {
3787 out.h[primary][other] += input.0.h[primary][other] * weight;
3788 }
3789 out.h[other][primary] = out.h[primary][other];
3790 }
3791 }
3792 Order2(out)
3793 }
3794
3795 #[inline(always)]
3796 fn add_constant(&self, constant: f64) -> Self {
3797 let mut out = *self;
3798 out.0.v += constant;
3799 out
3800 }
3801
3802 #[inline(always)]
3803 fn multiply_add(&self, right: &Self, addend: &Self) -> Self {
3804 let mut out = crate::jet_tower::Tower2::zero();
3805 out.v = self.0.v * right.0.v + addend.0.v;
3806 for primary in 0..K {
3807 out.g[primary] =
3808 self.0.v * right.0.g[primary] + self.0.g[primary] * right.0.v + addend.0.g[primary];
3809 for other in primary..K {
3810 let channel = self.0.v * right.0.h[primary][other]
3811 + self.0.g[primary] * right.0.g[other]
3812 + self.0.g[other] * right.0.g[primary]
3813 + self.0.h[primary][other] * right.0.v
3814 + addend.0.h[primary][other];
3815 out.h[primary][other] = channel;
3816 out.h[other][primary] = channel;
3817 }
3818 }
3819 Order2(out)
3820 }
3821
3822 #[inline(always)]
3823 fn product(&self, right: &Self) -> Self {
3824 let mut out = crate::jet_tower::Tower2::zero();
3825 out.v = self.0.v * right.0.v;
3826 for primary in 0..K {
3827 out.g[primary] = self.0.v * right.0.g[primary] + self.0.g[primary] * right.0.v;
3828 for other in primary..K {
3829 let channel = self.0.v * right.0.h[primary][other]
3830 + self.0.g[primary] * right.0.g[other]
3831 + self.0.g[other] * right.0.g[primary]
3832 + self.0.h[primary][other] * right.0.v;
3833 out.h[primary][other] = channel;
3834 out.h[other][primary] = channel;
3835 }
3836 }
3837 Order2(out)
3838 }
3839
3840 #[inline(always)]
3841 fn affine_compose(
3842 &self,
3843 input_scale: f64,
3844 input_shift: f64,
3845 derivative_stack: [f64; 5],
3846 ) -> Self {
3847 assert!(input_shift.is_finite(), "affine input shift must be finite");
3848 let first = derivative_stack[1] * input_scale;
3849 let second = derivative_stack[2] * input_scale * input_scale;
3850 let mut out = crate::jet_tower::Tower2::zero();
3851 out.v = derivative_stack[0];
3852 for primary in 0..K {
3853 out.g[primary] = first * self.0.g[primary];
3854 for other in primary..K {
3855 let channel =
3856 first * self.0.h[primary][other] + second * self.0.g[primary] * self.0.g[other];
3857 out.h[primary][other] = channel;
3858 out.h[other][primary] = channel;
3859 }
3860 }
3861 Order2(out)
3862 }
3863
3864 #[inline(always)]
3865 fn affine_composed_sum(
3866 inputs: &[Self],
3867 input_scales: &[f64],
3868 derivative_stacks: &[[f64; 5]],
3869 ) -> Self {
3870 assert_eq!(inputs.len(), input_scales.len());
3871 assert_eq!(inputs.len(), derivative_stacks.len());
3872 let mut out = crate::jet_tower::Tower2::zero();
3873 for ((input, &input_scale), stack) in inputs.iter().zip(input_scales).zip(derivative_stacks)
3874 {
3875 let first = stack[1] * input_scale;
3876 let second = stack[2] * input_scale * input_scale;
3877 out.v += stack[0];
3878 for primary in 0..K {
3879 out.g[primary] += first * input.0.g[primary];
3880 for other in primary..K {
3881 out.h[primary][other] += first * input.0.h[primary][other]
3882 + second * input.0.g[primary] * input.0.g[other];
3883 }
3884 }
3885 }
3886 for primary in 0..K {
3887 for other in primary + 1..K {
3888 out.h[other][primary] = out.h[primary][other];
3889 }
3890 }
3891 Order2(out)
3892 }
3893
3894 #[inline(always)]
3895 fn shared_multiply_add_affine_composed_sum<const N: usize>(
3896 lefts: &[&Self; N],
3897 right: &Self,
3898 addend: &Self,
3899 addend_scales: &[f64; N],
3900 input_scales: &[f64; N],
3901 derivative_stacks: &[[f64; 5]; N],
3902 ) -> Self {
3903 let (representatives, term_sources, source_count) =
3904 canonical_shared_source_schedule::<N>(|term, representative| {
3905 std::ptr::eq(lefts[term], lefts[representative])
3906 && addend_scales[term] == addend_scales[representative]
3907 });
3908 let (value, source_derivatives) =
3909 aggregate_shared_source_derivatives(&term_sources, input_scales, derivative_stacks);
3910 let mut source_gradients = [[0.0; K]; N];
3911 let mut out = crate::jet_tower::Tower2::zero();
3912 out.v = value;
3913 let mut right_first = 0.0;
3914 let mut addend_first = 0.0;
3915 for source in 0..source_count {
3916 let term = representatives[source];
3917 let first = source_derivatives[source][1];
3918 right_first += first * lefts[term].0.v;
3919 addend_first += first * addend_scales[term];
3920 for primary in 0..K {
3921 let product_gradient =
3922 lefts[term].0.v * right.0.g[primary] + lefts[term].0.g[primary] * right.0.v;
3923 let inner_gradient = if addend_scales[term] == 0.0 {
3924 product_gradient
3925 } else if addend_scales[term] == 1.0 {
3926 product_gradient + addend.0.g[primary]
3927 } else {
3928 product_gradient + addend_scales[term] * addend.0.g[primary]
3929 };
3930 source_gradients[source][primary] = inner_gradient;
3931 out.g[primary] += first * lefts[term].0.g[primary] * right.0.v;
3932 }
3933 }
3934 if N != 0 {
3935 for primary in 0..K {
3936 out.g[primary] += right_first * right.0.g[primary];
3937 }
3938 }
3939 let addend_live = addend_scales.iter().any(|&scale| scale != 0.0);
3940 if addend_live {
3941 for primary in 0..K {
3942 out.g[primary] += addend_first * addend.0.g[primary];
3943 }
3944 }
3945 for primary in 0..K {
3946 for other in primary..K {
3947 let mut channel = if N == 0 {
3948 0.0
3949 } else {
3950 right_first * right.0.h[primary][other]
3951 };
3952 if addend_live {
3953 channel += addend_first * addend.0.h[primary][other];
3954 }
3955 for source in 0..source_count {
3956 let term = representatives[source];
3957 let local_product_hessian = lefts[term].0.g[primary] * right.0.g[other]
3958 + lefts[term].0.g[other] * right.0.g[primary]
3959 + lefts[term].0.h[primary][other] * right.0.v;
3960 channel += source_derivatives[source][1] * local_product_hessian
3961 + source_derivatives[source][2]
3962 * source_gradients[source][primary]
3963 * source_gradients[source][other];
3964 }
3965 out.h[primary][other] = channel;
3966 out.h[other][primary] = channel;
3967 }
3968 }
3969 Order2(out)
3970 }
3971
3972 #[inline(always)]
3973 fn composed_sum(inputs: &[Self], derivative_stacks: &[[f64; 5]]) -> Self {
3974 assert_eq!(inputs.len(), derivative_stacks.len());
3975 let mut out = crate::jet_tower::Tower2::zero();
3976 for (input, stack) in inputs.iter().zip(derivative_stacks) {
3977 out.v += stack[0];
3978 for primary in 0..K {
3979 out.g[primary] += stack[1] * input.0.g[primary];
3980 for other in primary..K {
3981 out.h[primary][other] += stack[1] * input.0.h[primary][other]
3982 + stack[2] * input.0.g[primary] * input.0.g[other];
3983 }
3984 }
3985 }
3986 for primary in 0..K {
3987 for other in primary + 1..K {
3988 out.h[other][primary] = out.h[primary][other];
3989 }
3990 }
3991 Order2(out)
3992 }
3993}
3994
3995impl<const K: usize> crate::nested_dual::JetField for Order2<K> {
3996 fn value(&self) -> f64 {
3997 self.0.v
3998 }
3999 fn add(&self, o: &Self) -> Self {
4000 Order2(self.0 + o.0)
4001 }
4002 fn sub(&self, o: &Self) -> Self {
4003 Order2(self.0 + o.0.scale(-1.0))
4006 }
4007 fn mul(&self, o: &Self) -> Self {
4008 Order2(crate::jet_tower::Tower2::mul(&self.0, &o.0))
4009 }
4010 fn neg(&self) -> Self {
4011 Order2(self.0.scale(-1.0))
4012 }
4013 fn scale(&self, s: f64) -> Self {
4014 Order2(self.0.scale(s))
4015 }
4016 fn compose_unary(&self, d: [f64; 5]) -> Self {
4017 Order2(self.0.compose_unary([d[0], d[1], d[2]]))
4019 }
4020 fn constant_like(&self, v: f64) -> Self {
4021 <Self as JetScalar<K>>::constant(v)
4025 }
4026 fn with_value(&self, v: f64) -> Self {
4027 let mut out = *self;
4028 out.0.v = v;
4029 out
4030 }
4031}
4032
4033#[derive(Clone, Copy, Debug)]
4058pub struct MappedOrder2Accumulator<const K: usize> {
4059 value: f64,
4060 gradient: [f64; K],
4061 hessian: [[f64; K]; K],
4062}
4063
4064#[derive(Clone, Copy, Debug)]
4071pub struct StaticOrder2Atom<
4072 const N: usize,
4073 const H: usize,
4074 const GRADIENT_BITS: u128,
4075 const HESSIAN_BITS: u128,
4076> {
4077 value: f64,
4078 gradient: [f64; N],
4079 hessian: [f64; H],
4080}
4081
4082impl<const N: usize, const H: usize, const G: u128, const Q: u128> StaticOrder2Atom<N, H, G, Q> {
4083 #[inline(always)]
4085 #[must_use]
4086 pub fn new(value: f64, gradient: [f64; N], hessian: [f64; H]) -> Self {
4087 assert!(H == N * (N + 1) / 2, "invalid packed order-two shape");
4088 assert!(N <= 128 && H <= 128, "static atom sparsity mask overflow");
4089 Self {
4090 value,
4091 gradient,
4092 hessian,
4093 }
4094 }
4095
4096 #[inline(always)]
4098 #[must_use]
4099 pub fn value(&self) -> f64 {
4100 self.value
4101 }
4102
4103 #[inline(always)]
4105 #[must_use]
4106 pub fn gradient(&self) -> [f64; N] {
4107 self.gradient
4108 }
4109
4110 #[inline(always)]
4112 #[must_use]
4113 pub fn hessian_at(&self, row: usize, column: usize) -> f64 {
4114 assert!(
4115 row < N && column < N,
4116 "static atom Hessian axis out of range"
4117 );
4118 let (row, column) = if row <= column {
4119 (row, column)
4120 } else {
4121 (column, row)
4122 };
4123 let index = row * (2 * N - row + 1) / 2 + column - row;
4124 self.hessian[index]
4125 }
4126}
4127
4128pub trait Order2AtomChannels<const N: usize> {
4133 const GRADIENT_BITS: u128;
4135 const HESSIAN_BITS: u128;
4137 fn gradient_at(&self, axis: usize) -> f64;
4139 fn hessian_at(&self, row: usize, column: usize) -> f64;
4141}
4142
4143impl<const N: usize> Order2AtomChannels<N> for Order2<N> {
4144 const GRADIENT_BITS: u128 = low_mask(N);
4145 const HESSIAN_BITS: u128 = low_mask(N * (N + 1) / 2);
4146
4147 #[inline(always)]
4148 fn gradient_at(&self, axis: usize) -> f64 {
4149 self.0.g[axis]
4150 }
4151
4152 #[inline(always)]
4153 fn hessian_at(&self, row: usize, column: usize) -> f64 {
4154 self.0.h[row][column]
4155 }
4156}
4157
4158impl<const N: usize, const H: usize, const G: u128, const Q: u128> Order2AtomChannels<N>
4159 for StaticOrder2Atom<N, H, G, Q>
4160{
4161 const GRADIENT_BITS: u128 = G;
4162 const HESSIAN_BITS: u128 = Q;
4163
4164 #[inline(always)]
4165 fn gradient_at(&self, axis: usize) -> f64 {
4166 self.gradient[axis]
4167 }
4168
4169 #[inline(always)]
4170 fn hessian_at(&self, row: usize, column: usize) -> f64 {
4171 StaticOrder2Atom::hessian_at(self, row, column)
4172 }
4173}
4174
4175const fn low_mask(channels: usize) -> u128 {
4176 if channels >= 128 {
4177 u128::MAX
4178 } else {
4179 (1u128 << channels) - 1
4180 }
4181}
4182
4183impl<const K: usize> MappedOrder2Accumulator<K> {
4184 #[inline(always)]
4186 #[must_use]
4187 pub fn zero() -> Self {
4188 Self {
4189 value: 0.0,
4190 gradient: [0.0; K],
4191 hessian: [[0.0; K]; K],
4192 }
4193 }
4194
4195 #[inline(always)]
4202 pub fn add_composed<const N: usize, const H: usize, A: Order2AtomChannels<N>>(
4203 &mut self,
4204 atom: &A,
4205 axes: [usize; N],
4206 derivatives: [f64; 3],
4207 value_add: bool,
4208 gradient_add: [bool; N],
4209 hessian_add: [bool; H],
4210 ) {
4211 assert!(H == N * (N + 1) / 2, "invalid mapped Hessian write shape");
4212 assert!(N <= 128 && H <= 128, "mapped atom sparsity mask overflow");
4213 assert!(
4214 axes.iter().all(|&axis| axis < K),
4215 "mapped atom axis must be within the global primary dimension"
4216 );
4217 assert!(
4218 axes.iter()
4219 .enumerate()
4220 .all(|(i, axis)| !axes[..i].contains(axis)),
4221 "mapped atom axes must be injective"
4222 );
4223
4224 if value_add {
4225 self.value += derivatives[0];
4226 } else {
4227 self.value = derivatives[0];
4228 }
4229 let mut packed = 0;
4230 for local_i in 0..N {
4231 let global_i = axes[local_i];
4232 if A::GRADIENT_BITS & (1u128 << local_i) != 0 {
4233 let channel = derivatives[1] * atom.gradient_at(local_i);
4234 if gradient_add[local_i] {
4235 self.gradient[global_i] += channel;
4236 } else {
4237 self.gradient[global_i] = channel;
4238 }
4239 }
4240 for local_j in local_i..N {
4241 let global_j = axes[local_j];
4242 let inner_live = A::HESSIAN_BITS & (1u128 << packed) != 0;
4243 let outer_live = A::GRADIENT_BITS & (1u128 << local_i) != 0
4244 && A::GRADIENT_BITS & (1u128 << local_j) != 0;
4245 let channel = if inner_live {
4246 let inner = derivatives[1] * atom.hessian_at(local_i, local_j);
4247 if outer_live {
4248 inner
4249 + derivatives[2] * atom.gradient_at(local_i) * atom.gradient_at(local_j)
4250 } else {
4251 inner
4252 }
4253 } else if outer_live {
4254 derivatives[2] * atom.gradient_at(local_i) * atom.gradient_at(local_j)
4255 } else {
4256 packed += 1;
4257 continue;
4258 };
4259 if hessian_add[packed] {
4260 self.hessian[global_i][global_j] += channel;
4261 if global_i != global_j {
4262 self.hessian[global_j][global_i] += channel;
4263 }
4264 } else {
4265 self.hessian[global_i][global_j] = channel;
4266 if global_i != global_j {
4267 self.hessian[global_j][global_i] = channel;
4268 }
4269 }
4270 packed += 1;
4271 }
4272 }
4273 }
4274
4275 #[inline(always)]
4277 #[must_use]
4278 pub fn into_channels(self) -> (f64, [f64; K], [[f64; K]; K]) {
4279 (self.value, self.gradient, self.hessian)
4280 }
4281}
4282
4283pub trait DynamicOrder2Term {
4290 fn outer_first(&self) -> f64;
4292
4293 fn outer_second(&self) -> f64;
4295
4296 fn inner_gradient(&self, axis: usize) -> f64;
4298
4299 fn inner_hessian(&self, row: usize, column: usize) -> f64;
4301}
4302
4303#[derive(Debug)]
4320pub struct DynamicOrder2Accumulator {
4321 value: f64,
4322 gradient: Vec<f64>,
4323 hessian: Vec<f64>,
4324}
4325
4326impl DynamicOrder2Accumulator {
4327 #[inline(always)]
4329 #[must_use]
4330 pub fn from_composed_sum<T: DynamicOrder2Term, const N: usize>(
4331 dimension: usize,
4332 value: f64,
4333 terms: &[T; N],
4334 ) -> Self {
4335 let mut gradient = vec![0.0; dimension];
4336 let mut hessian = vec![0.0; dimension * dimension];
4337
4338 for axis in 0..dimension {
4339 let mut channel = 0.0;
4340 for term in terms {
4341 channel += term.outer_first() * term.inner_gradient(axis);
4342 }
4343 gradient[axis] = channel;
4344 }
4345
4346 for row in 0..dimension {
4347 for column in row..dimension {
4348 let mut channel = 0.0;
4349 for term in terms {
4350 let row_gradient = term.inner_gradient(row);
4351 let column_gradient = term.inner_gradient(column);
4352 channel += term.outer_second() * row_gradient * column_gradient
4353 + term.outer_first() * term.inner_hessian(row, column);
4354 }
4355 hessian[row * dimension + column] = channel;
4356 hessian[column * dimension + row] = channel;
4357 }
4358 }
4359
4360 Self {
4361 value,
4362 gradient,
4363 hessian,
4364 }
4365 }
4366
4367 #[inline(always)]
4369 #[must_use]
4370 pub fn into_channels(self) -> (f64, Vec<f64>, Vec<f64>) {
4371 (self.value, self.gradient, self.hessian)
4372 }
4373}
4374
4375pub trait Lane: Copy {
4411 const LANES: usize;
4414 fn splat(x: f64) -> Self;
4416 fn add(self, o: Self) -> Self;
4418 fn sub(self, o: Self) -> Self;
4420 fn mul(self, o: Self) -> Self;
4422 fn lane(self, i: usize) -> f64;
4425 fn unary3(self, stack: impl Fn(f64) -> [f64; 3]) -> [Self; 3];
4431 fn unary5(self, stack: impl Fn(f64) -> [f64; 5]) -> [Self; 5];
4440}
4441
4442impl Lane for f64 {
4443 const LANES: usize = 1;
4444 #[inline]
4445 fn splat(x: f64) -> Self {
4446 x
4447 }
4448 #[inline]
4449 fn add(self, o: Self) -> Self {
4450 self + o
4451 }
4452 #[inline]
4453 fn sub(self, o: Self) -> Self {
4454 self - o
4455 }
4456 #[inline]
4457 fn mul(self, o: Self) -> Self {
4458 self * o
4459 }
4460 #[inline]
4461 fn lane(self, i: usize) -> f64 {
4462 assert!(
4463 i < <Self as Lane>::LANES,
4464 "the f64 Lane carries one row; lane {i} does not exist"
4465 );
4466 self
4467 }
4468 #[inline]
4469 fn unary3(self, stack: impl Fn(f64) -> [f64; 3]) -> [Self; 3] {
4470 stack(self)
4471 }
4472 #[inline]
4473 fn unary5(self, stack: impl Fn(f64) -> [f64; 5]) -> [Self; 5] {
4474 stack(self)
4475 }
4476}
4477
4478impl Lane for wide::f64x4 {
4479 const LANES: usize = 4;
4480 #[inline]
4481 fn splat(x: f64) -> Self {
4482 wide::f64x4::splat(x)
4483 }
4484 #[inline]
4485 fn add(self, o: Self) -> Self {
4486 self + o
4487 }
4488 #[inline]
4489 fn sub(self, o: Self) -> Self {
4490 self - o
4491 }
4492 #[inline]
4493 fn mul(self, o: Self) -> Self {
4494 self * o
4495 }
4496 #[inline]
4497 fn lane(self, i: usize) -> f64 {
4498 self.to_array()[i]
4499 }
4500 #[inline]
4501 fn unary3(self, stack: impl Fn(f64) -> [f64; 3]) -> [Self; 3] {
4502 let a = self.to_array();
4503 let mut d0 = [0.0_f64; 4];
4504 let mut d1 = [0.0_f64; 4];
4505 let mut d2 = [0.0_f64; 4];
4506 for i in 0..4 {
4507 let s = stack(a[i]);
4508 d0[i] = s[0];
4509 d1[i] = s[1];
4510 d2[i] = s[2];
4511 }
4512 [
4513 wide::f64x4::new(d0),
4514 wide::f64x4::new(d1),
4515 wide::f64x4::new(d2),
4516 ]
4517 }
4518 #[inline]
4519 fn unary5(self, stack: impl Fn(f64) -> [f64; 5]) -> [Self; 5] {
4520 let a = self.to_array();
4521 let mut d = [[0.0_f64; 4]; 5];
4522 for i in 0..4 {
4523 let s = stack(a[i]);
4524 for (k, dk) in d.iter_mut().enumerate() {
4525 dk[i] = s[k];
4526 }
4527 }
4528 [
4529 wide::f64x4::new(d[0]),
4530 wide::f64x4::new(d[1]),
4531 wide::f64x4::new(d[2]),
4532 wide::f64x4::new(d[3]),
4533 wide::f64x4::new(d[4]),
4534 ]
4535 }
4536}
4537
4538#[derive(Clone, Copy, Debug)]
4548pub struct Order2Lane<L: Lane, const K: usize> {
4549 pub v: L,
4551 pub g: [L; K],
4553 pub h: [[L; K]; K],
4555}
4556
4557pub type Order2Batch<const K: usize> = Order2Lane<wide::f64x4, K>;
4559
4560impl<L: Lane, const K: usize> Order2Lane<L, K> {
4561 #[inline]
4563 pub fn constant(c: L) -> Self {
4564 Order2Lane {
4565 v: c,
4566 g: [L::splat(0.0); K],
4567 h: [[L::splat(0.0); K]; K],
4568 }
4569 }
4570
4571 #[inline]
4575 pub fn variable(value: L, axis: usize) -> Self {
4576 let mut out = Self::constant(value);
4577 out.g[axis] = L::splat(1.0);
4578 out
4579 }
4580
4581 #[inline]
4583 pub fn add(&self, o: &Self) -> Self {
4584 let mut out = *self;
4585 out.v = self.v.add(o.v);
4586 for i in 0..K {
4587 out.g[i] = self.g[i].add(o.g[i]);
4588 for j in 0..K {
4589 out.h[i][j] = self.h[i][j].add(o.h[i][j]);
4590 }
4591 }
4592 out
4593 }
4594
4595 #[inline]
4597 pub fn scale(&self, s: f64) -> Self {
4598 let sl = L::splat(s);
4599 let mut out = *self;
4600 out.v = self.v.mul(sl);
4601 for i in 0..K {
4602 out.g[i] = self.g[i].mul(sl);
4603 for j in 0..K {
4604 out.h[i][j] = self.h[i][j].mul(sl);
4605 }
4606 }
4607 out
4608 }
4609
4610 #[inline]
4613 pub fn sub(&self, o: &Self) -> Self {
4614 self.add(&o.scale(-1.0))
4615 }
4616
4617 #[inline]
4619 pub fn neg(&self) -> Self {
4620 self.scale(-1.0)
4621 }
4622
4623 #[inline]
4638 pub fn mul(&self, o: &Self) -> Self {
4639 let a = self;
4640 let b = o;
4641 let mut out = Self::constant(a.v.mul(b.v));
4642 for i in 0..K {
4643 out.g[i] = a.v.mul(b.g[i]).add(a.g[i].mul(b.v));
4645 }
4646 for i in 0..K {
4647 for j in i..K {
4648 let hij =
4650 a.v.mul(b.h[i][j])
4651 .add(a.g[i].mul(b.g[j]))
4652 .add(a.g[j].mul(b.g[i]))
4653 .add(a.h[i][j].mul(b.v));
4654 out.h[i][j] = hij;
4655 out.h[j][i] = hij;
4656 }
4657 }
4658 out
4659 }
4660
4661 #[inline]
4666 pub fn compose_unary(&self, d: [L; 3]) -> Self {
4667 let mut out = Self::constant(d[0]);
4668 for i in 0..K {
4669 let mut acc = L::splat(0.0);
4670 acc = acc.add(d[1].mul(self.g[i]));
4671 out.g[i] = acc;
4672 }
4673 for i in 0..K {
4674 for j in 0..K {
4675 let mut acc = L::splat(0.0);
4676 acc = acc.add(d[1].mul(self.h[i][j]));
4677 acc = acc.add(d[2].mul(self.g[i]).mul(self.g[j]));
4678 out.h[i][j] = acc;
4679 }
4680 }
4681 out
4682 }
4683
4684 #[inline]
4687 pub fn exp(&self) -> Self {
4688 let d = self.v.unary3(|u| {
4689 let e = u.exp();
4690 [e, e, e]
4691 });
4692 self.compose_unary(d)
4693 }
4694
4695 #[inline]
4698 pub fn ln(&self) -> Self {
4699 let d = self.v.unary3(|u| {
4700 let r = 1.0 / u;
4701 [u.ln(), r, -r * r]
4702 });
4703 self.compose_unary(d)
4704 }
4705
4706 #[inline]
4709 pub fn sqrt(&self) -> Self {
4710 let d = self.v.unary3(|u| {
4711 let s = u.sqrt();
4712 [s, 0.5 / s, -0.25 / (u * s)]
4713 });
4714 self.compose_unary(d)
4715 }
4716
4717 #[inline]
4719 pub fn recip(&self) -> Self {
4720 let d = self.v.unary3(|u| {
4721 let r = 1.0 / u;
4722 let r2 = r * r;
4723 [r, -r2, 2.0 * r2 * r]
4724 });
4725 self.compose_unary(d)
4726 }
4727
4728 #[inline]
4731 pub fn powf(&self, a: f64) -> Self {
4732 let d = self.v.unary3(|u| {
4733 [
4734 u.powf(a),
4735 a * u.powf(a - 1.0),
4736 a * (a - 1.0) * u.powf(a - 2.0),
4737 ]
4738 });
4739 self.compose_unary(d)
4740 }
4741}
4742
4743impl<const K: usize> Order2Batch<K> {
4744 #[inline]
4748 #[must_use]
4749 pub fn lane(&self, i: usize) -> Order2<K> {
4750 let mut t = crate::jet_tower::Tower2::<K>::constant(self.v.lane(i));
4751 for a in 0..K {
4752 t.g[a] = self.g[a].lane(i);
4753 for b in 0..K {
4754 t.h[a][b] = self.h[a][b].lane(i);
4755 }
4756 }
4757 Order2(t)
4758 }
4759}
4760
4761#[derive(Clone, Copy, Debug)]
4777pub struct Order1<const K: usize> {
4778 pub v: f64,
4780 pub g: [f64; K],
4782}
4783
4784impl<const K: usize> Order1<K> {
4785 #[inline]
4787 #[must_use]
4788 pub fn g(&self) -> &[f64; K] {
4789 &self.g
4790 }
4791
4792 #[inline]
4794 #[must_use]
4795 pub fn into_channels(self) -> (f64, [f64; K]) {
4796 (self.v, self.g)
4797 }
4798}
4799
4800impl<const K: usize> JetScalar<K> for Order1<K> {
4801 fn constant(c: f64) -> Self {
4802 Order1 { v: c, g: [0.0; K] }
4804 }
4805 fn variable(x: f64, axis: usize) -> Self {
4806 let mut g = [0.0; K];
4808 g[axis] = 1.0;
4809 Order1 { v: x, g }
4810 }
4811}
4812
4813impl<const K: usize> crate::nested_dual::JetField for Order1<K> {
4814 fn value(&self) -> f64 {
4815 self.v
4816 }
4817 fn add(&self, o: &Self) -> Self {
4818 let mut g = self.g;
4820 for i in 0..K {
4821 g[i] += o.g[i];
4822 }
4823 Order1 { v: self.v + o.v, g }
4824 }
4825 fn sub(&self, o: &Self) -> Self {
4826 self.add(&o.scale(-1.0))
4828 }
4829 fn mul(&self, o: &Self) -> Self {
4830 let a = self;
4835 let b = o;
4836 let mut g = [0.0; K];
4837 for i in 0..K {
4838 g[i] = a.v * b.g[i] + a.g[i] * b.v;
4839 }
4840 Order1 { v: a.v * b.v, g }
4841 }
4842 fn neg(&self) -> Self {
4843 self.scale(-1.0)
4845 }
4846 fn scale(&self, s: f64) -> Self {
4847 let mut g = self.g;
4849 for i in 0..K {
4850 g[i] *= s;
4851 }
4852 Order1 { v: self.v * s, g }
4853 }
4854 fn compose_unary(&self, d: [f64; 5]) -> Self {
4855 let mut g = [0.0; K];
4861 for i in 0..K {
4862 g[i] = d[1] * self.g[i];
4863 }
4864 Order1 { v: d[0], g }
4865 }
4866}
4867
4868#[derive(Clone, Copy, Debug)]
4885pub struct OneSeed<const K: usize> {
4886 pub base: Order2<K>,
4888 pub eps: Order2<K>,
4891}
4892
4893impl<const K: usize> OneSeed<K> {
4894 pub fn seed_direction(x: f64, axis: usize, u_axis: f64) -> Self {
4898 OneSeed {
4899 base: Order2::variable(x, axis),
4900 eps: Order2::constant(u_axis),
4901 }
4902 }
4903
4904 pub fn contracted_third(&self) -> [[f64; K]; K] {
4907 *self.eps.h()
4908 }
4909}
4910
4911impl<const K: usize> JetScalar<K> for OneSeed<K> {
4912 fn constant(c: f64) -> Self {
4913 OneSeed {
4914 base: Order2::constant(c),
4915 eps: Order2::constant(0.0),
4916 }
4917 }
4918 fn variable(x: f64, axis: usize) -> Self {
4919 OneSeed {
4921 base: Order2::variable(x, axis),
4922 eps: Order2::constant(0.0),
4923 }
4924 }
4925}
4926
4927impl<const K: usize> crate::nested_dual::JetField for OneSeed<K> {
4928 fn value(&self) -> f64 {
4929 self.base.value()
4930 }
4931 fn add(&self, o: &Self) -> Self {
4932 OneSeed {
4933 base: self.base.add(&o.base),
4934 eps: self.eps.add(&o.eps),
4935 }
4936 }
4937 fn sub(&self, o: &Self) -> Self {
4938 OneSeed {
4939 base: self.base.sub(&o.base),
4940 eps: self.eps.sub(&o.eps),
4941 }
4942 }
4943 fn mul(&self, o: &Self) -> Self {
4944 let ab = &self.base.0;
4950 let ae = &self.eps.0;
4951 let bb = &o.base.0;
4952 let be = &o.eps.0;
4953 let mut eps = crate::jet_tower::Tower2::<K>::zero();
4954 eps.v = ab.v * be.v + ae.v * bb.v;
4955 for i in 0..K {
4956 eps.g[i] = ab.v * be.g[i] + ab.g[i] * be.v + ae.v * bb.g[i] + ae.g[i] * bb.v;
4957 }
4958 for i in 0..K {
4959 for j in i..K {
4960 let channel = ab.v * be.h[i][j]
4961 + ab.g[i] * be.g[j]
4962 + ab.g[j] * be.g[i]
4963 + ab.h[i][j] * be.v
4964 + ae.v * bb.h[i][j]
4965 + ae.g[i] * bb.g[j]
4966 + ae.g[j] * bb.g[i]
4967 + ae.h[i][j] * bb.v;
4968 eps.h[i][j] = channel;
4969 eps.h[j][i] = channel;
4970 }
4971 }
4972 OneSeed {
4973 base: self.base.mul(&o.base),
4974 eps: Order2(eps),
4975 }
4976 }
4977 fn neg(&self) -> Self {
4978 OneSeed {
4979 base: self.base.neg(),
4980 eps: self.eps.neg(),
4981 }
4982 }
4983 fn scale(&self, s: f64) -> Self {
4984 OneSeed {
4985 base: self.base.scale(s),
4986 eps: self.eps.scale(s),
4987 }
4988 }
4989 fn compose_unary(&self, d: [f64; 5]) -> Self {
4990 let base = self.base.compose_unary([d[0], d[1], d[2], d[3], d[4]]);
4995 let b = &self.base.0;
4996 let e = &self.eps.0;
4997 let mut eps = crate::jet_tower::Tower2::<K>::zero();
4998 eps.v = d[1] * e.v;
4999 for i in 0..K {
5000 eps.g[i] = d[2] * b.g[i] * e.v + d[1] * e.g[i];
5001 }
5002 for i in 0..K {
5003 for j in i..K {
5004 let channel = d[1] * e.h[i][j]
5005 + d[2] * (b.g[i] * e.g[j] + b.g[j] * e.g[i] + b.h[i][j] * e.v)
5006 + d[3] * b.g[i] * b.g[j] * e.v;
5007 eps.h[i][j] = channel;
5008 eps.h[j][i] = channel;
5009 }
5010 }
5011 OneSeed {
5012 base,
5013 eps: Order2(eps),
5014 }
5015 }
5016 fn constant_like(&self, v: f64) -> Self {
5017 OneSeed {
5018 base: self.base.constant_like(v),
5019 eps: self.eps.constant_like(0.0),
5020 }
5021 }
5022 fn with_value(&self, v: f64) -> Self {
5023 OneSeed {
5026 base: self.base.with_value(v),
5027 eps: self.eps,
5028 }
5029 }
5030}
5031
5032#[derive(Clone, Copy, Debug)]
5044pub struct OneSeedLane<L: Lane, const K: usize> {
5045 pub base: Order2Lane<L, K>,
5047 pub eps: Order2Lane<L, K>,
5050}
5051
5052pub type OneSeedBatch<const K: usize> = OneSeedLane<wide::f64x4, K>;
5054
5055impl<L: Lane, const K: usize> OneSeedLane<L, K> {
5056 #[inline]
5058 pub fn constant(c: L) -> Self {
5059 OneSeedLane {
5060 base: Order2Lane::constant(c),
5061 eps: Order2Lane::constant(L::splat(0.0)),
5062 }
5063 }
5064
5065 #[inline]
5068 pub fn variable(value: L, axis: usize) -> Self {
5069 OneSeedLane {
5070 base: Order2Lane::variable(value, axis),
5071 eps: Order2Lane::constant(L::splat(0.0)),
5072 }
5073 }
5074
5075 #[inline]
5080 pub fn seed_direction(value: L, axis: usize, u_axis: L) -> Self {
5081 OneSeedLane {
5082 base: Order2Lane::variable(value, axis),
5083 eps: Order2Lane::constant(u_axis),
5084 }
5085 }
5086
5087 #[inline]
5090 #[must_use]
5091 pub fn contracted_third(&self) -> [[L; K]; K] {
5092 self.eps.h
5093 }
5094
5095 #[inline]
5097 pub fn add(&self, o: &Self) -> Self {
5098 OneSeedLane {
5099 base: self.base.add(&o.base),
5100 eps: self.eps.add(&o.eps),
5101 }
5102 }
5103
5104 #[inline]
5106 pub fn sub(&self, o: &Self) -> Self {
5107 OneSeedLane {
5108 base: self.base.sub(&o.base),
5109 eps: self.eps.sub(&o.eps),
5110 }
5111 }
5112
5113 #[inline]
5115 pub fn mul(&self, o: &Self) -> Self {
5116 let ab = &self.base;
5117 let ae = &self.eps;
5118 let bb = &o.base;
5119 let be = &o.eps;
5120 let mut eps = Order2Lane::constant(ab.v.mul(be.v).add(ae.v.mul(bb.v)));
5121 for i in 0..K {
5122 eps.g[i] =
5123 ab.v.mul(be.g[i])
5124 .add(ab.g[i].mul(be.v))
5125 .add(ae.v.mul(bb.g[i]))
5126 .add(ae.g[i].mul(bb.v));
5127 }
5128 for i in 0..K {
5129 for j in i..K {
5130 let channel =
5131 ab.v.mul(be.h[i][j])
5132 .add(ab.g[i].mul(be.g[j]))
5133 .add(ab.g[j].mul(be.g[i]))
5134 .add(ab.h[i][j].mul(be.v))
5135 .add(ae.v.mul(bb.h[i][j]))
5136 .add(ae.g[i].mul(bb.g[j]))
5137 .add(ae.g[j].mul(bb.g[i]))
5138 .add(ae.h[i][j].mul(bb.v));
5139 eps.h[i][j] = channel;
5140 eps.h[j][i] = channel;
5141 }
5142 }
5143 OneSeedLane {
5144 base: self.base.mul(&o.base),
5145 eps,
5146 }
5147 }
5148
5149 #[inline]
5151 pub fn neg(&self) -> Self {
5152 OneSeedLane {
5153 base: self.base.neg(),
5154 eps: self.eps.neg(),
5155 }
5156 }
5157
5158 #[inline]
5160 pub fn scale(&self, s: f64) -> Self {
5161 OneSeedLane {
5162 base: self.base.scale(s),
5163 eps: self.eps.scale(s),
5164 }
5165 }
5166
5167 #[inline]
5173 pub fn compose_unary(&self, d: [L; 5]) -> Self {
5174 let base = self.base.compose_unary([d[0], d[1], d[2]]);
5175 let b = &self.base;
5176 let e = &self.eps;
5177 let mut eps = Order2Lane::constant(d[1].mul(e.v));
5178 for i in 0..K {
5179 eps.g[i] = d[2].mul(b.g[i]).mul(e.v).add(d[1].mul(e.g[i]));
5180 }
5181 for i in 0..K {
5182 for j in i..K {
5183 let mixed = b.g[i]
5184 .mul(e.g[j])
5185 .add(b.g[j].mul(e.g[i]))
5186 .add(b.h[i][j].mul(e.v));
5187 let channel = d[1]
5188 .mul(e.h[i][j])
5189 .add(d[2].mul(mixed))
5190 .add(d[3].mul(b.g[i]).mul(b.g[j]).mul(e.v));
5191 eps.h[i][j] = channel;
5192 eps.h[j][i] = channel;
5193 }
5194 }
5195 OneSeedLane { base, eps }
5196 }
5197
5198 #[inline]
5200 pub fn exp(&self) -> Self {
5201 let d = self.base.v.unary5(|u| {
5202 let e = u.exp();
5203 [e, e, e, e, e]
5204 });
5205 self.compose_unary(d)
5206 }
5207
5208 #[inline]
5210 pub fn ln(&self) -> Self {
5211 let d = self.base.v.unary5(|u| {
5212 let r = 1.0 / u;
5213 [u.ln(), r, -r * r, 2.0 * r * r * r, -6.0 * r * r * r * r]
5214 });
5215 self.compose_unary(d)
5216 }
5217
5218 #[inline]
5220 pub fn sqrt(&self) -> Self {
5221 let d = self.base.v.unary5(|u| {
5222 let s = u.sqrt();
5223 [
5224 s,
5225 0.5 / s,
5226 -0.25 / (u * s),
5227 0.375 / (u * u * s),
5228 -0.9375 / (u * u * u * s),
5229 ]
5230 });
5231 self.compose_unary(d)
5232 }
5233
5234 #[inline]
5236 pub fn recip(&self) -> Self {
5237 let d = self.base.v.unary5(|u| {
5238 let r = 1.0 / u;
5239 let r2 = r * r;
5240 [r, -r2, 2.0 * r2 * r, -6.0 * r2 * r2, 24.0 * r2 * r2 * r]
5241 });
5242 self.compose_unary(d)
5243 }
5244
5245 #[inline]
5248 pub fn powf(&self, a: f64) -> Self {
5249 let d = self.base.v.unary5(|u| {
5250 [
5251 u.powf(a),
5252 a * u.powf(a - 1.0),
5253 a * (a - 1.0) * u.powf(a - 2.0),
5254 a * (a - 1.0) * (a - 2.0) * u.powf(a - 3.0),
5255 a * (a - 1.0) * (a - 2.0) * (a - 3.0) * u.powf(a - 4.0),
5256 ]
5257 });
5258 self.compose_unary(d)
5259 }
5260
5261 #[inline]
5264 pub fn ln_gamma(&self) -> Self {
5265 let d = self
5266 .base
5267 .v
5268 .unary5(crate::jet_tower::ln_gamma_derivative_stack);
5269 self.compose_unary(d)
5270 }
5271
5272}
5273
5274impl<const K: usize> OneSeedBatch<K> {
5275 #[inline]
5279 #[must_use]
5280 pub fn lane(&self, i: usize) -> OneSeed<K> {
5281 OneSeed {
5282 base: self.base.lane(i),
5283 eps: self.eps.lane(i),
5284 }
5285 }
5286}
5287
5288#[derive(Clone, Copy, Debug)]
5305pub struct TwoSeed<const K: usize> {
5306 pub base: Order2<K>,
5308 pub eps: Order2<K>,
5310 pub del: Order2<K>,
5312 pub eps_del: Order2<K>,
5315}
5316
5317impl<const K: usize> TwoSeed<K> {
5318 pub fn seed(x: f64, axis: usize, u_axis: f64, v_axis: f64) -> Self {
5322 TwoSeed {
5323 base: Order2::variable(x, axis),
5324 eps: Order2::constant(u_axis),
5325 del: Order2::constant(v_axis),
5326 eps_del: Order2::constant(0.0),
5327 }
5328 }
5329
5330 pub fn contracted_fourth(&self) -> [[f64; K]; K] {
5333 *self.eps_del.h()
5334 }
5335}
5336
5337impl<const K: usize> JetScalar<K> for TwoSeed<K> {
5338 fn constant(c: f64) -> Self {
5339 TwoSeed {
5340 base: Order2::constant(c),
5341 eps: Order2::constant(0.0),
5342 del: Order2::constant(0.0),
5343 eps_del: Order2::constant(0.0),
5344 }
5345 }
5346 fn variable(x: f64, axis: usize) -> Self {
5347 TwoSeed {
5348 base: Order2::variable(x, axis),
5349 eps: Order2::constant(0.0),
5350 del: Order2::constant(0.0),
5351 eps_del: Order2::constant(0.0),
5352 }
5353 }
5354}
5355
5356impl<const K: usize> crate::nested_dual::JetField for TwoSeed<K> {
5357 fn value(&self) -> f64 {
5358 self.base.value()
5359 }
5360 fn add(&self, o: &Self) -> Self {
5361 TwoSeed {
5362 base: self.base.add(&o.base),
5363 eps: self.eps.add(&o.eps),
5364 del: self.del.add(&o.del),
5365 eps_del: self.eps_del.add(&o.eps_del),
5366 }
5367 }
5368 fn sub(&self, o: &Self) -> Self {
5369 TwoSeed {
5370 base: self.base.sub(&o.base),
5371 eps: self.eps.sub(&o.eps),
5372 del: self.del.sub(&o.del),
5373 eps_del: self.eps_del.sub(&o.eps_del),
5374 }
5375 }
5376 fn mul(&self, o: &Self) -> Self {
5377 let a = self;
5378 let b = o;
5379 let base = a.base.mul(&b.base);
5381 let eps = a.base.mul(&b.eps).add(&a.eps.mul(&b.base));
5382 let del = a.base.mul(&b.del).add(&a.del.mul(&b.base));
5383 let eps_del = a
5384 .base
5385 .mul(&b.eps_del)
5386 .add(&a.eps.mul(&b.del))
5387 .add(&a.del.mul(&b.eps))
5388 .add(&a.eps_del.mul(&b.base));
5389 TwoSeed {
5390 base,
5391 eps,
5392 del,
5393 eps_del,
5394 }
5395 }
5396 fn neg(&self) -> Self {
5397 TwoSeed {
5398 base: self.base.neg(),
5399 eps: self.eps.neg(),
5400 del: self.del.neg(),
5401 eps_del: self.eps_del.neg(),
5402 }
5403 }
5404 fn scale(&self, s: f64) -> Self {
5405 TwoSeed {
5406 base: self.base.scale(s),
5407 eps: self.eps.scale(s),
5408 del: self.del.scale(s),
5409 eps_del: self.eps_del.scale(s),
5410 }
5411 }
5412 fn compose_unary(&self, d: [f64; 5]) -> Self {
5413 let base = self.base.compose_unary([d[0], d[1], d[2], d[3], d[4]]);
5423 let fprime = self.base.compose_unary([d[1], d[2], d[3], d[4], d[4]]); let fsecond = self.base.compose_unary([d[2], d[3], d[4], d[4], d[4]]); let eps = fprime.mul(&self.eps);
5426 let del = fprime.mul(&self.del);
5427 let eps_del = fsecond
5428 .mul(&self.eps)
5429 .mul(&self.del)
5430 .add(&fprime.mul(&self.eps_del));
5431 TwoSeed {
5432 base,
5433 eps,
5434 del,
5435 eps_del,
5436 }
5437 }
5438}
5439
5440#[derive(Clone, Copy, Debug)]
5451pub struct TwoSeedLane<L: Lane, const K: usize> {
5452 pub base: Order2Lane<L, K>,
5454 pub eps: Order2Lane<L, K>,
5456 pub del: Order2Lane<L, K>,
5458 pub eps_del: Order2Lane<L, K>,
5461}
5462
5463pub type TwoSeedBatch<const K: usize> = TwoSeedLane<wide::f64x4, K>;
5465
5466impl<L: Lane, const K: usize> TwoSeedLane<L, K> {
5467 #[inline]
5470 pub fn constant(c: L) -> Self {
5471 let z = Order2Lane::constant(L::splat(0.0));
5472 TwoSeedLane {
5473 base: Order2Lane::constant(c),
5474 eps: z,
5475 del: z,
5476 eps_del: z,
5477 }
5478 }
5479
5480 #[inline]
5483 pub fn variable(value: L, axis: usize) -> Self {
5484 let z = Order2Lane::constant(L::splat(0.0));
5485 TwoSeedLane {
5486 base: Order2Lane::variable(value, axis),
5487 eps: z,
5488 del: z,
5489 eps_del: z,
5490 }
5491 }
5492
5493 #[inline]
5497 pub fn seed(value: L, axis: usize, u_axis: L, v_axis: L) -> Self {
5498 TwoSeedLane {
5499 base: Order2Lane::variable(value, axis),
5500 eps: Order2Lane::constant(u_axis),
5501 del: Order2Lane::constant(v_axis),
5502 eps_del: Order2Lane::constant(L::splat(0.0)),
5503 }
5504 }
5505
5506 #[inline]
5510 #[must_use]
5511 pub fn contracted_fourth(&self) -> [[L; K]; K] {
5512 self.eps_del.h
5513 }
5514
5515 #[inline]
5517 pub fn add(&self, o: &Self) -> Self {
5518 TwoSeedLane {
5519 base: self.base.add(&o.base),
5520 eps: self.eps.add(&o.eps),
5521 del: self.del.add(&o.del),
5522 eps_del: self.eps_del.add(&o.eps_del),
5523 }
5524 }
5525
5526 #[inline]
5528 pub fn sub(&self, o: &Self) -> Self {
5529 TwoSeedLane {
5530 base: self.base.sub(&o.base),
5531 eps: self.eps.sub(&o.eps),
5532 del: self.del.sub(&o.del),
5533 eps_del: self.eps_del.sub(&o.eps_del),
5534 }
5535 }
5536
5537 #[inline]
5539 pub fn mul(&self, o: &Self) -> Self {
5540 let a = self;
5541 let b = o;
5542 let base = a.base.mul(&b.base);
5543 let eps = a.base.mul(&b.eps).add(&a.eps.mul(&b.base));
5544 let del = a.base.mul(&b.del).add(&a.del.mul(&b.base));
5545 let eps_del = a
5546 .base
5547 .mul(&b.eps_del)
5548 .add(&a.eps.mul(&b.del))
5549 .add(&a.del.mul(&b.eps))
5550 .add(&a.eps_del.mul(&b.base));
5551 TwoSeedLane {
5552 base,
5553 eps,
5554 del,
5555 eps_del,
5556 }
5557 }
5558
5559 #[inline]
5561 pub fn neg(&self) -> Self {
5562 TwoSeedLane {
5563 base: self.base.neg(),
5564 eps: self.eps.neg(),
5565 del: self.del.neg(),
5566 eps_del: self.eps_del.neg(),
5567 }
5568 }
5569
5570 #[inline]
5572 pub fn scale(&self, s: f64) -> Self {
5573 TwoSeedLane {
5574 base: self.base.scale(s),
5575 eps: self.eps.scale(s),
5576 del: self.del.scale(s),
5577 eps_del: self.eps_del.scale(s),
5578 }
5579 }
5580
5581 #[inline]
5587 pub fn compose_unary(&self, d: [L; 5]) -> Self {
5588 let base = self.base.compose_unary([d[0], d[1], d[2]]);
5589 let fprime = self.base.compose_unary([d[1], d[2], d[3]]);
5590 let fsecond = self.base.compose_unary([d[2], d[3], d[4]]);
5591 let eps = fprime.mul(&self.eps);
5592 let del = fprime.mul(&self.del);
5593 let eps_del = fsecond
5594 .mul(&self.eps)
5595 .mul(&self.del)
5596 .add(&fprime.mul(&self.eps_del));
5597 TwoSeedLane {
5598 base,
5599 eps,
5600 del,
5601 eps_del,
5602 }
5603 }
5604
5605 #[inline]
5607 pub fn exp(&self) -> Self {
5608 let d = self.base.v.unary5(|u| {
5609 let e = u.exp();
5610 [e, e, e, e, e]
5611 });
5612 self.compose_unary(d)
5613 }
5614
5615 #[inline]
5617 pub fn ln(&self) -> Self {
5618 let d = self.base.v.unary5(|u| {
5619 let r = 1.0 / u;
5620 [u.ln(), r, -r * r, 2.0 * r * r * r, -6.0 * r * r * r * r]
5621 });
5622 self.compose_unary(d)
5623 }
5624
5625 #[inline]
5627 pub fn sqrt(&self) -> Self {
5628 let d = self.base.v.unary5(|u| {
5629 let s = u.sqrt();
5630 [
5631 s,
5632 0.5 / s,
5633 -0.25 / (u * s),
5634 0.375 / (u * u * s),
5635 -0.9375 / (u * u * u * s),
5636 ]
5637 });
5638 self.compose_unary(d)
5639 }
5640
5641 #[inline]
5643 pub fn recip(&self) -> Self {
5644 let d = self.base.v.unary5(|u| {
5645 let r = 1.0 / u;
5646 let r2 = r * r;
5647 [r, -r2, 2.0 * r2 * r, -6.0 * r2 * r2, 24.0 * r2 * r2 * r]
5648 });
5649 self.compose_unary(d)
5650 }
5651
5652 #[inline]
5655 pub fn powf(&self, a: f64) -> Self {
5656 let d = self.base.v.unary5(|u| {
5657 [
5658 u.powf(a),
5659 a * u.powf(a - 1.0),
5660 a * (a - 1.0) * u.powf(a - 2.0),
5661 a * (a - 1.0) * (a - 2.0) * u.powf(a - 3.0),
5662 a * (a - 1.0) * (a - 2.0) * (a - 3.0) * u.powf(a - 4.0),
5663 ]
5664 });
5665 self.compose_unary(d)
5666 }
5667
5668 #[inline]
5670 pub fn ln_gamma(&self) -> Self {
5671 let d = self
5672 .base
5673 .v
5674 .unary5(crate::jet_tower::ln_gamma_derivative_stack);
5675 self.compose_unary(d)
5676 }
5677
5678}
5679
5680impl<const K: usize> TwoSeedBatch<K> {
5681 #[inline]
5685 #[must_use]
5686 pub fn lane(&self, i: usize) -> TwoSeed<K> {
5687 TwoSeed {
5688 base: self.base.lane(i),
5689 eps: self.eps.lane(i),
5690 del: self.del.lane(i),
5691 eps_del: self.eps_del.lane(i),
5692 }
5693 }
5694}
5695
5696impl<const K: usize> JetScalar<K> for crate::jet_tower::Tower3<K> {
5703 fn constant(c: f64) -> Self {
5704 crate::jet_tower::Tower3::constant(c)
5705 }
5706 fn variable(x: f64, axis: usize) -> Self {
5707 crate::jet_tower::Tower3::variable(x, axis)
5708 }
5709}
5710
5711impl<const K: usize> crate::nested_dual::JetField for crate::jet_tower::Tower3<K> {
5712 fn value(&self) -> f64 {
5713 self.v
5714 }
5715 fn add(&self, o: &Self) -> Self {
5716 *self + *o
5717 }
5718 fn sub(&self, o: &Self) -> Self {
5719 *self + o.scale(-1.0)
5720 }
5721 fn mul(&self, o: &Self) -> Self {
5722 crate::jet_tower::Tower3::mul(self, o)
5723 }
5724 fn neg(&self) -> Self {
5725 self.scale(-1.0)
5726 }
5727 fn scale(&self, s: f64) -> Self {
5728 crate::jet_tower::Tower3::scale(self, s)
5729 }
5730 fn compose_unary(&self, d: [f64; 5]) -> Self {
5731 crate::jet_tower::Tower3::compose_unary(self, [d[0], d[1], d[2], d[3]])
5732 }
5733}
5734
5735impl<const K: usize> JetScalar<K> for crate::jet_tower::Tower4<K> {
5750 fn constant(c: f64) -> Self {
5751 crate::jet_tower::Tower4::constant(c)
5752 }
5753 fn variable(x: f64, axis: usize) -> Self {
5754 crate::jet_tower::Tower4::variable(x, axis)
5755 }
5756}
5757
5758impl<const K: usize> crate::nested_dual::JetField for crate::jet_tower::Tower4<K> {
5759 fn value(&self) -> f64 {
5760 self.v
5761 }
5762 fn add(&self, o: &Self) -> Self {
5763 *self + *o
5764 }
5765 fn sub(&self, o: &Self) -> Self {
5766 *self - *o
5767 }
5768 fn mul(&self, o: &Self) -> Self {
5769 crate::jet_tower::Tower4::mul(self, o)
5770 }
5771 fn neg(&self) -> Self {
5772 self.scale(-1.0)
5773 }
5774 fn scale(&self, s: f64) -> Self {
5775 crate::jet_tower::Tower4::scale(self, s)
5776 }
5777 fn compose_unary(&self, d: [f64; 5]) -> Self {
5778 crate::jet_tower::Tower4::compose_unary(self, d)
5779 }
5780}
5781
5782#[cfg(test)]
5783mod tests {
5784 use super::*;
5785 use crate::jet_tower::{RowProgram, Tower4, program_full_tower};
5786 use crate::nested_dual::JetField;
5787
5788 #[test]
5789 fn runtime_fused_product_composition_preserves_tower4_channels() {
5790 const K: usize = 2;
5791 const N: usize = 9;
5792 let vars = [
5793 Tower4::<K>::variable(0.37, 0),
5794 Tower4::<K>::variable(-0.61, 1),
5795 ];
5796 let upstream = [
5797 vars[0].mul(&vars[1]).add(&vars[0].exp()),
5798 vars[1].mul(&vars[1]).add(&vars[0].scale(0.3)),
5799 vars[0].mul(&vars[0]).sub(&vars[1].scale(-0.2)),
5800 ];
5801 let mut lefts: [Tower4<K>; N] = std::array::from_fn(|term| upstream[term % upstream.len()]);
5802 lefts[4] = lefts[0];
5803 lefts[5] = lefts[0];
5804 let right = upstream[1];
5805 let addend = upstream[2];
5806 let addend_scales: [f64; N] = std::array::from_fn(|term| [-0.0, 1.0, -0.7, 0.25][term % 4]);
5807 let mut input_scales: [f64; N] =
5808 std::array::from_fn(|term| [0.0, -1.3, 0.45, 1.1][term % 4]);
5809 input_scales[0] = -1.1;
5810 input_scales[4] = 1.1;
5811 let stacks: [[f64; 5]; N] = std::array::from_fn(|term| {
5812 let t = term as f64 + 1.0;
5813 [0.17 * t, -0.11 * t, 0.07 * t, -0.03 * t, 0.013 * t]
5814 });
5815
5816 let expected = (0..N).fold(Tower4::<K>::constant(0.0), |sum, term| {
5817 let inner = if addend_scales[term] == 0.0 {
5818 lefts[term].mul(&right)
5819 } else if addend_scales[term] == 1.0 {
5820 JetScalar::multiply_add(&lefts[term], &right, &addend)
5821 } else {
5822 JetScalar::multiply_add(&lefts[term], &right, &addend.scale(addend_scales[term]))
5823 };
5824 sum.add(&JetScalar::affine_compose(
5825 &inner,
5826 input_scales[term],
5827 0.0,
5828 stacks[term],
5829 ))
5830 });
5831 let wrapped_lefts: [FixedRuntimeJet<Tower4<K>, K>; N] =
5832 std::array::from_fn(|term| FixedRuntimeJet::from_inner(lefts[term]));
5833 let wrapped_right = FixedRuntimeJet::from_inner(right);
5834 let wrapped_addend = FixedRuntimeJet::from_inner(addend);
5835 let mut wrapped_left_refs: [&FixedRuntimeJet<Tower4<K>, K>; N] =
5836 std::array::from_fn(|term| &wrapped_lefts[term]);
5837 wrapped_left_refs[4] = &wrapped_lefts[0];
5838 wrapped_left_refs[5] = &wrapped_lefts[0];
5839 let actual = FixedRuntimeJet::<Tower4<K>, K>::shared_multiply_add_affine_composed_sum(
5840 &wrapped_left_refs,
5841 &wrapped_right,
5842 &wrapped_addend,
5843 &addend_scales,
5844 &input_scales,
5845 &stacks,
5846 K,
5847 &(),
5848 )
5849 .into_inner();
5850
5851 let same = |label: &str, got: f64, want: f64| {
5852 let tolerance = 2.0e-13 * got.abs().max(want.abs()).max(1.0);
5853 assert!(
5854 (got - want).abs() <= tolerance,
5855 "{label}: got={got:+.17e}, want={want:+.17e}, tolerance={tolerance:.3e}"
5856 );
5857 };
5858 same("value", actual.v, expected.v);
5859 for a in 0..K {
5860 same("gradient", actual.g[a], expected.g[a]);
5861 for b in 0..K {
5862 same("Hessian", actual.h[a][b], expected.h[a][b]);
5863 for c in 0..K {
5864 same("third", actual.t3[a][b][c], expected.t3[a][b][c]);
5865 for d in 0..K {
5866 same("fourth", actual.t4[a][b][c][d], expected.t4[a][b][c][d]);
5867 }
5868 }
5869 }
5870 }
5871 }
5872
5873 fn row_expr<S: JetScalar<2>>(p: &[S; 2]) -> S {
5878 let g = p[0].mul(&p[1]).exp();
5879 let inner = g.add(&S::constant(2.0));
5880 let radic = p[0].mul(&p[0]).add(&S::constant(1.0)).sqrt();
5881 inner.mul(&radic).sub(&p[1].mul(&p[1]).scale(0.5))
5882 }
5883
5884 struct ExprProgram {
5886 p: [f64; 2],
5887 }
5888 impl RowProgram<2> for ExprProgram {
5889 fn n_rows(&self) -> usize {
5890 1
5891 }
5892 fn primaries(&self, row: usize) -> Result<[f64; 2], String> {
5893 if row >= self.n_rows() {
5894 return Err(format!("ExprProgram: row {row} out of range"));
5895 }
5896 Ok(self.p)
5897 }
5898 fn eval<S: JetScalar<2>>(&self, row: usize, p: &[S; 2]) -> Result<S, String> {
5899 if row >= self.n_rows() {
5900 return Err(format!("ExprProgram: row {row} out of range"));
5901 }
5902 Ok(row_expr(p))
5903 }
5904 }
5905
5906 const SEED: [f64; 2] = [0.37, -0.81];
5907 const TOL: f64 = 1e-10;
5908
5909 fn close(a: f64, b: f64, label: &str) {
5910 let band = TOL + TOL * a.abs().max(b.abs());
5911 assert!(
5912 (a - b).abs() <= band,
5913 "{label}: {a:+.15e} vs {b:+.15e} (band {band:.3e})"
5914 );
5915 }
5916
5917 fn tower() -> Tower4<2> {
5918 *program_full_tower(&ExprProgram { p: SEED }, 0).expect("tower")
5919 }
5920
5921 #[test]
5923 fn order2_matches_tower_value_grad_hessian() {
5924 let t = tower();
5925 let vars: [Order2<2>; 2] = std::array::from_fn(|a| Order2::variable(SEED[a], a));
5926 let s = row_expr(&vars);
5927 close(s.value(), t.v, "value");
5928 for a in 0..2 {
5929 close(s.0.g[a], t.g[a], &format!("grad[{a}]"));
5930 for b in 0..2 {
5931 close(s.h()[a][b], t.h[a][b], &format!("hess[{a}][{b}]"));
5932 }
5933 }
5934 }
5935
5936 #[test]
5937 fn mapped_order2_accumulator_matches_dense_overlapping_atoms() {
5938 const K: usize = 4;
5939 let p = [0.2_f64, 0.7, -0.4, 0.3];
5940 let dense_vars: [Order2<K>; K] =
5941 std::array::from_fn(|axis| Order2::variable(p[axis], axis));
5942 let dense_q0 = dense_vars[3].mul(&dense_vars[1]).add(&dense_vars[3].exp());
5943 let dense_q1 = dense_vars[1].mul(&dense_vars[2]).sub(&dense_vars[2]);
5944 let dense = dense_q0.ln().add(&dense_q1.exp());
5945
5946 let local_q0_vars: [Order2<2>; 2] =
5947 std::array::from_fn(|axis| Order2::variable(p[[3, 1][axis]], axis));
5948 let local_q0 = local_q0_vars[0]
5949 .mul(&local_q0_vars[1])
5950 .add(&local_q0_vars[0].exp());
5951 let local_q1_vars: [Order2<2>; 2] =
5952 std::array::from_fn(|axis| Order2::variable(p[[1, 2][axis]], axis));
5953 let local_q1 = local_q1_vars[0]
5954 .mul(&local_q1_vars[1])
5955 .sub(&local_q1_vars[1]);
5956
5957 let q0 = local_q0.value();
5958 let q1_exp = local_q1.value().exp();
5959 let mut lowered = MappedOrder2Accumulator::<K>::zero();
5960 lowered.add_composed(
5961 &local_q0,
5962 [3, 1],
5963 [q0.ln(), q0.recip(), -1.0 / (q0 * q0)],
5964 false,
5965 [false, false],
5966 [false, false, false],
5967 );
5968 lowered.add_composed(
5969 &local_q1,
5970 [1, 2],
5971 [q1_exp, q1_exp, q1_exp],
5972 true,
5973 [true, false],
5974 [true, false, false],
5975 );
5976 let (value, gradient, hessian) = lowered.into_channels();
5977
5978 close(value, dense.value(), "mapped value");
5979 for i in 0..K {
5980 close(gradient[i], dense.g()[i], &format!("mapped gradient[{i}]"));
5981 for j in 0..K {
5982 close(
5983 hessian[i][j],
5984 dense.h()[i][j],
5985 &format!("mapped Hessian[{i},{j}]"),
5986 );
5987 }
5988 }
5989 }
5990
5991 #[test]
5992 #[should_panic(expected = "mapped atom axes must be injective")]
5993 fn mapped_order2_accumulator_rejects_duplicate_axes() {
5994 let vars: [Order2<2>; 2] = std::array::from_fn(|axis| Order2::variable(0.2, axis));
5995 let atom = vars[0].add(&vars[1]);
5996 let mut lowered = MappedOrder2Accumulator::<2>::zero();
5997 lowered.add_composed(
5998 &atom,
5999 [1, 1],
6000 [0.4, 1.0, 0.0],
6001 false,
6002 [false, false],
6003 [false, false, false],
6004 );
6005 }
6006
6007 #[test]
6008 #[should_panic(expected = "mapped atom axis must be within")]
6009 fn mapped_order2_accumulator_rejects_out_of_range_axes() {
6010 let atom = Order2::<1>::variable(0.2, 0);
6011 let mut lowered = MappedOrder2Accumulator::<2>::zero();
6012 lowered.add_composed(&atom, [2], [0.2, 1.0, 0.0], false, [false], [false]);
6013 }
6014
6015 #[test]
6016 fn dynamic_order2_accumulator_matches_dense_composed_sum() {
6017 const K: usize = 4;
6018
6019 struct Term {
6020 first: f64,
6021 second: f64,
6022 gradient: [f64; K],
6023 hessian: [[f64; K]; K],
6024 }
6025
6026 impl DynamicOrder2Term for Term {
6027 fn outer_first(&self) -> f64 {
6028 self.first
6029 }
6030
6031 fn outer_second(&self) -> f64 {
6032 self.second
6033 }
6034
6035 fn inner_gradient(&self, axis: usize) -> f64 {
6036 self.gradient[axis]
6037 }
6038
6039 fn inner_hessian(&self, row: usize, column: usize) -> f64 {
6040 self.hessian[row][column]
6041 }
6042 }
6043
6044 let p = [0.7, -0.3, 0.2, 0.8];
6045 let vars: [Order2<K>; K] = std::array::from_fn(|axis| Order2::variable(p[axis], axis));
6046 let first_atom = vars[0]
6047 .mul(&vars[1])
6048 .add(&vars[2].exp())
6049 .add(&Order2::constant(1.5));
6050 let second_atom = vars[1].mul(&vars[3]).sub(&vars[0]);
6051 let first_value = first_atom.value();
6052 let second_exp = second_atom.value().exp();
6053 let first_stack = [
6054 first_value.ln(),
6055 first_value.recip(),
6056 -1.0 / (first_value * first_value),
6057 0.0,
6058 0.0,
6059 ];
6060 let second_stack = [second_exp, second_exp, second_exp, second_exp, second_exp];
6061 let dense = first_atom
6062 .compose_unary(first_stack)
6063 .add(&second_atom.compose_unary(second_stack));
6064 let terms = [
6065 Term {
6066 first: first_stack[1],
6067 second: first_stack[2],
6068 gradient: *first_atom.g(),
6069 hessian: *first_atom.h(),
6070 },
6071 Term {
6072 first: second_stack[1],
6073 second: second_stack[2],
6074 gradient: *second_atom.g(),
6075 hessian: *second_atom.h(),
6076 },
6077 ];
6078 let (value, gradient, hessian) = DynamicOrder2Accumulator::from_composed_sum(
6079 K,
6080 first_stack[0] + second_stack[0],
6081 &terms,
6082 )
6083 .into_channels();
6084
6085 close(value, dense.value(), "dynamic value");
6086 for row in 0..K {
6087 close(
6088 gradient[row],
6089 dense.g()[row],
6090 &format!("dynamic gradient[{row}]"),
6091 );
6092 for column in 0..K {
6093 close(
6094 hessian[row * K + column],
6095 dense.h()[row][column],
6096 &format!("dynamic Hessian[{row},{column}]"),
6097 );
6098 }
6099 }
6100 }
6101
6102 #[derive(Clone, Copy, Debug)]
6103 struct FullTwoPattern;
6104
6105 impl HessianPattern<2, 3> for FullTwoPattern {
6106 const PAIRS: [(usize, usize); 3] = [(0, 0), (0, 1), (1, 1)];
6107 const PAIR_BITS: [[u128; 2]; 2] = hessian_pair_bits(Self::PAIRS);
6108 }
6109
6110 #[test]
6113 fn patterned_order2_matches_dense_order2() {
6114 type Sparse = PatternedOrder2<FullTwoPattern, 2, 3>;
6115 let dense_vars: [Order2<2>; 2] = std::array::from_fn(|a| Order2::variable(SEED[a], a));
6116 let sparse_vars: [Sparse; 2] = std::array::from_fn(|a| Sparse::variable(SEED[a], a));
6117 let dense = row_expr(&dense_vars);
6118 let sparse = row_expr(&sparse_vars);
6119 close(sparse.value(), dense.value(), "patterned value");
6120 for i in 0..2 {
6121 close(sparse.g()[i], dense.g()[i], &format!("patterned grad[{i}]"));
6122 for j in 0..2 {
6123 close(
6124 sparse.h()[i][j],
6125 dense.h()[i][j],
6126 &format!("patterned hess[{i}][{j}]"),
6127 );
6128 }
6129 }
6130 }
6131
6132 #[test]
6136 fn compose_unary_with_scalar_seam_bit_identical() {
6137 fn rand_unit(state: &mut u64) -> f64 {
6138 let mut x = *state;
6139 x ^= x << 13;
6140 x ^= x >> 7;
6141 x ^= x << 17;
6142 *state = x;
6143 2.0 * ((x >> 11) as f64 / ((1u64 << 53) as f64)) - 1.0
6144 }
6145 fn stack(u: f64) -> [f64; 5] {
6147 [
6148 u.sin(),
6149 u.cos(),
6150 (2.0 * u).sin(),
6151 (0.5 * u).cos(),
6152 u * u - 0.3,
6153 ]
6154 }
6155 fn run<const K: usize>(state: &mut u64, n: usize) -> usize {
6156 for _ in 0..n {
6157 let base = rand_unit(state);
6160 let mut s = Order2::<K>::variable(base, 0);
6161 for a in 1..K {
6162 s = crate::nested_dual::JetField::mul(
6163 &s,
6164 &Order2::<K>::variable(rand_unit(state), a),
6165 );
6166 }
6167 let with = s.compose_unary_with(stack);
6168 let explicit = s.compose_unary(stack(s.value()));
6169 assert_eq!(with.value().to_bits(), explicit.value().to_bits(), "value");
6170 for a in 0..K {
6171 assert_eq!(with.g()[a].to_bits(), explicit.g()[a].to_bits(), "g[{a}]");
6172 for b in 0..K {
6173 assert_eq!(
6174 with.h()[a][b].to_bits(),
6175 explicit.h()[a][b].to_bits(),
6176 "h[{a}][{b}]"
6177 );
6178 }
6179 }
6180 }
6181 n
6182 }
6183 let mut st = 0x9e37_79b9_7f4a_7c15u64;
6184 let total = run::<2>(&mut st, 1100)
6185 + run::<3>(&mut st, 1100)
6186 + run::<4>(&mut st, 1100)
6187 + run::<9>(&mut st, 1100);
6188 assert_eq!(total, 4400);
6189 }
6190
6191 #[test]
6196 fn fused_one_seed_channels_match_unfused_definition_932() {
6197 const K: usize = 8;
6198
6199 fn random_scalar(state: &mut u64) -> f64 {
6200 *state ^= *state << 13;
6201 *state ^= *state >> 7;
6202 *state ^= *state << 17;
6203 ((*state >> 11) as f64 / ((1_u64 << 53) as f64)) * 2.0 - 1.0
6204 }
6205
6206 fn random_order2<const N: usize>(state: &mut u64) -> Order2<N> {
6207 let mut tower = crate::jet_tower::Tower2::<N>::zero();
6208 tower.v = random_scalar(state);
6209 for axis in 0..N {
6210 tower.g[axis] = random_scalar(state);
6211 }
6212 for row in 0..N {
6213 for column in row..N {
6214 let channel = random_scalar(state);
6215 tower.h[row][column] = channel;
6216 tower.h[column][row] = channel;
6217 }
6218 }
6219 Order2(tower)
6220 }
6221
6222 fn assert_channels_close<const N: usize>(
6223 label: &str,
6224 actual: &OneSeed<N>,
6225 expected: &OneSeed<N>,
6226 ) {
6227 for (part_label, actual_part, expected_part, require_exact_symmetry) in [
6228 ("base", &actual.base.0, &expected.base.0, false),
6229 ("eps", &actual.eps.0, &expected.eps.0, true),
6230 ] {
6231 let check = |channel: &str, got: f64, want: f64| {
6232 let tolerance = 2.0e-14 * got.abs().max(want.abs()).max(1.0);
6233 assert!(
6234 (got - want).abs() <= tolerance,
6235 "{label} {part_label} {channel}: got={got:+.17e} want={want:+.17e}"
6236 );
6237 };
6238 check("value", actual_part.v, expected_part.v);
6239 for row in 0..N {
6240 check(
6241 &format!("gradient[{row}]"),
6242 actual_part.g[row],
6243 expected_part.g[row],
6244 );
6245 for column in 0..N {
6246 check(
6247 &format!("hessian[{row},{column}]"),
6248 actual_part.h[row][column],
6249 expected_part.h[row][column],
6250 );
6251 if require_exact_symmetry {
6252 assert_eq!(
6253 actual_part.h[row][column].to_bits(),
6254 actual_part.h[column][row].to_bits(),
6255 "{label} {part_label} Hessian symmetry at [{row},{column}]"
6256 );
6257 }
6258 }
6259 }
6260 }
6261 }
6262
6263 let mut state = 0x9320_1eed_5eed_cafe_u64;
6264 for sample in 0..256 {
6265 let left = OneSeed {
6266 base: random_order2::<K>(&mut state),
6267 eps: random_order2::<K>(&mut state),
6268 };
6269 let right = OneSeed {
6270 base: random_order2::<K>(&mut state),
6271 eps: random_order2::<K>(&mut state),
6272 };
6273
6274 let fused_product = left.mul(&right);
6275 let unfused_product = OneSeed {
6276 base: left.base.mul(&right.base),
6277 eps: left.base.mul(&right.eps).add(&left.eps.mul(&right.base)),
6278 };
6279 assert_channels_close(
6280 &format!("sample {sample} product"),
6281 &fused_product,
6282 &unfused_product,
6283 );
6284
6285 let derivatives: [f64; 5] = std::array::from_fn(|_| random_scalar(&mut state));
6286 let fused_composition = left.compose_unary(derivatives);
6287 let unfused_composition = OneSeed {
6288 base: left.base.compose_unary(derivatives),
6289 eps: left
6290 .base
6291 .compose_unary([
6292 derivatives[1],
6293 derivatives[2],
6294 derivatives[3],
6295 derivatives[4],
6296 derivatives[4],
6297 ])
6298 .mul(&left.eps),
6299 };
6300 assert_channels_close(
6301 &format!("sample {sample} composition"),
6302 &fused_composition,
6303 &unfused_composition,
6304 );
6305 }
6306 }
6307
6308 #[test]
6315 fn tower4_as_jetscalar_matches_program_tower_all_channels() {
6316 let t = tower();
6317 let vars: [Tower4<2>; 2] = std::array::from_fn(|a| Tower4::variable(SEED[a], a));
6318 let s = row_expr(&vars);
6319 close(s.v, t.v, "tower-jetscalar value");
6320 for a in 0..2 {
6321 close(s.g[a], t.g[a], &format!("tower-jetscalar grad[{a}]"));
6322 for b in 0..2 {
6323 close(
6324 s.h[a][b],
6325 t.h[a][b],
6326 &format!("tower-jetscalar hess[{a}][{b}]"),
6327 );
6328 for c in 0..2 {
6329 close(
6330 s.t3[a][b][c],
6331 t.t3[a][b][c],
6332 &format!("tower-jetscalar t3[{a}][{b}][{c}]"),
6333 );
6334 for d in 0..2 {
6335 close(
6336 s.t4[a][b][c][d],
6337 t.t4[a][b][c][d],
6338 &format!("tower-jetscalar t4[{a}][{b}][{c}][{d}]"),
6339 );
6340 }
6341 }
6342 }
6343 }
6344 }
6345
6346 #[test]
6350 fn runtime_directional_jets_match_fixed_packed_algebra_932() {
6351 fn expression<'arena, S: RuntimeJetScalar<'arena>>(vars: &[S]) -> S {
6352 let bilinear = vars[0].mul(&vars[1]);
6353 let curved = vars[2].scale(0.7).add(&vars[3].mul(&vars[3]).scale(-0.2));
6354 bilinear
6355 .add(&curved)
6356 .exp()
6357 .mul(&vars[4].compose_unary([0.4, -0.3, 0.2, -0.1, 0.05]))
6358 }
6359
6360 const K: usize = 5;
6361 let values = [0.2, -0.7, 0.4, 1.1, -0.3];
6362 let direction_u = [0.5, -0.2, 0.7, -0.4, 0.1];
6363 let direction_v = [-0.3, 0.8, 0.2, 0.6, -0.5];
6364 let close = |actual: f64, expected: f64| {
6365 let tolerance = 1.0e-13 * (1.0 + actual.abs().max(expected.abs()));
6366 assert!((actual - expected).abs() <= tolerance);
6367 };
6368
6369 let fixed_one: Vec<FixedRuntimeJet<OneSeed<K>, K>> = (0..K)
6370 .map(|axis| FixedRuntimeJet {
6371 inner: OneSeed::seed_direction(values[axis], axis, direction_u[axis]),
6372 })
6373 .collect();
6374 let arena_one = DynamicJetArena::new();
6375 let dynamic_one: Vec<DynamicOneSeed<'_>> = (0..K)
6376 .map(|axis| {
6377 DynamicOneSeed::seed_direction(values[axis], axis, direction_u[axis], K, &arena_one)
6378 })
6379 .collect();
6380 let fixed_third = expression(&fixed_one).into_inner().contracted_third();
6381 let dynamic_third = expression(&dynamic_one);
6382 for a in 0..K {
6383 for b in 0..K {
6384 assert_eq!(
6385 dynamic_third.contracted_third()[a * K + b].to_bits(),
6386 dynamic_third.contracted_third()[b * K + a].to_bits(),
6387 "arena third Hessian must be exactly symmetric at ({a},{b})"
6388 );
6389 close(
6390 dynamic_third.contracted_third()[a * K + b],
6391 fixed_third[a][b],
6392 );
6393 }
6394 }
6395
6396 let fixed_one_v: Vec<FixedRuntimeJet<OneSeed<K>, K>> = (0..K)
6397 .map(|axis| FixedRuntimeJet {
6398 inner: OneSeed::seed_direction(values[axis], axis, direction_v[axis]),
6399 })
6400 .collect();
6401 let fixed_third_v = expression(&fixed_one_v).into_inner().contracted_third();
6402 let batch_workspace = DynamicJetBatchWorkspace::new(2);
6403 let directions = [direction_u, direction_v];
6404 let batch_vars = batch_workspace.alloc_slice_fill_with(K, |axis| {
6405 DynamicOneSeedBatch::seed_directions(values[axis], axis, K, &batch_workspace, |lane| {
6406 directions[lane][axis]
6407 })
6408 });
6409 let dynamic_batch = expression(batch_vars);
6410 assert_eq!(dynamic_batch.lanes(), 2);
6411 for lane in 0..2 {
6412 let expected = if lane == 0 {
6413 &fixed_third
6414 } else {
6415 &fixed_third_v
6416 };
6417 for a in 0..K {
6418 for b in 0..K {
6419 close(
6420 dynamic_batch.contracted_third(lane)[a * K + b],
6421 expected[a][b],
6422 );
6423 }
6424 }
6425 }
6426
6427 let fixed_two: Vec<FixedRuntimeJet<TwoSeed<K>, K>> = (0..K)
6428 .map(|axis| FixedRuntimeJet {
6429 inner: TwoSeed::seed(values[axis], axis, direction_u[axis], direction_v[axis]),
6430 })
6431 .collect();
6432 let arena_two = DynamicJetArena::new();
6433 let dynamic_two: Vec<DynamicTwoSeed<'_>> = (0..K)
6434 .map(|axis| {
6435 DynamicTwoSeed::seed(
6436 values[axis],
6437 axis,
6438 direction_u[axis],
6439 direction_v[axis],
6440 K,
6441 &arena_two,
6442 )
6443 })
6444 .collect();
6445 let fixed_fourth = expression(&fixed_two).into_inner().contracted_fourth();
6446 let dynamic_fourth = expression(&dynamic_two);
6447 for a in 0..K {
6448 for b in 0..K {
6449 close(
6450 dynamic_fourth.contracted_fourth()[a * K + b],
6451 fixed_fourth[a][b],
6452 );
6453 }
6454 }
6455
6456 let fixed_two_swapped: Vec<FixedRuntimeJet<TwoSeed<K>, K>> = (0..K)
6457 .map(|axis| {
6458 FixedRuntimeJet::from_inner(TwoSeed::seed(
6459 values[axis],
6460 axis,
6461 direction_v[axis],
6462 direction_u[axis],
6463 ))
6464 })
6465 .collect();
6466 let fixed_fourth_swapped = expression(&fixed_two_swapped)
6467 .into_inner()
6468 .contracted_fourth();
6469 let pair_workspace = DynamicJetBatchWorkspace::new(2);
6470 let direction_pairs = [(direction_u, direction_v), (direction_v, direction_u)];
6471 let pair_vars = pair_workspace.alloc_slice_fill_with(K, |axis| {
6472 DynamicTwoSeedBatch::seed_direction_pairs(
6473 values[axis],
6474 axis,
6475 K,
6476 &pair_workspace,
6477 |lane| (direction_pairs[lane].0[axis], direction_pairs[lane].1[axis]),
6478 )
6479 });
6480 let dynamic_pair_batch = expression(pair_vars);
6481 assert_eq!(dynamic_pair_batch.lanes(), 2);
6482 for lane in 0..2 {
6483 let expected = if lane == 0 {
6484 &fixed_fourth
6485 } else {
6486 &fixed_fourth_swapped
6487 };
6488 for a in 0..K {
6489 for b in 0..K {
6490 close(
6491 dynamic_pair_batch.contracted_fourth(lane)[a * K + b],
6492 expected[a][b],
6493 );
6494 }
6495 }
6496 }
6497 }
6498
6499 #[test]
6500 fn dynamic_jet_arena_compacts_fragmented_high_water_932() {
6501 const WORDS_PER_ALLOCATION: usize = 1 << 17;
6502 const ALLOCATIONS: usize = 6;
6503
6504 let mut arena = DynamicJetArena::new();
6505 for lane in 0..ALLOCATIONS {
6506 let allocation = arena.alloc_slice_fill_with(WORDS_PER_ALLOCATION, |_| lane as u64);
6507 std::hint::black_box(allocation);
6508 }
6509 let fragmented_high_water = arena.allocated_bytes();
6510
6511 arena.reset();
6512 let compact_high_water = arena.allocated_bytes();
6513 assert!(
6514 compact_high_water >= fragmented_high_water,
6515 "compacted arena must retain the complete fragmented tape"
6516 );
6517
6518 for lane in 0..ALLOCATIONS {
6519 let allocation = arena.alloc_slice_fill_with(WORDS_PER_ALLOCATION, |_| lane as u64);
6520 std::hint::black_box(allocation);
6521 }
6522 assert_eq!(
6523 arena.allocated_bytes(),
6524 compact_high_water,
6525 "equal replay must fit in the compacted chunk"
6526 );
6527
6528 arena.reset();
6529 assert_eq!(
6530 arena.allocated_bytes(),
6531 compact_high_water,
6532 "stable reset must retain the compacted chunk"
6533 );
6534 }
6535}
6536
6537#[cfg(test)]
6538mod batch_tests {
6539 use super::{
6547 JetScalar, Lane, OneSeed, OneSeedBatch, OneSeedLane, Order2, Order2Batch, Order2Lane,
6548 TwoSeed, TwoSeedBatch, TwoSeedLane,
6549 };
6550 use crate::nested_dual::JetField;
6553
6554 trait RowAlg<const K: usize>: Copy {
6558 fn constant(c: f64) -> Self;
6559 fn add(&self, o: &Self) -> Self;
6560 fn sub(&self, o: &Self) -> Self;
6561 fn mul(&self, o: &Self) -> Self;
6562 fn scale(&self, s: f64) -> Self;
6563 fn exp(&self) -> Self;
6564 fn sqrt(&self) -> Self;
6565 fn recip(&self) -> Self;
6566 }
6567
6568 impl<const K: usize> RowAlg<K> for Order2<K> {
6569 fn constant(c: f64) -> Self {
6570 <Self as JetScalar<K>>::constant(c)
6571 }
6572 fn add(&self, o: &Self) -> Self {
6573 crate::nested_dual::JetField::add(self, o)
6574 }
6575 fn sub(&self, o: &Self) -> Self {
6576 crate::nested_dual::JetField::sub(self, o)
6577 }
6578 fn mul(&self, o: &Self) -> Self {
6579 crate::nested_dual::JetField::mul(self, o)
6580 }
6581 fn scale(&self, s: f64) -> Self {
6582 crate::nested_dual::JetField::scale(self, s)
6583 }
6584 fn exp(&self) -> Self {
6585 JetScalar::exp(self)
6586 }
6587 fn sqrt(&self) -> Self {
6588 JetScalar::sqrt(self)
6589 }
6590 fn recip(&self) -> Self {
6591 JetScalar::recip(self)
6592 }
6593 }
6594
6595 impl<L: Lane, const K: usize> RowAlg<K> for Order2Lane<L, K> {
6596 fn constant(c: f64) -> Self {
6597 Order2Lane::constant(L::splat(c))
6598 }
6599 fn add(&self, o: &Self) -> Self {
6600 Order2Lane::add(self, o)
6601 }
6602 fn sub(&self, o: &Self) -> Self {
6603 Order2Lane::sub(self, o)
6604 }
6605 fn mul(&self, o: &Self) -> Self {
6606 Order2Lane::mul(self, o)
6607 }
6608 fn scale(&self, s: f64) -> Self {
6609 Order2Lane::scale(self, s)
6610 }
6611 fn exp(&self) -> Self {
6612 Order2Lane::exp(self)
6613 }
6614 fn sqrt(&self) -> Self {
6615 Order2Lane::sqrt(self)
6616 }
6617 fn recip(&self) -> Self {
6618 Order2Lane::recip(self)
6619 }
6620 }
6621
6622 fn row_expr<const K: usize, A: RowAlg<K>>(p: &[A; K]) -> A {
6627 let mut s = A::constant(0.3);
6628 for a in 0..K {
6629 let b = (a + 1) % K;
6630 s = s.add(&p[a].mul(&p[b]).scale(0.1 + 0.05 * a as f64));
6631 }
6632 let e = s.exp();
6633 let r = s.mul(&s).add(&A::constant(1.0)).sqrt();
6634 let denom = e.add(&A::constant(2.0));
6635 e.mul(&r).sub(&s.scale(0.5)).mul(&denom.recip())
6636 }
6637
6638 fn rand_unit(state: &mut u64) -> f64 {
6640 let mut x = *state;
6641 x ^= x << 13;
6642 x ^= x >> 7;
6643 x ^= x << 17;
6644 *state = x;
6645 let u = (x >> 11) as f64 / ((1u64 << 53) as f64); 2.0 * u - 1.0
6647 }
6648
6649 fn check_k<const K: usize>(state: &mut u64, batches: usize) -> usize {
6652 let mut verified_rows = 0usize;
6653 for _ in 0..batches {
6654 let rows: [[f64; K]; 4] =
6656 std::array::from_fn(|_| std::array::from_fn(|_| rand_unit(state)));
6657
6658 let prod: [Order2<K>; 4] = std::array::from_fn(|r| {
6660 let p: [Order2<K>; K] = std::array::from_fn(|a| Order2::variable(rows[r][a], a));
6661 row_expr(&p)
6662 });
6663
6664 let scal: [Order2Lane<f64, K>; 4] = std::array::from_fn(|r| {
6666 let p: [Order2Lane<f64, K>; K] =
6667 std::array::from_fn(|a| Order2Lane::variable(rows[r][a], a));
6668 row_expr(&p)
6669 });
6670
6671 let pbatch: [Order2Batch<K>; K] = std::array::from_fn(|a| {
6673 let packed = wide::f64x4::new([rows[0][a], rows[1][a], rows[2][a], rows[3][a]]);
6674 Order2Batch::variable(packed, a)
6675 });
6676 let batch = row_expr(&pbatch);
6677
6678 for r in 0..4 {
6679 let g = prod[r].0;
6680 assert_eq!(scal[r].v.to_bits(), g.v.to_bits(), "K={K} scalar v");
6682 let lr = batch.lane(r).0;
6684 assert_eq!(lr.v.to_bits(), g.v.to_bits(), "K={K} batch lane {r} v");
6685 for a in 0..K {
6686 assert_eq!(
6687 scal[r].g[a].to_bits(),
6688 g.g[a].to_bits(),
6689 "K={K} scalar g[{a}]"
6690 );
6691 assert_eq!(
6692 lr.g[a].to_bits(),
6693 g.g[a].to_bits(),
6694 "K={K} batch lane {r} g[{a}]"
6695 );
6696 for b in 0..K {
6697 assert_eq!(
6698 scal[r].h[a][b].to_bits(),
6699 g.h[a][b].to_bits(),
6700 "K={K} scalar h[{a}][{b}]"
6701 );
6702 assert_eq!(
6703 lr.h[a][b].to_bits(),
6704 g.h[a][b].to_bits(),
6705 "K={K} batch lane {r} h[{a}][{b}]"
6706 );
6707 }
6708 }
6709 verified_rows += 1;
6710 }
6711 }
6712 verified_rows
6713 }
6714
6715 #[test]
6718 fn batch_lanes_bit_identical_to_scalar_per_row() {
6719 let mut state = 0x9E37_79B9_7F4A_7C15_u64;
6720 let mut verified = 0usize;
6721 verified += check_k::<2>(&mut state, 2000);
6722 verified += check_k::<3>(&mut state, 2000);
6723 verified += check_k::<4>(&mut state, 2000);
6724 verified += check_k::<9>(&mut state, 2000);
6725 assert_eq!(verified, 4 * 2000 * 4, "every batch row must be verified");
6727 }
6728
6729 impl<const K: usize> RowAlg<K> for OneSeed<K> {
6738 fn constant(c: f64) -> Self {
6739 <Self as JetScalar<K>>::constant(c)
6740 }
6741 fn add(&self, o: &Self) -> Self {
6742 crate::nested_dual::JetField::add(self, o)
6743 }
6744 fn sub(&self, o: &Self) -> Self {
6745 crate::nested_dual::JetField::sub(self, o)
6746 }
6747 fn mul(&self, o: &Self) -> Self {
6748 crate::nested_dual::JetField::mul(self, o)
6749 }
6750 fn scale(&self, s: f64) -> Self {
6751 crate::nested_dual::JetField::scale(self, s)
6752 }
6753 fn exp(&self) -> Self {
6754 JetScalar::exp(self)
6755 }
6756 fn sqrt(&self) -> Self {
6757 JetScalar::sqrt(self)
6758 }
6759 fn recip(&self) -> Self {
6760 JetScalar::recip(self)
6761 }
6762 }
6763
6764 impl<L: Lane, const K: usize> RowAlg<K> for OneSeedLane<L, K> {
6765 fn constant(c: f64) -> Self {
6766 OneSeedLane::constant(L::splat(c))
6767 }
6768 fn add(&self, o: &Self) -> Self {
6769 OneSeedLane::add(self, o)
6770 }
6771 fn sub(&self, o: &Self) -> Self {
6772 OneSeedLane::sub(self, o)
6773 }
6774 fn mul(&self, o: &Self) -> Self {
6775 OneSeedLane::mul(self, o)
6776 }
6777 fn scale(&self, s: f64) -> Self {
6778 OneSeedLane::scale(self, s)
6779 }
6780 fn exp(&self) -> Self {
6781 OneSeedLane::exp(self)
6782 }
6783 fn sqrt(&self) -> Self {
6784 OneSeedLane::sqrt(self)
6785 }
6786 fn recip(&self) -> Self {
6787 OneSeedLane::recip(self)
6788 }
6789 }
6790
6791 impl<const K: usize> RowAlg<K> for TwoSeed<K> {
6792 fn constant(c: f64) -> Self {
6793 <Self as JetScalar<K>>::constant(c)
6794 }
6795 fn add(&self, o: &Self) -> Self {
6796 crate::nested_dual::JetField::add(self, o)
6797 }
6798 fn sub(&self, o: &Self) -> Self {
6799 crate::nested_dual::JetField::sub(self, o)
6800 }
6801 fn mul(&self, o: &Self) -> Self {
6802 crate::nested_dual::JetField::mul(self, o)
6803 }
6804 fn scale(&self, s: f64) -> Self {
6805 crate::nested_dual::JetField::scale(self, s)
6806 }
6807 fn exp(&self) -> Self {
6808 JetScalar::exp(self)
6809 }
6810 fn sqrt(&self) -> Self {
6811 JetScalar::sqrt(self)
6812 }
6813 fn recip(&self) -> Self {
6814 JetScalar::recip(self)
6815 }
6816 }
6817
6818 impl<L: Lane, const K: usize> RowAlg<K> for TwoSeedLane<L, K> {
6819 fn constant(c: f64) -> Self {
6820 TwoSeedLane::constant(L::splat(c))
6821 }
6822 fn add(&self, o: &Self) -> Self {
6823 TwoSeedLane::add(self, o)
6824 }
6825 fn sub(&self, o: &Self) -> Self {
6826 TwoSeedLane::sub(self, o)
6827 }
6828 fn mul(&self, o: &Self) -> Self {
6829 TwoSeedLane::mul(self, o)
6830 }
6831 fn scale(&self, s: f64) -> Self {
6832 TwoSeedLane::scale(self, s)
6833 }
6834 fn exp(&self) -> Self {
6835 TwoSeedLane::exp(self)
6836 }
6837 fn sqrt(&self) -> Self {
6838 TwoSeedLane::sqrt(self)
6839 }
6840 fn recip(&self) -> Self {
6841 TwoSeedLane::recip(self)
6842 }
6843 }
6844
6845 fn check_oneseed<const K: usize>(state: &mut u64, batches: usize) -> usize {
6846 let mut rows_checked = 0;
6847 for _ in 0..batches {
6848 let rows: [[f64; K]; 4] =
6849 std::array::from_fn(|_| std::array::from_fn(|_| rand_unit(state)));
6850 let u: [[f64; K]; 4] =
6852 std::array::from_fn(|_| std::array::from_fn(|_| rand_unit(state)));
6853
6854 let prod: [OneSeed<K>; 4] = std::array::from_fn(|r| {
6856 let p: [OneSeed<K>; K] =
6857 std::array::from_fn(|a| OneSeed::seed_direction(rows[r][a], a, u[r][a]));
6858 row_expr(&p)
6859 });
6860
6861 let scal: [OneSeedLane<f64, K>; 4] = std::array::from_fn(|r| {
6863 let p: [OneSeedLane<f64, K>; K] =
6864 std::array::from_fn(|a| OneSeedLane::seed_direction(rows[r][a], a, u[r][a]));
6865 row_expr(&p)
6866 });
6867
6868 let pbatch: [OneSeedBatch<K>; K] = std::array::from_fn(|a| {
6870 let val = wide::f64x4::new([rows[0][a], rows[1][a], rows[2][a], rows[3][a]]);
6871 let uu = wide::f64x4::new([u[0][a], u[1][a], u[2][a], u[3][a]]);
6872 OneSeedBatch::seed_direction(val, a, uu)
6873 });
6874 let batch = row_expr(&pbatch);
6875
6876 for r in 0..4 {
6877 let want = prod[r].contracted_third();
6878 let got_scal = scal[r].contracted_third();
6879 let got_batch = batch.lane(r).contracted_third();
6880 assert_eq!(
6882 scal[r].base.v.to_bits(),
6883 prod[r].base.value().to_bits(),
6884 "OneSeed K={K} scalar value"
6885 );
6886 assert_eq!(
6887 batch.lane(r).base.value().to_bits(),
6888 prod[r].base.value().to_bits(),
6889 "OneSeed K={K} batch lane {r} value"
6890 );
6891 for a in 0..K {
6892 for b in 0..K {
6893 assert_eq!(
6894 got_scal[a][b].to_bits(),
6895 want[a][b].to_bits(),
6896 "OneSeed K={K} scalar third[{a}][{b}]"
6897 );
6898 assert_eq!(
6899 got_batch[a][b].to_bits(),
6900 want[a][b].to_bits(),
6901 "OneSeed K={K} batch lane {r} third[{a}][{b}]"
6902 );
6903 }
6904 }
6905 rows_checked += 1;
6906 }
6907 }
6908 rows_checked
6909 }
6910
6911 fn check_twoseed<const K: usize>(state: &mut u64, batches: usize) -> usize {
6912 let mut rows_checked = 0;
6913 for _ in 0..batches {
6914 let rows: [[f64; K]; 4] =
6915 std::array::from_fn(|_| std::array::from_fn(|_| rand_unit(state)));
6916 let u: [[f64; K]; 4] =
6917 std::array::from_fn(|_| std::array::from_fn(|_| rand_unit(state)));
6918 let v: [[f64; K]; 4] =
6919 std::array::from_fn(|_| std::array::from_fn(|_| rand_unit(state)));
6920
6921 let prod: [TwoSeed<K>; 4] = std::array::from_fn(|r| {
6922 let p: [TwoSeed<K>; K] =
6923 std::array::from_fn(|a| TwoSeed::seed(rows[r][a], a, u[r][a], v[r][a]));
6924 row_expr(&p)
6925 });
6926
6927 let scal: [TwoSeedLane<f64, K>; 4] = std::array::from_fn(|r| {
6928 let p: [TwoSeedLane<f64, K>; K] =
6929 std::array::from_fn(|a| TwoSeedLane::seed(rows[r][a], a, u[r][a], v[r][a]));
6930 row_expr(&p)
6931 });
6932
6933 let pbatch: [TwoSeedBatch<K>; K] = std::array::from_fn(|a| {
6934 let val = wide::f64x4::new([rows[0][a], rows[1][a], rows[2][a], rows[3][a]]);
6935 let uu = wide::f64x4::new([u[0][a], u[1][a], u[2][a], u[3][a]]);
6936 let vv = wide::f64x4::new([v[0][a], v[1][a], v[2][a], v[3][a]]);
6937 TwoSeedBatch::seed(val, a, uu, vv)
6938 });
6939 let batch = row_expr(&pbatch);
6940
6941 for r in 0..4 {
6942 let want = prod[r].contracted_fourth();
6943 let got_scal = scal[r].contracted_fourth();
6944 let got_batch = batch.lane(r).contracted_fourth();
6945 assert_eq!(
6946 scal[r].base.v.to_bits(),
6947 prod[r].base.value().to_bits(),
6948 "TwoSeed K={K} scalar value"
6949 );
6950 assert_eq!(
6951 batch.lane(r).base.value().to_bits(),
6952 prod[r].base.value().to_bits(),
6953 "TwoSeed K={K} batch lane {r} value"
6954 );
6955 for a in 0..K {
6956 for b in 0..K {
6957 assert_eq!(
6958 got_scal[a][b].to_bits(),
6959 want[a][b].to_bits(),
6960 "TwoSeed K={K} scalar fourth[{a}][{b}]"
6961 );
6962 assert_eq!(
6963 got_batch[a][b].to_bits(),
6964 want[a][b].to_bits(),
6965 "TwoSeed K={K} batch lane {r} fourth[{a}][{b}]"
6966 );
6967 }
6968 }
6969 rows_checked += 1;
6970 }
6971 }
6972 rows_checked
6973 }
6974
6975 #[test]
6979 fn oneseed_lanes_contracted_third_bit_identical() {
6980 let mut state = 0x1234_5678_9ABC_DEF0_u64;
6981 let batches = 2000;
6982 let rows_checked = check_oneseed::<2>(&mut state, batches)
6983 + check_oneseed::<3>(&mut state, batches)
6984 + check_oneseed::<4>(&mut state, batches)
6985 + check_oneseed::<9>(&mut state, batches);
6986 assert_eq!(rows_checked, 4 * batches * 4);
6989 }
6990
6991 #[test]
6995 fn twoseed_lanes_contracted_fourth_bit_identical() {
6996 let mut state = 0x0FED_CBA9_8765_4321_u64;
6997 let batches = 2000;
6998 let rows_checked = check_twoseed::<2>(&mut state, batches)
6999 + check_twoseed::<3>(&mut state, batches)
7000 + check_twoseed::<4>(&mut state, batches)
7001 + check_twoseed::<9>(&mut state, batches);
7002 assert_eq!(rows_checked, 4 * batches * 4);
7005 }
7006}
7007
7008#[cfg(test)]
7009mod unit_tests {
7010 use super::{JetScalar, OneSeed, Order1, Order2, filtered_implicit_solve_scalar};
7011 use crate::nested_dual::{Dual2, JetField};
7012
7013 fn family_program<const K: usize, S: JetScalar<K>>(x: &S, y: &S, theta: &S) -> S {
7017 let xy = x.mul(y);
7018 let exponential = theta.mul(&xy).exp();
7019 let theta_squared_x_squared = theta.mul(theta).mul(&x.mul(x)).scale(0.375);
7020 let theta_y_cubed = theta.mul(&y.mul(y).mul(y)).scale(-0.2);
7021 exponential
7022 .add(&theta_squared_x_squared)
7023 .add(&theta_y_cubed)
7024 }
7025
7026 fn analytic_family_first<const K: usize, S: JetScalar<K>>(x: &S, y: &S, theta: &S) -> S {
7027 let xy = x.mul(y);
7028 let exponential = theta.mul(&xy).exp();
7029 xy.mul(&exponential)
7030 .add(&theta.mul(&x.mul(x)).scale(0.75))
7031 .add(&y.mul(y).mul(y).scale(-0.2))
7032 }
7033
7034 fn analytic_family_second<const K: usize, S: JetScalar<K>>(x: &S, y: &S, theta: &S) -> S {
7035 let xy = x.mul(y);
7036 let exponential = theta.mul(&xy).exp();
7037 xy.mul(&xy).mul(&exponential).add(&x.mul(x).scale(0.75))
7038 }
7039
7040 fn assert_channel_close(actual: f64, expected: f64, channel: &str) {
7041 let tolerance = 256.0 * f64::EPSILON * (1.0 + actual.abs().max(expected.abs()));
7042 assert!(
7043 (actual - expected).abs() <= tolerance,
7044 "{channel}: actual={actual:.17e}, expected={expected:.17e}, tolerance={tolerance:.3e}"
7045 );
7046 }
7047
7048 fn assert_order2_channels<const K: usize>(
7049 actual: &Order2<K>,
7050 expected: &Order2<K>,
7051 prefix: &str,
7052 ) {
7053 assert_channel_close(actual.value(), expected.value(), &format!("{prefix}.value"));
7054 for a in 0..K {
7055 assert_channel_close(actual.g()[a], expected.g()[a], &format!("{prefix}.g[{a}]"));
7056 for b in 0..K {
7057 assert_channel_close(
7058 actual.h()[a][b],
7059 expected.h()[a][b],
7060 &format!("{prefix}.h[{a}][{b}]"),
7061 );
7062 }
7063 }
7064 }
7065
7066 #[test]
7069 fn dual2_order2_extracts_exact_family_value_gradient_hessian_channels() {
7070 const K: usize = 2;
7071 let x0 = 0.7;
7072 let y0 = -0.45;
7073 let theta0 = 0.6;
7074 let x = <Dual2<Order2<K>> as JetScalar<K>>::variable(x0, 0);
7075 let y = <Dual2<Order2<K>> as JetScalar<K>>::variable(y0, 1);
7076 let theta = Dual2 {
7077 v: Order2::constant(theta0),
7078 g: Order2::constant(1.0),
7079 h: Order2::constant(0.0),
7080 };
7081
7082 let actual = family_program(&x, &y, &theta);
7083 let reference_x = Order2::variable(x0, 0);
7084 let reference_y = Order2::variable(y0, 1);
7085 let reference_theta = Order2::constant(theta0);
7086 let expected_first = analytic_family_first(&reference_x, &reference_y, &reference_theta);
7087 let expected_second = analytic_family_second(&reference_x, &reference_y, &reference_theta);
7088
7089 assert_order2_channels(&actual.g, &expected_first, "family_first");
7090 assert_order2_channels(&actual.h, &expected_second, "family_second");
7091 }
7092
7093 #[test]
7096 fn dual2_oneseed_extracts_exact_family_hessian_drift() {
7097 const K: usize = 2;
7098 let x0 = 0.7;
7099 let y0 = -0.45;
7100 let theta0 = 0.6;
7101 let direction = [0.3, -0.8];
7102
7103 let mut x = <Dual2<OneSeed<K>> as JetScalar<K>>::variable(x0, 0);
7104 let mut y = <Dual2<OneSeed<K>> as JetScalar<K>>::variable(y0, 1);
7105 x.v.eps = Order2::constant(direction[0]);
7106 y.v.eps = Order2::constant(direction[1]);
7107 let theta = Dual2 {
7108 v: OneSeed::constant(theta0),
7109 g: OneSeed::constant(1.0),
7110 h: OneSeed::constant(0.0),
7111 };
7112
7113 let actual = family_program(&x, &y, &theta);
7114 let reference_x = OneSeed::seed_direction(x0, 0, direction[0]);
7115 let reference_y = OneSeed::seed_direction(y0, 1, direction[1]);
7116 let reference_theta = OneSeed::constant(theta0);
7117 let expected = analytic_family_first(&reference_x, &reference_y, &reference_theta);
7118
7119 assert_order2_channels(&actual.g.eps, &expected.eps, "family_first_drift");
7120 }
7121
7122 #[test]
7126 fn order2_constant_has_zero_derivatives() {
7127 let s = Order2::<3>::constant(7.5);
7128 assert_eq!(s.value(), 7.5);
7129 for a in 0..3 {
7130 assert_eq!(s.g()[a], 0.0, "grad[{a}] should be zero");
7131 for b in 0..3 {
7132 assert_eq!(s.h()[a][b], 0.0, "hess[{a}][{b}] should be zero");
7133 }
7134 }
7135 }
7136
7137 #[test]
7139 fn order2_variable_has_unit_gradient_in_seeded_slot() {
7140 let x = -2.5_f64;
7141 let s = Order2::<4>::variable(x, 2);
7142 assert_eq!(s.value(), x);
7143 for a in 0..4 {
7144 let expected_g = if a == 2 { 1.0 } else { 0.0 };
7145 assert_eq!(s.g()[a], expected_g, "grad[{a}]");
7146 for b in 0..4 {
7147 assert_eq!(s.h()[a][b], 0.0, "hess[{a}][{b}] should be zero");
7148 }
7149 }
7150 }
7151
7152 #[test]
7155 fn order2_add_sub_roundtrip() {
7156 let p = Order2::<2>::variable(3.0, 0);
7157 let q = Order2::<2>::variable(2.0, 1);
7158 let pq = crate::nested_dual::JetField::add(&p, &q);
7159 assert_eq!(pq.value(), 5.0, "add value");
7161 let back = crate::nested_dual::JetField::sub(&pq, &q);
7162 for a in 0..2 {
7164 assert_eq!(back.g()[a], p.g()[a], "grad[{a}] roundtrip");
7165 }
7166 }
7167
7168 #[test]
7171 fn order2_mul_satisfies_leibniz_rule() {
7172 let pv = 3.0_f64;
7173 let qv = -2.0_f64;
7174 let p = Order2::<2>::variable(pv, 0);
7175 let q = Order2::<2>::variable(qv, 1);
7176 let pq = crate::nested_dual::JetField::mul(&p, &q);
7177 assert_eq!(pq.value(), pv * qv, "value = p·q");
7178 assert_eq!(pq.g()[0], qv, "∂(p·q)/∂p = q");
7179 assert_eq!(pq.g()[1], pv, "∂(p·q)/∂q = p");
7180 assert_eq!(pq.h()[0][1], 1.0, "∂²(p·q)/∂p∂q = 1");
7181 assert_eq!(pq.h()[1][0], 1.0, "∂²(p·q)/∂q∂p = 1 (symmetric)");
7182 assert_eq!(pq.h()[0][0], 0.0, "∂²(p·q)/∂p² = 0");
7183 assert_eq!(pq.h()[1][1], 0.0, "∂²(p·q)/∂q² = 0");
7184 }
7185
7186 #[test]
7188 fn order2_scale_multiplies_all_channels() {
7189 let p = Order2::<2>::variable(4.0, 0);
7190 let s = 2.5_f64;
7191 let ps = crate::nested_dual::JetField::scale(&p, s);
7192 assert_eq!(ps.value(), 4.0 * s);
7193 assert_eq!(ps.g()[0], 1.0 * s);
7194 assert_eq!(ps.g()[1], 0.0);
7195 }
7196
7197 #[test]
7200 fn order2_exp_derivative_stack_correct() {
7201 let p0 = 1.0_f64;
7202 let p = Order2::<1>::variable(p0, 0);
7203 let ep = JetScalar::exp(&p);
7204 let e = p0.exp();
7205 assert!((ep.value() - e).abs() < 1e-15, "exp value");
7206 assert!((ep.g()[0] - e).abs() < 1e-15, "d/dp exp(p) = exp(p)");
7207 assert!((ep.h()[0][0] - e).abs() < 1e-15, "d²/dp² exp(p) = exp(p)");
7208 }
7209
7210 #[test]
7212 fn order2_ln_derivative_stack_correct() {
7213 let p0 = 2.0_f64;
7214 let p = Order2::<1>::variable(p0, 0);
7215 let lnp = JetScalar::ln(&p);
7216 assert!((lnp.value() - p0.ln()).abs() < 1e-15, "ln value");
7217 assert!((lnp.g()[0] - 1.0 / p0).abs() < 1e-15, "d/dp ln(p) = 1/p");
7218 assert!(
7219 (lnp.h()[0][0] - (-1.0 / (p0 * p0))).abs() < 1e-15,
7220 "d²/dp² ln(p) = -1/p²"
7221 );
7222 }
7223
7224 #[test]
7226 fn order2_exp_ln_roundtrip_at_value() {
7227 let p0 = 0.8_f64;
7228 let p = Order2::<1>::variable(p0, 0);
7229 let roundtrip = JetScalar::ln(&JetScalar::exp(&p));
7230 assert!((roundtrip.value() - p0).abs() < 1e-14, "ln(exp(p)) ≈ p");
7231 }
7232
7233 #[test]
7237 fn order1_constant_has_zero_gradient() {
7238 let s = Order1::<3>::constant(-5.0);
7239 assert_eq!(s.value(), -5.0);
7240 for a in 0..3 {
7241 assert_eq!(s.g()[a], 0.0, "g[{a}] should be zero");
7242 }
7243 }
7244
7245 #[test]
7247 fn order1_variable_has_unit_gradient_in_seeded_slot() {
7248 let s = Order1::<3>::variable(2.0, 1);
7249 assert_eq!(s.value(), 2.0);
7250 assert_eq!(s.g()[0], 0.0);
7251 assert_eq!(s.g()[1], 1.0);
7252 assert_eq!(s.g()[2], 0.0);
7253 }
7254
7255 #[test]
7257 fn order1_mul_satisfies_product_rule() {
7258 let pv = 3.0_f64;
7259 let qv = -2.0_f64;
7260 let p = Order1::<2>::variable(pv, 0);
7261 let q = Order1::<2>::variable(qv, 1);
7262 let pq = crate::nested_dual::JetField::mul(&p, &q);
7263 assert_eq!(pq.value(), pv * qv);
7264 assert_eq!(pq.g()[0], qv, "∂(p·q)/∂p = q");
7265 assert_eq!(pq.g()[1], pv, "∂(p·q)/∂q = p");
7266 }
7267
7268 #[test]
7270 fn order1_exp_has_correct_value_and_gradient() {
7271 let p0 = 0.5_f64;
7272 let p = Order1::<2>::variable(p0, 0);
7273 let ep = JetScalar::exp(&p);
7274 let e = p0.exp();
7275 assert!((ep.value() - e).abs() < 1e-15, "exp value");
7276 assert!((ep.g()[0] - e).abs() < 1e-15, "d/dp exp(p)");
7277 assert_eq!(ep.g()[1], 0.0, "irrelevant gradient slot is zero");
7278 }
7279
7280 #[test]
7282 fn order1_and_order2_agree_on_value_and_gradient() {
7283 let p0 = 1.3_f64;
7284 let q0 = -0.7_f64;
7285 let p1 = Order1::<2>::variable(p0, 0);
7287 let q1 = Order1::<2>::variable(q0, 1);
7288 let expr1 = JetScalar::exp(&crate::nested_dual::JetField::add(
7289 &crate::nested_dual::JetField::mul(&p1, &q1),
7290 &p1,
7291 ));
7292
7293 let p2 = Order2::<2>::variable(p0, 0);
7294 let q2 = Order2::<2>::variable(q0, 1);
7295 let expr2 = JetScalar::exp(&crate::nested_dual::JetField::add(
7296 &crate::nested_dual::JetField::mul(&p2, &q2),
7297 &p2,
7298 ));
7299
7300 assert!(
7301 (expr1.value() - expr2.value()).abs() < 1e-14,
7302 "value mismatch"
7303 );
7304 for a in 0..2 {
7305 assert!(
7306 (expr1.g()[a] - expr2.g()[a]).abs() < 1e-14,
7307 "gradient[{a}] mismatch"
7308 );
7309 }
7310 }
7311
7312 #[test]
7317 fn filtered_implicit_solve_linear_constraint_gives_exact_jet() {
7318 let theta0 = 3.0_f64;
7319 let theta = Order2::<1>::variable(theta0, 0);
7320 let a = filtered_implicit_solve_scalar::<1, Order2<1>>(theta0, 1.0, 2, |a_jet| {
7322 crate::nested_dual::JetField::sub(a_jet, &theta)
7323 });
7324 assert!((a.value() - theta0).abs() < 1e-14, "value = theta0");
7325 assert!((a.g()[0] - 1.0).abs() < 1e-14, "gradient = 1");
7327 assert!(a.h()[0][0].abs() < 1e-14, "hessian = 0");
7329 }
7330
7331 #[test]
7334 fn filtered_implicit_solve_quadratic_constraint_matches_analytic_derivatives() {
7335 let theta0 = 4.0_f64;
7336 let a0 = theta0.sqrt();
7337 let inv_fa = 1.0 / (2.0 * a0);
7338 let theta = Order2::<1>::variable(theta0, 0);
7339 let a = filtered_implicit_solve_scalar::<1, Order2<1>>(a0, inv_fa, 2, |a_jet| {
7341 let aa = crate::nested_dual::JetField::mul(a_jet, a_jet);
7342 crate::nested_dual::JetField::sub(&aa, &theta)
7343 });
7344 let tol = 1e-12;
7345 assert!((a.value() - a0).abs() < tol, "value = sqrt(theta0)");
7346 let expected_g = 0.5 / a0;
7347 assert!(
7348 (a.g()[0] - expected_g).abs() < tol,
7349 "da/dtheta = 1/(2*sqrt)"
7350 );
7351 let expected_h = -0.25 / (theta0 * a0);
7352 assert!(
7353 (a.h()[0][0] - expected_h).abs() < tol,
7354 "d2a/dtheta2 = -1/(4*theta^1.5)"
7355 );
7356 }
7357
7358 #[test]
7366 fn runtime_shaped_value_primitives_are_exact_and_skip_constant_composition_932() {
7367 use super::{DynamicJetArena, DynamicOrder2, RuntimeJetScalar};
7368 use crate::paired_timing::{SpeedGate, batched, paired_interleaved};
7369
7370 const K: usize = 48;
7371 let arena = DynamicJetArena::new();
7372 let variable = DynamicOrder2::variable(0.75, 7, K, &arena);
7373
7374 let constant = variable.constant_like(1.25);
7375 assert_eq!(constant.value().to_bits(), 1.25_f64.to_bits());
7376 assert_eq!(constant.dimension(), K);
7377 assert!(constant.g().iter().all(|&channel| channel == 0.0));
7378 assert!(constant.h().iter().all(|&channel| channel == 0.0));
7379
7380 let replaced = variable.with_value(-2.5);
7381 assert_eq!(replaced.value().to_bits(), (-2.5_f64).to_bits());
7382 assert_eq!(replaced.g(), variable.g());
7383 assert_eq!(replaced.h(), variable.h());
7384
7385 if cfg!(debug_assertions) {
7392 return;
7393 }
7394 let mut gate = SpeedGate::open("RUNTIME-CONSTANT-932");
7395 let mut direct_arena = DynamicJetArena::new();
7396 let mut composed_arena = DynamicJetArena::new();
7397 let timing = paired_interleaved(
7398 15,
7399 1_000,
7400 0x9320_C057,
7401 batched(16, |nudge| {
7402 direct_arena.reset();
7403 let variable = DynamicOrder2::variable(0.75 + nudge, 7, K, &direct_arena);
7404 let constant = variable.constant_like(1.25);
7405 constant.value() + constant.g()[K - 1] + constant.h()[K * K - 1]
7406 }),
7407 batched(16, |nudge| {
7408 composed_arena.reset();
7409 let variable = DynamicOrder2::variable(0.75 + nudge, 7, K, &composed_arena);
7410 let constant = variable.compose_unary([1.25, 0.0, 0.0, 0.0, 0.0]);
7411 constant.value() + constant.g()[K - 1] + constant.h()[K * K - 1]
7412 }),
7413 );
7414 gate.faster(&format!("dimension={K}"), &timing, "direct", "composed");
7415 gate.finish();
7416 }
7417}
7418
7419#[cfg(test)]
7420mod weighted_compose_sum_979_tests {
7421 use super::{DynamicJetArena, DynamicOrder2, RuntimeJetScalar};
7434
7435 const DIMENSION: usize = 6;
7436 const TERMS: usize = 4;
7437
7438 fn stacks() -> [[f64; 5]; TERMS] {
7439 [
7440 [0.31, 0.62, -0.24, 0.11, -0.05],
7441 [-0.17, 0.45, 0.33, -0.28, 0.09],
7442 [0.52, -0.38, 0.19, 0.07, -0.13],
7443 [0.28, 0.71, -0.46, 0.22, 0.04],
7444 ]
7445 }
7446
7447 fn operands(arena: &DynamicJetArena) -> (Vec<DynamicOrder2<'_>>, DynamicOrder2<'_>, DynamicOrder2<'_>) {
7450 let vars: Vec<DynamicOrder2<'_>> = (0..DIMENSION)
7451 .map(|axis| {
7452 DynamicOrder2::variable(0.35 - 0.11 * (axis as f64), axis, DIMENSION, arena)
7453 })
7454 .collect();
7455 let right = vars[0]
7456 .mul(&vars[1])
7457 .add(&vars[2].compose_unary([0.9, -0.5, 0.27, -0.14, 0.06]));
7458 let lefts: Vec<DynamicOrder2<'_>> = (0..TERMS)
7459 .map(|term| {
7460 let scale = 0.4 + 0.23 * (term as f64);
7461 vars[term % DIMENSION]
7462 .mul(&vars[(term + 3) % DIMENSION])
7463 .scale(scale)
7464 .add(&vars[(term + 1) % DIMENSION])
7465 })
7466 .collect();
7467 let addend = vars[4].mul(&vars[5]).scale(-0.7);
7468 (lefts, right, addend)
7469 }
7470
7471 fn by_loop<'arena>(
7474 lefts: &[DynamicOrder2<'arena>],
7475 right: &DynamicOrder2<'arena>,
7476 derivative_stacks: &[[f64; 5]],
7477 addend: &DynamicOrder2<'arena>,
7478 ) -> DynamicOrder2<'arena> {
7479 let mut sum = *addend;
7480 for (left, stack) in lefts.iter().zip(derivative_stacks) {
7481 sum = left.multiply_add(&right.compose_unary(*stack), &sum);
7482 }
7483 sum
7484 }
7485
7486 fn close(label: &str, fused: f64, looped: f64) {
7487 let tolerance = 1.0e-13 * fused.abs().max(looped.abs()).max(1.0);
7488 assert!(
7489 (fused - looped).abs() <= tolerance,
7490 "{label}: fused {fused:+.17e} vs loop {looped:+.17e} (allowed {tolerance:.3e})"
7491 );
7492 }
7493
7494 #[test]
7495 fn the_fused_sum_matches_the_composition_and_product_loop() {
7496 let arena = DynamicJetArena::new();
7497 let (lefts, right, addend) = operands(&arena);
7498 let derivative_stacks = stacks();
7499
7500 let fused = DynamicOrder2::weighted_compose_sum(
7501 &lefts,
7502 &right,
7503 &derivative_stacks,
7504 &addend,
7505 );
7506 let looped = by_loop(&lefts, &right, &derivative_stacks, &addend);
7507
7508 close("value", fused.value(), looped.value());
7509 for axis in 0..DIMENSION {
7510 close(&format!("gradient[{axis}]"), fused.g()[axis], looped.g()[axis]);
7511 }
7512 for entry in 0..DIMENSION * DIMENSION {
7513 close(&format!("hessian[{entry}]"), fused.h()[entry], looped.h()[entry]);
7514 }
7515
7516 assert!(
7520 fused.value().abs() > 1.0e-6,
7521 "the fixture must produce a nonzero value"
7522 );
7523 assert!(
7524 fused.g().iter().all(|channel| channel.abs() > 1.0e-9),
7525 "every gradient channel must be live: {:?}",
7526 fused.g()
7527 );
7528 assert!(
7529 fused.h().iter().filter(|channel| channel.abs() > 1.0e-9).count()
7530 > DIMENSION * DIMENSION / 2,
7531 "most Hessian channels must be live: {:?}",
7532 fused.h()
7533 );
7534 }
7535
7536 #[test]
7540 fn the_fused_sum_handles_no_terms_and_one_term() {
7541 let arena = DynamicJetArena::new();
7542 let (lefts, right, addend) = operands(&arena);
7543 let derivative_stacks = stacks();
7544
7545 let empty = DynamicOrder2::weighted_compose_sum(&[], &right, &[], &addend);
7546 close("empty value", empty.value(), addend.value());
7547 for entry in 0..DIMENSION * DIMENSION {
7548 close(
7549 &format!("empty hessian[{entry}]"),
7550 empty.h()[entry],
7551 addend.h()[entry],
7552 );
7553 }
7554
7555 let single = DynamicOrder2::weighted_compose_sum(
7556 &lefts[..1],
7557 &right,
7558 &derivative_stacks[..1],
7559 &addend,
7560 );
7561 let single_loop = by_loop(&lefts[..1], &right, &derivative_stacks[..1], &addend);
7562 close("single value", single.value(), single_loop.value());
7563 for entry in 0..DIMENSION * DIMENSION {
7564 close(
7565 &format!("single hessian[{entry}]"),
7566 single.h()[entry],
7567 single_loop.h()[entry],
7568 );
7569 }
7570 }
7571}
7572
7573#[cfg(test)]
7574mod dynamic_batch_fused_979_tests {
7575 use super::{
7598 DynamicJetArena, DynamicJetBatchWorkspace, DynamicOneSeed, DynamicOneSeedBatch,
7599 RuntimeJetScalar,
7600 };
7601
7602 const DIMENSION: usize = 5;
7603 const LANES: usize = 3;
7604
7605 fn point(axis: usize) -> f64 {
7607 0.35 - 0.11 * (axis as f64)
7608 }
7609
7610 fn direction(lane: usize, axis: usize) -> f64 {
7611 0.23 * ((lane + 1) as f64) - 0.07 * ((axis + 2) as f64) * ((lane + 1) as f64)
7614 }
7615
7616 fn expression<'arena, S: RuntimeJetScalar<'arena>>(
7619 vars: &[S],
7620 workspace: &'arena S::Workspace,
7621 ) -> S {
7622 let weights = [1.0, -0.4, 0.75, 0.2, -0.6];
7623 let combined = S::linear_combination(vars, &weights, DIMENSION, workspace);
7624 let scored = vars[1].multiply_add(&combined, &vars[0]);
7626 let stacks = [
7630 [0.31, 0.62, -0.24, 0.11, -0.05],
7631 [-0.17, 0.45, 0.33, -0.28, 0.09],
7632 [0.52, -0.38, 0.19, 0.07, -0.13],
7633 ];
7634 let scales = [0.8, -1.3, 0.45];
7635 let inputs = [scored.clone(), combined.clone(), vars[2].clone()];
7636 let summed = S::affine_composed_sum(&inputs, &scales, &stacks, DIMENSION, workspace);
7637 let producted = summed.mul(&vars[3]);
7639 let deviation_stacks = [
7645 [0.44, -0.21, 0.36, -0.12, 0.05],
7646 [-0.29, 0.58, -0.17, 0.23, -0.08],
7647 ];
7648 let deviation_lefts = [vars[4].clone(), vars[0].clone()];
7649 let warped =
7650 S::weighted_compose_sum(&deviation_lefts, &producted, &deviation_stacks, &summed);
7651 warped.compose_unary([0.9, -0.5, 0.27, -0.14, 0.06])
7653 }
7654
7655 fn assert_close(label: &str, fused: &[f64], generic: &[f64]) {
7656 assert_eq!(
7657 fused.len(),
7658 generic.len(),
7659 "{label}: fused length {} != generic length {}",
7660 fused.len(),
7661 generic.len()
7662 );
7663 for (index, (left, right)) in fused.iter().zip(generic).enumerate() {
7664 let scale = left.abs().max(right.abs()).max(1.0);
7665 assert!(
7666 (left - right).abs() <= 1e-12 * scale,
7667 "{label}[{index}]: fused {left:.17e} vs generic {right:.17e}"
7668 );
7669 }
7670 }
7671
7672 #[test]
7676 fn the_fused_batch_reproduces_the_generic_one_seed_on_every_channel() {
7677 let workspace = DynamicJetBatchWorkspace::new(LANES);
7678 let batch_vars: Vec<DynamicOneSeedBatch<'_>> = (0..DIMENSION)
7679 .map(|axis| {
7680 DynamicOneSeedBatch::seed_directions(
7681 point(axis),
7682 axis,
7683 DIMENSION,
7684 &workspace,
7685 |lane| direction(lane, axis),
7686 )
7687 })
7688 .collect();
7689 let batch = expression(&batch_vars, &workspace);
7690
7691 for lane in 0..LANES {
7692 let arena = DynamicJetArena::new();
7693 let seed_vars: Vec<DynamicOneSeed<'_>> = (0..DIMENSION)
7694 .map(|axis| {
7695 DynamicOneSeed::seed_direction(
7696 point(axis),
7697 axis,
7698 direction(lane, axis),
7699 DIMENSION,
7700 &arena,
7701 )
7702 })
7703 .collect();
7704 let single = expression(&seed_vars, &arena);
7705
7706 assert!(
7707 (batch.base.v - single.base.v).abs() <= 1e-12 * batch.base.v.abs().max(1.0),
7708 "lane {lane} value: fused {:.17e} vs generic {:.17e}",
7709 batch.base.v,
7710 single.base.v
7711 );
7712 assert_close(&format!("lane {lane} gradient"), batch.base.g(), single.base.g());
7713 assert_close(&format!("lane {lane} hessian"), batch.base.h(), single.base.h());
7714 assert_close(
7715 &format!("lane {lane} contracted third"),
7716 batch.contracted_third(lane),
7717 single.contracted_third(),
7718 );
7719 }
7720 }
7721
7722 #[test]
7727 fn the_fused_batch_control_is_not_vacuous() {
7728 let workspace = DynamicJetBatchWorkspace::new(LANES);
7729 let batch_vars: Vec<DynamicOneSeedBatch<'_>> = (0..DIMENSION)
7730 .map(|axis| {
7731 DynamicOneSeedBatch::seed_directions(
7732 point(axis),
7733 axis,
7734 DIMENSION,
7735 &workspace,
7736 |lane| direction(lane, axis),
7737 )
7738 })
7739 .collect();
7740 let batch = expression(&batch_vars, &workspace);
7741 assert!(batch.base.v.abs() > 1e-6, "value {:.3e}", batch.base.v);
7742 let gradient_scale = batch
7743 .base
7744 .g()
7745 .iter()
7746 .fold(0.0f64, |worst, value| worst.max(value.abs()));
7747 assert!(gradient_scale > 1e-6, "gradient scale {gradient_scale:.3e}");
7748 let hessian_scale = batch
7749 .base
7750 .h()
7751 .iter()
7752 .fold(0.0f64, |worst, value| worst.max(value.abs()));
7753 assert!(hessian_scale > 1e-6, "hessian scale {hessian_scale:.3e}");
7754 for lane in 0..LANES {
7755 let third_scale = batch
7756 .contracted_third(lane)
7757 .iter()
7758 .fold(0.0f64, |worst, value| worst.max(value.abs()));
7759 assert!(
7760 third_scale > 1e-6,
7761 "lane {lane} contracted-third scale {third_scale:.3e}"
7762 );
7763 }
7764 let first = batch.contracted_third(0).to_vec();
7767 let last = batch.contracted_third(LANES - 1).to_vec();
7768 let separation = first
7769 .iter()
7770 .zip(&last)
7771 .fold(0.0f64, |worst, (left, right)| worst.max((left - right).abs()));
7772 assert!(separation > 1e-6, "lane separation {separation:.3e}");
7773 }
7774}