1use std::cell::UnsafeCell;
14use std::mem::MaybeUninit;
15
16use crate::jet_scalar::{
17 Order2, RuntimeJetScalar, SymmetricQuadraticCoefficients, aggregate_shared_source_derivatives,
18 canonical_shared_source_schedule,
19};
20
21#[derive(Clone, Copy, Debug)]
22struct GraphNode {
23 value: f64,
24 support: u16,
25 edge_start: u16,
26 edge_len: u16,
27 primary_axis: u8,
28}
29
30#[derive(Clone, Copy, Debug)]
31enum CurvatureEvent {
32 RankOne {
33 owner: u8,
34 input: u8,
35 second: f64,
36 },
37 Cross {
38 owner: u8,
39 left: u8,
40 right: u8,
41 },
42 Diagonal {
43 owner: u8,
44 term_start: u16,
45 len: u16,
46 },
47 Projected {
48 owner: u8,
49 curvature_start: u16,
50 support: u16,
51 },
52}
53
54const MAX_PRIMARY_DIMENSION: usize = 16;
55const MAX_QUADRATIC_ARITY: usize = 32;
56const MAX_GRAPH_NODES: usize = 64;
57const MAX_GRAPH_EDGES: usize = MAX_GRAPH_NODES * MAX_GRAPH_NODES;
58const NO_PRIMARY_AXIS: u8 = u8::MAX;
59const MAX_PROJECTED_CURVATURE_VALUES: usize =
60 MAX_GRAPH_NODES * MAX_PRIMARY_DIMENSION * (MAX_PRIMARY_DIMENSION + 1) / 2;
61const EMPTY_NODE: GraphNode = GraphNode {
62 value: 0.0,
63 support: 0,
64 edge_start: 0,
65 edge_len: 0,
66 primary_axis: NO_PRIMARY_AXIS,
67};
68const EMPTY_CURVATURE_EVENT: CurvatureEvent = CurvatureEvent::RankOne {
69 owner: 0,
70 input: 0,
71 second: 0.0,
72};
73
74struct InlineScalars {
80 slots: [MaybeUninit<f64>; MAX_QUADRATIC_ARITY],
81 len: usize,
82}
83
84impl InlineScalars {
85 #[inline(always)]
86 fn initialize_zeros(storage: &mut MaybeUninit<Self>, len: usize) -> &mut Self {
87 assert!(
88 len <= MAX_QUADRATIC_ARITY,
89 "inline scalar capacity exceeded"
90 );
91 let scalars = storage.as_mut_ptr();
92 unsafe {
96 std::ptr::addr_of_mut!((*scalars).len).write(len);
97 let slots = &mut *std::ptr::addr_of_mut!((*scalars).slots);
98 for slot in &mut slots[..len] {
99 slot.write(0.0);
100 }
101 &mut *scalars
102 }
103 }
104
105 #[inline(always)]
106 fn as_slice(&self) -> &[f64] {
107 unsafe { std::slice::from_raw_parts(self.slots.as_ptr().cast::<f64>(), self.len) }
110 }
111
112 #[inline(always)]
113 fn as_mut_slice(&mut self) -> &mut [f64] {
114 unsafe { std::slice::from_raw_parts_mut(self.slots.as_mut_ptr().cast::<f64>(), self.len) }
116 }
117}
118
119#[derive(Debug)]
120struct GraphTape {
121 dimension: usize,
122 node_len: usize,
123 edge_len: usize,
124 event_len: usize,
125 diagonal_len: usize,
126 projected_curvature_len: usize,
127 nodes: [GraphNode; MAX_GRAPH_NODES],
128 gradients: [f64; MAX_GRAPH_NODES * MAX_PRIMARY_DIMENSION],
129 adjoints: [f64; MAX_GRAPH_NODES],
130 edge_parents: [u8; MAX_GRAPH_EDGES],
131 edge_firsts: [f64; MAX_GRAPH_EDGES],
132 events: [CurvatureEvent; MAX_GRAPH_NODES],
133 diagonal_inputs: [u8; MAX_GRAPH_EDGES],
134 diagonal_seconds: [f64; MAX_GRAPH_EDGES],
135 projected_curvatures: [f64; MAX_PROJECTED_CURVATURE_VALUES],
136}
137
138impl GraphTape {
139 fn new() -> Self {
140 Self {
141 dimension: 0,
142 node_len: 0,
143 edge_len: 0,
144 event_len: 0,
145 diagonal_len: 0,
146 projected_curvature_len: 0,
147 nodes: [EMPTY_NODE; MAX_GRAPH_NODES],
148 gradients: [0.0; MAX_GRAPH_NODES * MAX_PRIMARY_DIMENSION],
149 adjoints: [0.0; MAX_GRAPH_NODES],
150 edge_parents: [0; MAX_GRAPH_EDGES],
151 edge_firsts: [0.0; MAX_GRAPH_EDGES],
152 events: [EMPTY_CURVATURE_EVENT; MAX_GRAPH_NODES],
153 diagonal_inputs: [0; MAX_GRAPH_EDGES],
154 diagonal_seconds: [0.0; MAX_GRAPH_EDGES],
155 projected_curvatures: [0.0; MAX_PROJECTED_CURVATURE_VALUES],
156 }
157 }
158}
159
160trait HessianSink<const K: usize> {
161 fn reset(&mut self);
162 fn add_upper(&mut self, row: usize, column: usize, value: f64);
163 fn reflect_upper(&mut self);
164}
165
166struct ArrayHessianSink<'a, const K: usize>(&'a mut [[f64; K]; K]);
167
168impl<const K: usize> HessianSink<K> for ArrayHessianSink<'_, K> {
169 #[inline(always)]
170 fn reset(&mut self) {
171 }
173
174 #[inline(always)]
175 fn add_upper(&mut self, row: usize, column: usize, value: f64) {
176 self.0[row][column] += value;
177 }
178
179 #[inline(always)]
180 fn reflect_upper(&mut self) {
181 for row in 0..K {
182 for column in row + 1..K {
183 self.0[column][row] = self.0[row][column];
184 }
185 }
186 }
187}
188
189struct RowMajorHessianSink<'a, const K: usize>(&'a mut [f64]);
190
191impl<const K: usize> HessianSink<K> for RowMajorHessianSink<'_, K> {
192 #[inline(always)]
193 fn reset(&mut self) {
194 self.0.fill(0.0);
195 }
196
197 #[inline(always)]
198 fn add_upper(&mut self, row: usize, column: usize, value: f64) {
199 self.0[row * K + column] += value;
200 }
201
202 #[inline(always)]
203 fn reflect_upper(&mut self) {
204 for row in 0..K {
205 for column in row + 1..K {
206 self.0[column * K + row] = self.0[row * K + column];
207 }
208 }
209 }
210}
211
212#[derive(Debug)]
218pub struct Order2GraphWorkspace {
219 tape: UnsafeCell<Box<GraphTape>>,
220}
221
222impl Default for Order2GraphWorkspace {
223 fn default() -> Self {
224 Self::new()
225 }
226}
227
228impl Order2GraphWorkspace {
229 #[must_use]
231 pub fn new() -> Self {
232 Self {
233 tape: UnsafeCell::new(Box::new(GraphTape::new())),
234 }
235 }
236
237 pub fn reset(&mut self, dimension: usize) {
239 assert!(
240 dimension <= MAX_PRIMARY_DIMENSION,
241 "compiled graph supports at most {MAX_PRIMARY_DIMENSION} primaries"
242 );
243 let tape = self.tape.get_mut().as_mut();
244 tape.dimension = dimension;
245 tape.node_len = 0;
246 tape.edge_len = 0;
247 tape.event_len = 0;
248 tape.diagonal_len = 0;
249 tape.projected_curvature_len = 0;
250 }
251
252 #[inline(always)]
258 fn tape(&self) -> &GraphTape {
259 unsafe { (&*self.tape.get()).as_ref() }
263 }
264
265 #[inline(always)]
266 fn tape_mut(&self) -> &mut GraphTape {
267 unsafe { (&mut *self.tape.get()).as_mut() }
270 }
271
272 #[inline(always)]
273 fn push<const K: usize>(
274 tape: &mut GraphTape,
275 value: f64,
276 support: u16,
277 gradient: [f64; K],
278 edge_start: usize,
279 ) -> usize {
280 assert_eq!(tape.dimension, K, "compiled graph dimension mismatch");
281 assert!(
282 tape.node_len < MAX_GRAPH_NODES,
283 "compiled graph node capacity exceeded"
284 );
285 let node = tape.node_len;
286 let edge_len = tape.edge_len - edge_start;
287 tape.nodes[node] = GraphNode {
288 value,
289 support,
290 edge_start: edge_start as u16,
291 edge_len: edge_len as u16,
292 primary_axis: NO_PRIMARY_AXIS,
293 };
294 tape.gradients[node * K..(node + 1) * K].copy_from_slice(&gradient);
295 tape.node_len += 1;
296 node
297 }
298
299 #[inline(always)]
300 fn push_edge(tape: &mut GraphTape, parent: usize, first: f64) {
301 assert!(
302 tape.edge_len < MAX_GRAPH_EDGES,
303 "compiled graph edge capacity exceeded"
304 );
305 assert!(
306 parent < tape.node_len,
307 "compiled graph edge parent must name an existing node"
308 );
309 tape.edge_parents[tape.edge_len] = parent as u8;
310 tape.edge_firsts[tape.edge_len] = first;
311 tape.edge_len += 1;
312 }
313
314 #[inline(always)]
315 fn push_event(tape: &mut GraphTape, event: CurvatureEvent) {
316 assert!(
317 tape.event_len < MAX_GRAPH_NODES,
318 "compiled graph curvature-event capacity exceeded"
319 );
320 tape.events[tape.event_len] = event;
321 tape.event_len += 1;
322 }
323
324 #[inline(always)]
325 fn push_diagonal_term(tape: &mut GraphTape, input: usize, second: f64) {
326 assert!(
327 tape.diagonal_len < MAX_GRAPH_EDGES,
328 "compiled graph diagonal-term capacity exceeded"
329 );
330 assert!(
331 input < tape.node_len,
332 "compiled graph diagonal input must name an existing node"
333 );
334 tape.diagonal_inputs[tape.diagonal_len] = input as u8;
335 tape.diagonal_seconds[tape.diagonal_len] = second;
336 tape.diagonal_len += 1;
337 }
338
339 #[inline(always)]
340 fn lower_into<const K: usize, H: HessianSink<K>>(
341 &self,
342 output: usize,
343 gradient: &mut [f64],
344 hessian: &mut H,
345 ) -> f64 {
346 let tape = self.tape_mut();
347 assert_eq!(tape.dimension, K, "compiled graph dimension mismatch");
348 assert!(output < tape.node_len, "compiled graph output is absent");
349 assert_eq!(gradient.len(), K, "compiled graph gradient width mismatch");
350 tape.adjoints[..tape.node_len].fill(0.0);
351 tape.adjoints[output] = 1.0;
352
353 gradient.copy_from_slice(&tape.gradients[output * K..(output + 1) * K]);
354 hessian.reset();
355
356 for node_index in (0..=output).rev() {
357 let adjoint = tape.adjoints[node_index];
358 if adjoint == 0.0 {
359 continue;
360 }
361 let node = tape.nodes[node_index];
362 let edge_start = node.edge_start as usize;
363 for edge in edge_start..edge_start + node.edge_len as usize {
364 let parent = tape.edge_parents[edge] as usize;
365 tape.adjoints[parent] += adjoint * tape.edge_firsts[edge];
366 }
367 }
368
369 for event_index in (0..tape.event_len).rev() {
370 match tape.events[event_index] {
371 CurvatureEvent::RankOne {
372 owner,
373 input,
374 second,
375 } => {
376 let owner_adjoint = tape.adjoints[owner as usize];
377 if owner_adjoint == 0.0 {
378 continue;
379 }
380 let input = input as usize;
381 let curvature_scale = owner_adjoint * second;
382 for_each_supported_upper(tape.nodes[input].support, |primary, other| {
383 hessian.add_upper(
384 primary,
385 other,
386 curvature_scale
387 * tape.gradients[input * K + primary]
388 * tape.gradients[input * K + other],
389 );
390 });
391 }
392 CurvatureEvent::Cross { owner, left, right } => {
393 let owner_adjoint = tape.adjoints[owner as usize];
394 if owner_adjoint == 0.0 {
395 continue;
396 }
397 let left = left as usize;
398 let right = right as usize;
399 for_each_supported_upper(
400 tape.nodes[left].support | tape.nodes[right].support,
401 |primary, other| {
402 let left_primary = tape.gradients[left * K + primary];
403 let right_primary = tape.gradients[right * K + primary];
404 let curvature = left_primary * tape.gradients[right * K + other]
405 + right_primary * tape.gradients[left * K + other];
406 hessian.add_upper(primary, other, owner_adjoint * curvature);
407 },
408 );
409 }
410 CurvatureEvent::Diagonal {
411 owner,
412 term_start,
413 len,
414 } => {
415 let owner_adjoint = tape.adjoints[owner as usize];
416 if owner_adjoint == 0.0 {
417 continue;
418 }
419 let term_start = term_start as usize;
420 for term in term_start..term_start + len as usize {
421 let input = tape.diagonal_inputs[term] as usize;
422 let curvature_scale = owner_adjoint * tape.diagonal_seconds[term];
423 if curvature_scale == 0.0 {
424 continue;
425 }
426 for_each_supported_upper(tape.nodes[input].support, |primary, other| {
427 hessian.add_upper(
428 primary,
429 other,
430 curvature_scale
431 * tape.gradients[input * K + primary]
432 * tape.gradients[input * K + other],
433 );
434 });
435 }
436 }
437 CurvatureEvent::Projected {
438 owner,
439 curvature_start,
440 support,
441 } => {
442 let owner_adjoint = tape.adjoints[owner as usize];
443 if owner_adjoint == 0.0 {
444 continue;
445 }
446 let mut curvature_offset = 0;
447 let curvature_start = curvature_start as usize;
448 for_each_supported_upper_by_column(support, |primary, other| {
449 hessian.add_upper(
450 primary,
451 other,
452 owner_adjoint
453 * tape.projected_curvatures[curvature_start + curvature_offset],
454 );
455 curvature_offset += 1;
456 });
457 }
458 }
459 }
460
461 hessian.reflect_upper();
462 tape.nodes[output].value
463 }
464}
465
466#[inline(always)]
467fn for_each_supported_upper_by_column(support: u16, mut visit: impl FnMut(usize, usize)) {
468 let mut columns = support;
469 while columns != 0 {
470 let other = columns.trailing_zeros() as usize;
471 columns &= columns - 1;
472 let mut rows = support & (u16::MAX >> (MAX_PRIMARY_DIMENSION - other - 1));
473 while rows != 0 {
474 let primary = rows.trailing_zeros() as usize;
475 rows &= rows - 1;
476 visit(primary, other);
477 }
478 }
479}
480
481#[inline(always)]
483fn supported_upper_offset(support: u16, primary: usize, other: usize) -> usize {
484 let primary_rank = (support & ((1_u16 << primary) - 1)).count_ones() as usize;
485 let other_rank = (support & ((1_u16 << other) - 1)).count_ones() as usize;
486 other_rank * (other_rank + 1) / 2 + primary_rank
487}
488
489#[inline(always)]
490fn for_each_supported_upper(mut rows: u16, mut visit: impl FnMut(usize, usize)) {
491 while rows != 0 {
492 let primary = rows.trailing_zeros() as usize;
493 rows &= rows - 1;
494 let mut columns = rows | (1_u16 << primary);
495 while columns != 0 {
496 let other = columns.trailing_zeros() as usize;
497 columns &= columns - 1;
498 visit(primary, other);
499 }
500 }
501}
502
503#[derive(Clone, Copy, Debug)]
508pub struct Order2Graph<'arena, const K: usize> {
509 workspace: &'arena Order2GraphWorkspace,
510 node: usize,
511}
512
513impl<'arena, const K: usize> Order2Graph<'arena, K> {
514 #[must_use]
516 pub fn into_order2(self) -> Order2<K> {
517 let mut out = crate::jet_tower::Tower2::zero();
518 let mut hessian = ArrayHessianSink(&mut out.h);
519 out.v = self
520 .workspace
521 .lower_into(self.node, &mut out.g, &mut hessian);
522 Order2(out)
523 }
524
525 #[must_use]
530 pub fn lower_into(self, gradient: &mut [f64], hessian_row_major: &mut [f64]) -> f64 {
531 assert_eq!(gradient.len(), K, "compiled graph gradient width mismatch");
532 assert_eq!(
533 hessian_row_major.len(),
534 K * K,
535 "compiled graph Hessian width mismatch"
536 );
537 let mut hessian = RowMajorHessianSink::<K>(hessian_row_major);
538 self.workspace.lower_into(self.node, gradient, &mut hessian)
539 }
540
541 #[inline(always)]
542 fn assert_compatible(&self, other: &Self) {
543 assert!(
544 std::ptr::eq(self.workspace, other.workspace),
545 "compiled graph scalars belong to different workspaces"
546 );
547 }
548
549 #[inline(always)]
550 fn unary(&self, value: f64, first: f64, second: f64) -> Self {
551 let tape = self.workspace.tape_mut();
552 let mut gradient = [0.0; K];
553 for primary in 0..K {
554 gradient[primary] = first * tape.gradients[self.node * K + primary];
555 }
556 let support = tape.nodes[self.node].support;
557 let owner = tape.node_len;
558 let edge_start = tape.edge_len;
559 Order2GraphWorkspace::push_edge(tape, self.node, first);
560 Order2GraphWorkspace::push_event(
561 tape,
562 CurvatureEvent::RankOne {
563 owner: owner as u8,
564 input: self.node as u8,
565 second,
566 },
567 );
568 let node = Order2GraphWorkspace::push(tape, value, support, gradient, edge_start);
569 Self {
570 workspace: self.workspace,
571 node,
572 }
573 }
574}
575
576impl<'arena, const K: usize> RuntimeJetScalar<'arena> for Order2Graph<'arena, K> {
577 type Workspace = Order2GraphWorkspace;
578
579 #[inline(always)]
580 fn constant(c: f64, dimension: usize, workspace: &'arena Self::Workspace) -> Self {
581 assert_eq!(dimension, K, "compiled graph dimension mismatch");
582 let tape = workspace.tape_mut();
583 let edge_start = tape.edge_len;
584 let node = Order2GraphWorkspace::push(tape, c, 0, [0.0; K], edge_start);
585 Self { workspace, node }
586 }
587
588 #[inline(always)]
589 fn variable(x: f64, axis: usize, dimension: usize, workspace: &'arena Self::Workspace) -> Self {
590 assert_eq!(dimension, K, "compiled graph dimension mismatch");
591 assert!(axis < K, "compiled graph variable axis out of bounds");
592 let mut gradient = [0.0; K];
593 gradient[axis] = 1.0;
594 let tape = workspace.tape_mut();
595 let edge_start = tape.edge_len;
596 let node = Order2GraphWorkspace::push(tape, x, 1_u16 << axis, gradient, edge_start);
597 tape.nodes[node].primary_axis = axis as u8;
598 Self { workspace, node }
599 }
600
601 #[inline(always)]
602 fn symmetric_quadratic_form<C: SymmetricQuadraticCoefficients>(
603 inputs: &[Self],
604 coefficients: &C,
605 dimension: usize,
606 workspace: &'arena Self::Workspace,
607 ) -> Self {
608 assert_eq!(dimension, K, "compiled graph dimension mismatch");
609 assert_eq!(inputs.len(), coefficients.dimension());
610 assert!(
611 inputs.len() <= MAX_QUADRATIC_ARITY,
612 "compiled graph quadratic arity exceeds graph capacity"
613 );
614 assert!(
615 inputs
616 .iter()
617 .all(|input| std::ptr::eq(input.workspace, workspace)),
618 "compiled graph quadratic inputs belong to different workspaces"
619 );
620
621 let input_dimension = inputs.len();
622 let mut values_storage = MaybeUninit::uninit();
623 let values = InlineScalars::initialize_zeros(&mut values_storage, input_dimension);
624 let mut support = 0_u16;
625 {
626 let tape = workspace.tape();
627 for (value, input) in values.as_mut_slice().iter_mut().zip(inputs) {
628 *value = tape.nodes[input.node].value;
629 support |= tape.nodes[input.node].support;
630 }
631 }
632 let mut input_primary_axes = [NO_PRIMARY_AXIS; MAX_QUADRATIC_ARITY];
638 let all_inputs_primary = {
639 let tape = workspace.tape();
640 inputs.iter().enumerate().all(|(position, input)| {
641 let primary_axis = tape.nodes[input.node].primary_axis;
642 if primary_axis == NO_PRIMARY_AXIS {
643 false
644 } else {
645 input_primary_axes[position] = primary_axis;
646 true
647 }
648 })
649 };
650 let mut projected_values_storage = MaybeUninit::uninit();
651 let projected_values =
652 InlineScalars::initialize_zeros(&mut projected_values_storage, input_dimension);
653 coefficients.multiply(values.as_slice(), projected_values.as_mut_slice());
654 let value = values
655 .as_slice()
656 .iter()
657 .zip(projected_values.as_slice())
658 .map(|(&input, &projected)| input * projected)
659 .sum();
660
661 let supported_dimension = support.count_ones() as usize;
662 let curvature_len = supported_dimension * (supported_dimension + 1) / 2;
663 let curvature_start = {
664 let tape = workspace.tape_mut();
665 assert!(
666 curvature_len <= MAX_PROJECTED_CURVATURE_VALUES - tape.projected_curvature_len,
667 "compiled graph projected-curvature capacity exceeded"
668 );
669 let start = tape.projected_curvature_len;
670 tape.projected_curvature_len += curvature_len;
671 start
672 };
673 let mut curvature_offset = 0;
674 if all_inputs_primary {
675 workspace.tape_mut().projected_curvatures
680 [curvature_start..curvature_start + curvature_len]
681 .fill(0.0);
682 let mut direction_storage = MaybeUninit::uninit();
683 let direction =
684 InlineScalars::initialize_zeros(&mut direction_storage, input_dimension);
685 let mut projected_direction_storage = MaybeUninit::uninit();
686 let projected_direction =
687 InlineScalars::initialize_zeros(&mut projected_direction_storage, input_dimension);
688 coefficients.visit_upper_triangle(
689 direction.as_mut_slice(),
690 projected_direction.as_mut_slice(),
691 |row, column, coefficient| {
692 let row_axis = input_primary_axes[row] as usize;
693 let column_axis = input_primary_axes[column] as usize;
694 let primary = row_axis.min(column_axis);
695 let other = row_axis.max(column_axis);
696 let offset = supported_upper_offset(support, primary, other);
697 let repeated_axis_multiplicity = if row != column && row_axis == column_axis {
698 2.0
699 } else {
700 1.0
701 };
702 workspace.tape_mut().projected_curvatures[curvature_start + offset] +=
703 2.0 * repeated_axis_multiplicity * coefficient;
704 },
705 );
706 curvature_offset = curvature_len;
707 } else {
708 let mut direction_storage = MaybeUninit::uninit();
713 let direction =
714 InlineScalars::initialize_zeros(&mut direction_storage, input_dimension);
715 let mut projected_direction_storage = MaybeUninit::uninit();
716 let projected_direction =
717 InlineScalars::initialize_zeros(&mut projected_direction_storage, input_dimension);
718 let mut supported_columns = support;
719 while supported_columns != 0 {
720 let other = supported_columns.trailing_zeros() as usize;
721 supported_columns &= supported_columns - 1;
722 {
723 let tape = workspace.tape();
724 for (channel, input) in direction.as_mut_slice().iter_mut().zip(inputs) {
725 *channel = tape.gradients[input.node * K + other];
726 }
727 }
728 coefficients.multiply(direction.as_slice(), projected_direction.as_mut_slice());
729 let mut supported_rows =
730 support & (u16::MAX >> (MAX_PRIMARY_DIMENSION - other - 1));
731 while supported_rows != 0 {
732 let primary = supported_rows.trailing_zeros() as usize;
733 supported_rows &= supported_rows - 1;
734 let curvature = {
735 let tape = workspace.tape();
736 inputs
737 .iter()
738 .zip(projected_direction.as_slice())
739 .map(|(input, &projected)| {
740 tape.gradients[input.node * K + primary] * projected
741 })
742 .sum::<f64>()
743 };
744 workspace.tape_mut().projected_curvatures[curvature_start + curvature_offset] =
745 2.0 * curvature;
746 curvature_offset += 1;
747 }
748 }
749 }
750 assert_eq!(
751 curvature_offset, curvature_len,
752 "compiled graph projected-curvature schedule must fill its exact packed range"
753 );
754
755 let tape = workspace.tape_mut();
756 let owner = tape.node_len;
757 let edge_start = tape.edge_len;
758 let mut gradient = [0.0; K];
759 for axis in 0..input_dimension {
760 let first = 2.0 * projected_values.as_slice()[axis];
761 Order2GraphWorkspace::push_edge(tape, inputs[axis].node, first);
762 if all_inputs_primary {
763 let primary = tape.nodes[inputs[axis].node].primary_axis as usize;
764 gradient[primary] += first;
765 } else {
766 for primary in 0..K {
767 gradient[primary] += first * tape.gradients[inputs[axis].node * K + primary];
768 }
769 }
770 }
771 Order2GraphWorkspace::push_event(
772 tape,
773 CurvatureEvent::Projected {
774 owner: owner as u8,
775 curvature_start: curvature_start as u16,
776 support,
777 },
778 );
779 let node = Order2GraphWorkspace::push(tape, value, support, gradient, edge_start);
780 Self { workspace, node }
781 }
782
783 #[inline(always)]
784 fn linear_combination(
785 inputs: &[Self],
786 weights: &[f64],
787 dimension: usize,
788 workspace: &'arena Self::Workspace,
789 ) -> Self {
790 assert_eq!(dimension, K, "compiled graph dimension mismatch");
791 assert_eq!(inputs.len(), weights.len());
792 assert!(
793 inputs
794 .iter()
795 .all(|input| std::ptr::eq(input.workspace, workspace)),
796 "compiled graph linear inputs belong to different workspaces"
797 );
798 let tape = workspace.tape_mut();
799 let all_inputs_primary = inputs
800 .iter()
801 .all(|input| tape.nodes[input.node].primary_axis != NO_PRIMARY_AXIS);
802 let edge_start = tape.edge_len;
803 let mut value = 0.0;
804 let mut gradient = [0.0; K];
805 let mut support = 0_u16;
806 for (input, &weight) in inputs.iter().zip(weights) {
807 value += weight * tape.nodes[input.node].value;
808 Order2GraphWorkspace::push_edge(tape, input.node, weight);
809 support |= tape.nodes[input.node].support;
810 if all_inputs_primary {
811 let primary = tape.nodes[input.node].primary_axis as usize;
812 gradient[primary] += weight;
813 } else {
814 for primary in 0..K {
815 gradient[primary] += weight * tape.gradients[input.node * K + primary];
816 }
817 }
818 }
819 let node = Order2GraphWorkspace::push(tape, value, support, gradient, edge_start);
820 Self { workspace, node }
821 }
822
823 #[inline(always)]
824 fn add_constant(&self, constant: f64, workspace: &'arena Self::Workspace) -> Self {
825 assert!(std::ptr::eq(self.workspace, workspace));
826 self.unary(self.value() + constant, 1.0, 0.0)
827 }
828
829 #[inline(always)]
830 fn multiply_add(&self, right: &Self, addend: &Self) -> Self {
831 self.assert_compatible(right);
832 self.assert_compatible(addend);
833 let tape = self.workspace.tape_mut();
834 let left_value = tape.nodes[self.node].value;
835 let right_value = tape.nodes[right.node].value;
836 let mut gradient = [0.0; K];
837 for primary in 0..K {
838 gradient[primary] = left_value * tape.gradients[right.node * K + primary]
839 + tape.gradients[self.node * K + primary] * right_value
840 + tape.gradients[addend.node * K + primary];
841 }
842 let value = left_value * right_value + tape.nodes[addend.node].value;
843 let support = tape.nodes[self.node].support
844 | tape.nodes[right.node].support
845 | tape.nodes[addend.node].support;
846 let owner = tape.node_len;
847 let edge_start = tape.edge_len;
848 Order2GraphWorkspace::push_edge(tape, self.node, right_value);
849 Order2GraphWorkspace::push_edge(tape, right.node, left_value);
850 Order2GraphWorkspace::push_edge(tape, addend.node, 1.0);
851 Order2GraphWorkspace::push_event(
852 tape,
853 CurvatureEvent::Cross {
854 owner: owner as u8,
855 left: self.node as u8,
856 right: right.node as u8,
857 },
858 );
859 let node = Order2GraphWorkspace::push(tape, value, support, gradient, edge_start);
860 Self {
861 workspace: self.workspace,
862 node,
863 }
864 }
865
866 #[inline(always)]
867 fn composed_sum(
868 inputs: &[Self],
869 derivative_stacks: &[[f64; 5]],
870 dimension: usize,
871 workspace: &'arena Self::Workspace,
872 ) -> Self {
873 assert_eq!(dimension, K, "compiled graph dimension mismatch");
874 assert_eq!(inputs.len(), derivative_stacks.len());
875 assert!(
876 inputs
877 .iter()
878 .all(|input| std::ptr::eq(input.workspace, workspace)),
879 "compiled graph composed inputs belong to different workspaces"
880 );
881 let tape = workspace.tape_mut();
882 let owner = tape.node_len;
883 let edge_start = tape.edge_len;
884 let term_start = tape.diagonal_len;
885 let mut value = 0.0;
886 let mut gradient = [0.0; K];
887 let mut support = 0_u16;
888 for (input, stack) in inputs.iter().zip(derivative_stacks) {
889 value += stack[0];
890 Order2GraphWorkspace::push_edge(tape, input.node, stack[1]);
891 Order2GraphWorkspace::push_diagonal_term(tape, input.node, stack[2]);
892 support |= tape.nodes[input.node].support;
893 for primary in 0..K {
894 gradient[primary] += stack[1] * tape.gradients[input.node * K + primary];
895 }
896 }
897 Order2GraphWorkspace::push_event(
898 tape,
899 CurvatureEvent::Diagonal {
900 owner: owner as u8,
901 term_start: term_start as u16,
902 len: inputs.len() as u16,
903 },
904 );
905 let node = Order2GraphWorkspace::push(tape, value, support, gradient, edge_start);
906 Self { workspace, node }
907 }
908
909 #[inline(always)]
910 fn product(&self, right: &Self) -> Self {
911 self.mul(right)
912 }
913
914 #[inline(always)]
915 fn affine_compose(
916 &self,
917 input_scale: f64,
918 input_shift: f64,
919 derivative_stack: [f64; 5],
920 workspace: &'arena Self::Workspace,
921 ) -> Self {
922 assert!(std::ptr::eq(self.workspace, workspace));
923 assert!(input_shift.is_finite(), "affine input shift must be finite");
924 self.unary(
925 derivative_stack[0],
926 derivative_stack[1] * input_scale,
927 derivative_stack[2] * input_scale * input_scale,
928 )
929 }
930
931 #[inline(always)]
932 fn affine_composed_sum(
933 inputs: &[Self],
934 input_scales: &[f64],
935 derivative_stacks: &[[f64; 5]],
936 dimension: usize,
937 workspace: &'arena Self::Workspace,
938 ) -> Self {
939 assert_eq!(dimension, K, "compiled graph dimension mismatch");
940 assert_eq!(inputs.len(), input_scales.len());
941 assert_eq!(inputs.len(), derivative_stacks.len());
942 assert!(
943 inputs
944 .iter()
945 .all(|input| std::ptr::eq(input.workspace, workspace)),
946 "compiled graph composed inputs belong to different workspaces"
947 );
948 let tape = workspace.tape_mut();
949 let owner = tape.node_len;
950 let edge_start = tape.edge_len;
951 let term_start = tape.diagonal_len;
952 let mut value = 0.0;
953 let mut gradient = [0.0; K];
954 let mut support = 0_u16;
955 for ((input, &input_scale), stack) in inputs.iter().zip(input_scales).zip(derivative_stacks)
956 {
957 let first = stack[1] * input_scale;
958 let second = stack[2] * input_scale * input_scale;
959 value += stack[0];
960 Order2GraphWorkspace::push_edge(tape, input.node, first);
961 Order2GraphWorkspace::push_diagonal_term(tape, input.node, second);
962 support |= tape.nodes[input.node].support;
963 for primary in 0..K {
964 gradient[primary] += first * tape.gradients[input.node * K + primary];
965 }
966 }
967 Order2GraphWorkspace::push_event(
968 tape,
969 CurvatureEvent::Diagonal {
970 owner: owner as u8,
971 term_start: term_start as u16,
972 len: inputs.len() as u16,
973 },
974 );
975 let node = Order2GraphWorkspace::push(tape, value, support, gradient, edge_start);
976 Self { workspace, node }
977 }
978
979 #[inline(always)]
980 fn shared_multiply_add_affine_composed_sum<const N: usize>(
981 lefts: &[&Self; N],
982 right: &Self,
983 addend: &Self,
984 addend_scales: &[f64; N],
985 input_scales: &[f64; N],
986 derivative_stacks: &[[f64; 5]; N],
987 dimension: usize,
988 workspace: &'arena Self::Workspace,
989 ) -> Self {
990 assert_eq!(dimension, K, "compiled graph dimension mismatch");
991 assert!(
992 lefts
993 .iter()
994 .all(|input| std::ptr::eq(input.workspace, workspace))
995 && (N == 0 || std::ptr::eq(right.workspace, workspace)),
996 "compiled fused product-composition inputs belong to different workspaces"
997 );
998 let addend_live = addend_scales.iter().any(|&scale| scale != 0.0);
999 assert!(
1000 !addend_live || std::ptr::eq(addend.workspace, workspace),
1001 "live compiled fused addends belong to different workspaces"
1002 );
1003
1004 let tape = workspace.tape_mut();
1005 let right_node = right.node;
1006 let addend_node = addend.node;
1007 let (representatives, term_sources, source_count) =
1008 canonical_shared_source_schedule::<N>(|term, representative| {
1009 lefts[term].node == lefts[representative].node
1010 && addend_scales[term] == addend_scales[representative]
1011 });
1012 let (value, source_derivatives) =
1013 aggregate_shared_source_derivatives(&term_sources, input_scales, derivative_stacks);
1014 let mut source_gradients = [[0.0; K]; N];
1015 let mut source_primary_axes = [NO_PRIMARY_AXIS; N];
1016 let mut gradient = [0.0; K];
1017 let mut support = 0_u16;
1018 let mut right_first = 0.0;
1019 let mut addend_first = 0.0;
1020 if N != 0 {
1021 support |= tape.nodes[right_node].support;
1022 }
1023 if addend_live {
1024 support |= tape.nodes[addend_node].support;
1025 }
1026 for source in 0..source_count {
1027 let term = representatives[source];
1028 let left = lefts[term].node;
1029 let left_value = tape.nodes[left].value;
1030 let right_value = tape.nodes[right_node].value;
1031 let first = source_derivatives[source][1];
1032 let left_primary_axis = tape.nodes[left].primary_axis;
1033 source_primary_axes[source] = left_primary_axis;
1034 support |= tape.nodes[left].support;
1035 right_first += first * left_value;
1036 addend_first += first * addend_scales[term];
1037 for primary in 0..K {
1038 let product_gradient = left_value * tape.gradients[right_node * K + primary];
1039 let inner_gradient = if addend_scales[term] == 0.0 {
1040 product_gradient
1041 } else if addend_scales[term] == 1.0 {
1042 product_gradient + tape.gradients[addend_node * K + primary]
1043 } else {
1044 product_gradient
1045 + addend_scales[term] * tape.gradients[addend_node * K + primary]
1046 };
1047 source_gradients[source][primary] = inner_gradient;
1048 }
1049 if left_primary_axis == NO_PRIMARY_AXIS {
1050 for primary in 0..K {
1051 let left_contribution = tape.gradients[left * K + primary] * right_value;
1052 source_gradients[source][primary] += left_contribution;
1053 gradient[primary] += first * left_contribution;
1054 }
1055 } else {
1056 let primary = left_primary_axis as usize;
1057 source_gradients[source][primary] += right_value;
1058 gradient[primary] += first * right_value;
1059 }
1060 }
1061 if N != 0 {
1062 for primary in 0..K {
1063 gradient[primary] += right_first * tape.gradients[right_node * K + primary];
1064 }
1065 }
1066 if addend_live {
1067 for primary in 0..K {
1068 gradient[primary] += addend_first * tape.gradients[addend_node * K + primary];
1069 }
1070 }
1071
1072 let supported_dimension = support.count_ones() as usize;
1073 let curvature_len = supported_dimension * (supported_dimension + 1) / 2;
1074 assert!(
1075 curvature_len <= MAX_PROJECTED_CURVATURE_VALUES - tape.projected_curvature_len,
1076 "compiled graph projected-curvature capacity exceeded"
1077 );
1078 let curvature_start = tape.projected_curvature_len;
1079 tape.projected_curvature_len += curvature_len;
1080 let mut curvature_offset = 0;
1081 for_each_supported_upper_by_column(support, |primary, other| {
1082 let mut channel = 0.0;
1083 for source in 0..source_count {
1084 channel += source_derivatives[source][2]
1085 * source_gradients[source][primary]
1086 * source_gradients[source][other];
1087 if source_primary_axes[source] == NO_PRIMARY_AXIS {
1088 let term = representatives[source];
1089 let left = lefts[term].node;
1090 let cross = tape.gradients[left * K + primary]
1091 * tape.gradients[right_node * K + other]
1092 + tape.gradients[left * K + other]
1093 * tape.gradients[right_node * K + primary];
1094 channel += source_derivatives[source][1] * cross;
1095 }
1096 }
1097 tape.projected_curvatures[curvature_start + curvature_offset] = channel;
1098 curvature_offset += 1;
1099 });
1100 for source in 0..source_count {
1101 let primary_axis = source_primary_axes[source];
1102 if primary_axis == NO_PRIMARY_AXIS {
1103 continue;
1104 }
1105 let primary_axis = primary_axis as usize;
1106 let first = source_derivatives[source][1];
1107 let mut right_support = tape.nodes[right_node].support;
1108 while right_support != 0 {
1109 let right_axis = right_support.trailing_zeros() as usize;
1110 right_support &= right_support - 1;
1111 let primary = primary_axis.min(right_axis);
1112 let other = primary_axis.max(right_axis);
1113 let offset = supported_upper_offset(support, primary, other);
1114 let diagonal_scale = if primary_axis == right_axis { 2.0 } else { 1.0 };
1115 let cross = diagonal_scale * tape.gradients[right_node * K + right_axis];
1116 tape.projected_curvatures[curvature_start + offset] += first * cross;
1117 }
1118 }
1119 assert_eq!(
1120 curvature_offset, curvature_len,
1121 "compiled fused product-composition must fill its packed curvature"
1122 );
1123
1124 let owner = tape.node_len;
1125 let edge_start = tape.edge_len;
1126 for source in 0..source_count {
1127 let term = representatives[source];
1128 let left = lefts[term].node;
1129 let left_first = source_derivatives[source][1] * tape.nodes[right_node].value;
1130 Order2GraphWorkspace::push_edge(tape, left, left_first);
1131 }
1132 if N != 0 {
1133 Order2GraphWorkspace::push_edge(tape, right_node, right_first);
1134 }
1135 if addend_live {
1136 Order2GraphWorkspace::push_edge(tape, addend_node, addend_first);
1137 }
1138 Order2GraphWorkspace::push_event(
1139 tape,
1140 CurvatureEvent::Projected {
1141 owner: owner as u8,
1142 curvature_start: curvature_start as u16,
1143 support,
1144 },
1145 );
1146 let node = Order2GraphWorkspace::push(tape, value, support, gradient, edge_start);
1147 Self { workspace, node }
1148 }
1149
1150 #[inline(always)]
1151 fn dimension(&self) -> usize {
1152 K
1153 }
1154
1155 #[inline(always)]
1156 fn value(&self) -> f64 {
1157 self.workspace.tape().nodes[self.node].value
1158 }
1159
1160 #[inline(always)]
1161 fn add(&self, other: &Self) -> Self {
1162 self.assert_compatible(other);
1163 let tape = self.workspace.tape_mut();
1164 let mut gradient = [0.0; K];
1165 for primary in 0..K {
1166 gradient[primary] =
1167 tape.gradients[self.node * K + primary] + tape.gradients[other.node * K + primary];
1168 }
1169 let value = tape.nodes[self.node].value + tape.nodes[other.node].value;
1170 let support = tape.nodes[self.node].support | tape.nodes[other.node].support;
1171 let edge_start = tape.edge_len;
1172 Order2GraphWorkspace::push_edge(tape, self.node, 1.0);
1173 Order2GraphWorkspace::push_edge(tape, other.node, 1.0);
1174 let node = Order2GraphWorkspace::push(tape, value, support, gradient, edge_start);
1175 Self {
1176 workspace: self.workspace,
1177 node,
1178 }
1179 }
1180
1181 #[inline(always)]
1182 fn sub(&self, other: &Self) -> Self {
1183 self.assert_compatible(other);
1184 let tape = self.workspace.tape_mut();
1185 let mut gradient = [0.0; K];
1186 for primary in 0..K {
1187 gradient[primary] =
1188 tape.gradients[self.node * K + primary] - tape.gradients[other.node * K + primary];
1189 }
1190 let value = tape.nodes[self.node].value - tape.nodes[other.node].value;
1191 let support = tape.nodes[self.node].support | tape.nodes[other.node].support;
1192 let edge_start = tape.edge_len;
1193 Order2GraphWorkspace::push_edge(tape, self.node, 1.0);
1194 Order2GraphWorkspace::push_edge(tape, other.node, -1.0);
1195 let node = Order2GraphWorkspace::push(tape, value, support, gradient, edge_start);
1196 Self {
1197 workspace: self.workspace,
1198 node,
1199 }
1200 }
1201
1202 #[inline(always)]
1203 fn mul(&self, other: &Self) -> Self {
1204 self.assert_compatible(other);
1205 let tape = self.workspace.tape_mut();
1206 let left_value = tape.nodes[self.node].value;
1207 let right_value = tape.nodes[other.node].value;
1208 let mut gradient = [0.0; K];
1209 for primary in 0..K {
1210 gradient[primary] = left_value * tape.gradients[other.node * K + primary]
1211 + tape.gradients[self.node * K + primary] * right_value;
1212 }
1213 let support = tape.nodes[self.node].support | tape.nodes[other.node].support;
1214 let owner = tape.node_len;
1215 let edge_start = tape.edge_len;
1216 Order2GraphWorkspace::push_edge(tape, self.node, right_value);
1217 Order2GraphWorkspace::push_edge(tape, other.node, left_value);
1218 Order2GraphWorkspace::push_event(
1219 tape,
1220 CurvatureEvent::Cross {
1221 owner: owner as u8,
1222 left: self.node as u8,
1223 right: other.node as u8,
1224 },
1225 );
1226 let node = Order2GraphWorkspace::push(
1227 tape,
1228 left_value * right_value,
1229 support,
1230 gradient,
1231 edge_start,
1232 );
1233 Self {
1234 workspace: self.workspace,
1235 node,
1236 }
1237 }
1238
1239 #[inline(always)]
1240 fn neg(&self) -> Self {
1241 self.unary(-self.value(), -1.0, 0.0)
1242 }
1243
1244 #[inline(always)]
1245 fn scale(&self, scale: f64) -> Self {
1246 let tape = self.workspace.tape_mut();
1247 let mut gradient = [0.0; K];
1248 for primary in 0..K {
1249 gradient[primary] = scale * tape.gradients[self.node * K + primary];
1250 }
1251 let value = scale * tape.nodes[self.node].value;
1252 let support = tape.nodes[self.node].support;
1253 let edge_start = tape.edge_len;
1254 Order2GraphWorkspace::push_edge(tape, self.node, scale);
1255 let node = Order2GraphWorkspace::push(tape, value, support, gradient, edge_start);
1256 Self {
1257 workspace: self.workspace,
1258 node,
1259 }
1260 }
1261
1262 #[inline(always)]
1263 fn compose_unary(&self, derivatives: [f64; 5]) -> Self {
1264 self.unary(derivatives[0], derivatives[1], derivatives[2])
1265 }
1266}
1267
1268#[cfg(test)]
1269mod tests {
1270 use super::*;
1271 use crate::jet_scalar::{DynamicJetArena, DynamicOrder2, FixedRuntimeJet, JetScalar};
1272 use crate::nested_dual::JetField;
1273 use std::cell::Cell;
1274
1275 #[test]
1276 fn lower_into_overwrites_every_channel_across_workspace_reset() {
1277 let mut workspace = Order2GraphWorkspace::new();
1278 workspace.reset(3);
1279 let x = Order2Graph::<3>::variable(0.5, 0, 3, &workspace);
1280 let y = Order2Graph::<3>::variable(-0.25, 1, 3, &workspace);
1281 let output = x.product(&y);
1282 let mut gradient = [f64::NAN; 3];
1283 let mut hessian = [17.0; 9];
1284
1285 assert_eq!(output.lower_into(&mut gradient, &mut hessian), -0.125);
1286 assert_eq!(gradient, [-0.25, 0.5, 0.0]);
1287 assert_eq!(hessian, [0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
1288
1289 workspace.reset(3);
1290 let x = Order2Graph::<3>::variable(0.25, 0, 3, &workspace);
1291 let z = Order2Graph::<3>::variable(0.5, 2, 3, &workspace);
1292 let output = Order2Graph::linear_combination(&[x, z], &[2.0, -4.0], 3, &workspace);
1293 gradient.fill(f64::NAN);
1294 hessian.fill(-9.0);
1295
1296 assert_eq!(output.lower_into(&mut gradient, &mut hessian), -1.5);
1297 assert_eq!(gradient, [2.0, 0.0, -4.0]);
1298 assert_eq!(hessian, [0.0; 9]);
1299 }
1300
1301 struct DenseSymmetric3([[f64; 3]; 3]);
1302
1303 impl SymmetricQuadraticCoefficients for DenseSymmetric3 {
1304 fn dimension(&self) -> usize {
1305 3
1306 }
1307
1308 fn multiply(&self, input: &[f64], output: &mut [f64]) {
1309 for row in 0..3 {
1310 output[row] = (0..3)
1311 .map(|column| self.0[row][column] * input[column])
1312 .sum();
1313 }
1314 }
1315
1316 fn coefficient(&self, row: usize, column: usize) -> f64 {
1317 self.0[row][column]
1318 }
1319 }
1320
1321 struct MatrixFreeDense3<'arena> {
1322 matrix: [[f64; 3]; 3],
1323 workspace: &'arena Order2GraphWorkspace,
1324 }
1325
1326 impl SymmetricQuadraticCoefficients for MatrixFreeDense3<'_> {
1327 fn dimension(&self) -> usize {
1328 3
1329 }
1330
1331 fn multiply(&self, input: &[f64], output: &mut [f64]) {
1332 assert!(self.workspace.tape().node_len != 0);
1333 for row in 0..3 {
1334 output[row] = (0..3)
1335 .map(|column| self.matrix[row][column] * input[column])
1336 .sum();
1337 }
1338 }
1339
1340 fn coefficient(&self, _: usize, _: usize) -> f64 {
1341 panic!("compiled graph quadratic lowering must preserve matrix-free multiply")
1342 }
1343 }
1344
1345 struct MatrixFreeIdentity32<'arena> {
1346 workspace: &'arena Order2GraphWorkspace,
1347 }
1348
1349 impl SymmetricQuadraticCoefficients for MatrixFreeIdentity32<'_> {
1350 fn dimension(&self) -> usize {
1351 MAX_QUADRATIC_ARITY
1352 }
1353
1354 fn multiply(&self, input: &[f64], output: &mut [f64]) {
1355 assert!(self.workspace.tape().node_len != 0);
1356 output.copy_from_slice(input);
1357 }
1358
1359 fn coefficient(&self, _: usize, _: usize) -> f64 {
1360 panic!("compiled graph quadratic lowering must preserve matrix-free multiply")
1361 }
1362 }
1363
1364 struct CountingIdentity3<'arena> {
1365 workspace: &'arena Order2GraphWorkspace,
1366 multiply_calls: Cell<usize>,
1367 }
1368
1369 impl SymmetricQuadraticCoefficients for CountingIdentity3<'_> {
1370 fn dimension(&self) -> usize {
1371 3
1372 }
1373
1374 fn multiply(&self, input: &[f64], output: &mut [f64]) {
1375 assert!(self.workspace.tape().node_len != 0);
1376 self.multiply_calls.set(self.multiply_calls.get() + 1);
1377 output.copy_from_slice(input);
1378 }
1379
1380 fn coefficient(&self, _: usize, _: usize) -> f64 {
1381 panic!("compiled graph quadratic lowering must preserve matrix-free multiply")
1382 }
1383 }
1384
1385 #[test]
1386 fn compiled_graph_quadratic_arity_is_independent_and_matrix_free() {
1387 let mut workspace = Order2GraphWorkspace::new();
1388 workspace.reset(2);
1389 let x = Order2Graph::<2>::variable(0.4, 0, 2, &workspace);
1390 let y = Order2Graph::<2>::variable(-0.7, 1, 2, &workspace);
1391 let xy = x.product(&y);
1392 assert_eq!(
1393 workspace.tape().nodes[xy.node].primary_axis,
1394 NO_PRIMARY_AXIS,
1395 "derived quadratic inputs must select the unrestricted Jacobian projection",
1396 );
1397 let graph_linear = Order2Graph::linear_combination(&[x, xy], &[0.3, -0.8], 2, &workspace);
1398 let coefficients = MatrixFreeDense3 {
1399 matrix: [[1.2, -0.3, 0.25], [-0.3, 0.8, 0.17], [0.25, 0.17, 1.4]],
1400 workspace: &workspace,
1401 };
1402 let graph =
1403 Order2Graph::<2>::symmetric_quadratic_form(&[x, y, xy], &coefficients, 2, &workspace)
1404 .into_order2();
1405
1406 let arena = DynamicJetArena::new();
1407 let eager_x = DynamicOrder2::variable(0.4, 0, 2, &arena);
1408 let eager_y = DynamicOrder2::variable(-0.7, 1, 2, &arena);
1409 let eager_xy = eager_x.product(&eager_y);
1410 let eager_linear =
1411 DynamicOrder2::linear_combination(&[eager_x, eager_xy], &[0.3, -0.8], 2, &arena);
1412 let eager = DynamicOrder2::symmetric_quadratic_form(
1413 &[eager_x, eager_y, eager_xy],
1414 &coefficients,
1415 2,
1416 &arena,
1417 );
1418
1419 let close = |actual: f64, expected: f64| {
1420 let tolerance = 2.0e-13 * actual.abs().max(expected.abs()).max(1.0);
1421 assert!((actual - expected).abs() <= tolerance);
1422 };
1423 let graph_linear = graph_linear.into_order2();
1424 close(graph_linear.value(), eager_linear.v);
1425 close(graph.value(), eager.v);
1426 for primary in 0..2 {
1427 close(graph_linear.g()[primary], eager_linear.g()[primary]);
1428 close(graph.g()[primary], eager.g()[primary]);
1429 }
1430 for row in 0..2 {
1431 for column in 0..2 {
1432 close(
1433 graph_linear.h()[row][column],
1434 eager_linear.h_at(row, column),
1435 );
1436 close(graph.h()[row][column], eager.h_at(row, column));
1437 }
1438 }
1439 }
1440
1441 #[test]
1442 fn compiled_graph_primary_quadratic_groups_repeated_permuted_sparse_axes() {
1443 let mut workspace = Order2GraphWorkspace::new();
1444 workspace.reset(4);
1445 let x = Order2Graph::<4>::variable(0.4, 0, 4, &workspace);
1446 let z = Order2Graph::<4>::variable(1.1, 2, 4, &workspace);
1447 let linear = Order2Graph::linear_combination(&[z, x, z], &[0.3, -0.7, 1.1], 4, &workspace)
1448 .into_order2();
1449 let close = |actual: f64, expected: f64| {
1450 let tolerance = 2.0e-13 * actual.abs().max(expected.abs()).max(1.0);
1451 assert!((actual - expected).abs() <= tolerance);
1452 };
1453 assert!((linear.value() - 1.26).abs() <= 2.0e-13);
1454 for (actual, expected) in linear.g().iter().zip([-0.7, 0.0, 1.4, 0.0]) {
1455 close(*actual, expected);
1456 }
1457 assert!(linear.h().iter().flatten().all(|&channel| channel == 0.0));
1458 let coefficients = MatrixFreeDense3 {
1459 matrix: [[1.2, -0.3, 0.25], [-0.3, 0.8, 0.17], [0.25, 0.17, 1.4]],
1460 workspace: &workspace,
1461 };
1462 let graph =
1463 Order2Graph::<4>::symmetric_quadratic_form(&[z, x, z], &coefficients, 4, &workspace)
1464 .into_order2();
1465
1466 let arena = DynamicJetArena::new();
1467 let eager_x = DynamicOrder2::variable(0.4, 0, 4, &arena);
1468 let eager_z = DynamicOrder2::variable(1.1, 2, 4, &arena);
1469 let eager = DynamicOrder2::symmetric_quadratic_form(
1470 &[eager_z, eager_x, eager_z],
1471 &coefficients,
1472 4,
1473 &arena,
1474 );
1475
1476 close(graph.value(), eager.v);
1477 for primary in 0..4 {
1478 close(graph.g()[primary], eager.g()[primary]);
1479 for other in 0..4 {
1480 close(graph.h()[primary][other], eager.h_at(primary, other));
1481 }
1482 }
1483 }
1484
1485 #[test]
1486 fn compiled_graph_accepts_maximum_quadratic_arity_plus_output_node() {
1487 let mut workspace = Order2GraphWorkspace::new();
1488 workspace.reset(MAX_PRIMARY_DIMENSION);
1489 let values: [f64; MAX_QUADRATIC_ARITY] =
1490 std::array::from_fn(|axis| 0.01 * (axis + 1) as f64);
1491 let vars: [Order2Graph<'_, MAX_PRIMARY_DIMENSION>; MAX_QUADRATIC_ARITY] =
1492 std::array::from_fn(|axis| {
1493 Order2Graph::variable(
1494 values[axis],
1495 axis % MAX_PRIMARY_DIMENSION,
1496 MAX_PRIMARY_DIMENSION,
1497 &workspace,
1498 )
1499 });
1500 let coefficients = MatrixFreeIdentity32 {
1501 workspace: &workspace,
1502 };
1503 let graph = Order2Graph::symmetric_quadratic_form(
1504 &vars,
1505 &coefficients,
1506 MAX_PRIMARY_DIMENSION,
1507 &workspace,
1508 )
1509 .into_order2();
1510
1511 let expected_value = values.iter().map(|value| value * value).sum::<f64>();
1512 let tolerance = 2.0e-13;
1513 assert!((graph.value() - expected_value).abs() <= tolerance);
1514 for primary in 0..MAX_PRIMARY_DIMENSION {
1515 let expected_gradient = 2.0 * (values[primary] + values[primary + 16]);
1516 assert!((graph.g()[primary] - expected_gradient).abs() <= tolerance);
1517 for other in 0..MAX_PRIMARY_DIMENSION {
1518 let expected_hessian = if primary == other { 4.0 } else { 0.0 };
1519 assert!((graph.h()[primary][other] - expected_hessian).abs() <= tolerance);
1520 }
1521 }
1522 }
1523
1524 #[test]
1525 fn compiled_graph_projects_only_sparse_supported_primary_directions() {
1526 let mut workspace = Order2GraphWorkspace::new();
1527 workspace.reset(MAX_PRIMARY_DIMENSION);
1528 let x = Order2Graph::<MAX_PRIMARY_DIMENSION>::variable(
1529 0.4,
1530 0,
1531 MAX_PRIMARY_DIMENSION,
1532 &workspace,
1533 );
1534 let y = Order2Graph::<MAX_PRIMARY_DIMENSION>::variable(
1535 -0.7,
1536 7,
1537 MAX_PRIMARY_DIMENSION,
1538 &workspace,
1539 );
1540 let z = Order2Graph::<MAX_PRIMARY_DIMENSION>::variable(
1541 1.1,
1542 15,
1543 MAX_PRIMARY_DIMENSION,
1544 &workspace,
1545 );
1546 let coefficients = CountingIdentity3 {
1547 workspace: &workspace,
1548 multiply_calls: Cell::new(0),
1549 };
1550 let graph = Order2Graph::symmetric_quadratic_form(
1551 &[x, y, z],
1552 &coefficients,
1553 MAX_PRIMARY_DIMENSION,
1554 &workspace,
1555 )
1556 .into_order2();
1557
1558 assert_eq!(coefficients.multiply_calls.get(), 4);
1559 for primary in 0..MAX_PRIMARY_DIMENSION {
1560 let active = matches!(primary, 0 | 7 | 15);
1561 let expected_gradient = match primary {
1562 0 => 0.8,
1563 7 => -1.4,
1564 15 => 2.2,
1565 _ => 0.0,
1566 };
1567 assert_eq!(graph.g()[primary], expected_gradient);
1568 for other in 0..MAX_PRIMARY_DIMENSION {
1569 assert_eq!(
1570 graph.h()[primary][other],
1571 if active && primary == other { 2.0 } else { 0.0 }
1572 );
1573 }
1574 }
1575 }
1576
1577 #[test]
1578 fn compiled_fused_addend_support_obeys_structural_coefficient() {
1579 let mut workspace = Order2GraphWorkspace::new();
1580 {
1581 workspace.reset(3);
1582 let left = Order2Graph::<3>::variable(0.4, 0, 3, &workspace);
1583 let right = Order2Graph::<3>::variable(-0.7, 1, 3, &workspace);
1584 let omitted = Order2Graph::<3>::variable(1.1, 2, 3, &workspace);
1585 let output = Order2Graph::shared_multiply_add_affine_composed_sum(
1586 &[&left],
1587 &right,
1588 &omitted,
1589 &[-0.0],
1590 &[1.0],
1591 &[[0.2, 0.0, 1.0, 0.0, 0.0]],
1592 3,
1593 &workspace,
1594 );
1595 assert_eq!(workspace.tape().nodes[output.node].support, 0b011);
1596 let channels = output.into_order2();
1597 assert!(channels.g()[2] == 0.0);
1598 assert!((0..3).all(|axis| channels.h()[axis][2] == 0.0));
1599 }
1600
1601 workspace.reset(3);
1602 let left = Order2Graph::<3>::variable(0.4, 0, 3, &workspace);
1603 let right = Order2Graph::<3>::variable(-0.7, 1, 3, &workspace);
1604 let live = Order2Graph::<3>::variable(1.1, 2, 3, &workspace);
1605 let output = Order2Graph::shared_multiply_add_affine_composed_sum(
1606 &[&left],
1607 &right,
1608 &live,
1609 &[1.0],
1610 &[1.0],
1611 &[[0.2, 0.0, 1.0, 0.0, 0.0]],
1612 3,
1613 &workspace,
1614 );
1615 assert_eq!(workspace.tape().nodes[output.node].support, 0b111);
1616 let channels = output.into_order2();
1617 assert_eq!(channels.g()[2], 0.0);
1618 assert_eq!(channels.h()[2][2], 1.0);
1619 }
1620
1621 fn mixed_primary_derived_fused_expression<'arena, S: RuntimeJetScalar<'arena>>(
1622 vars: &[S; 5],
1623 workspace: &'arena S::Workspace,
1624 ) -> S {
1625 let right = vars[4].affine_compose(1.2, -0.1, [0.7, -1.2, 0.45, 0.0, 0.0], workspace);
1626 let derived_left = vars[2].multiply_add(&vars[3], &vars[0]);
1627 let addend = S::linear_combination(vars, &[0.3, -0.8, 0.5, 1.1, -0.4], 5, workspace);
1628 S::shared_multiply_add_affine_composed_sum(
1629 &[&vars[1], &vars[0], &vars[1], &derived_left],
1630 &right,
1631 &addend,
1632 &[1.0, 1.0, 1.0, -0.4],
1633 &[-1.0, -1.0, 1.0, 0.75],
1634 &[
1635 [0.4, -0.8, 0.3, 0.0, 0.0],
1636 [-0.2, 0.5, -0.7, 0.0, 0.0],
1637 [0.9, 1.1, 0.2, 0.0, 0.0],
1638 [-0.6, 0.4, 0.8, 0.0, 0.0],
1639 ],
1640 5,
1641 workspace,
1642 )
1643 }
1644
1645 #[test]
1646 fn compiled_fused_primary_scatter_matches_eager_with_mixed_lefts() {
1647 let values = [0.4, -0.7, 1.1, -0.3, 0.8];
1648 let eager_vars: [FixedRuntimeJet<Order2<5>, 5>; 5] = std::array::from_fn(|axis| {
1649 FixedRuntimeJet::from_inner(Order2::variable(values[axis], axis))
1650 });
1651 let eager = mixed_primary_derived_fused_expression(&eager_vars, &()).into_inner();
1652
1653 let mut workspace = Order2GraphWorkspace::new();
1654 workspace.reset(5);
1655 let graph_vars: [Order2Graph<'_, 5>; 5] =
1656 std::array::from_fn(|axis| Order2Graph::variable(values[axis], axis, 5, &workspace));
1657 let graph_output = mixed_primary_derived_fused_expression(&graph_vars, &workspace);
1658 let output_node = workspace.tape().nodes[graph_output.node];
1659 assert_eq!(output_node.edge_len, 5);
1660 let graph = graph_output.into_order2();
1661
1662 let close = |actual: f64, expected: f64| {
1663 let tolerance = 2.0e-12 * actual.abs().max(expected.abs()).max(1.0);
1664 assert!((actual - expected).abs() <= tolerance);
1665 };
1666 close(graph.value(), eager.value());
1667 for primary in 0..5 {
1668 close(graph.g()[primary], eager.g()[primary]);
1669 for other in 0..5 {
1670 close(graph.h()[primary][other], eager.h()[primary][other]);
1671 }
1672 }
1673 }
1674
1675 fn expression<'arena, S: RuntimeJetScalar<'arena>>(
1676 vars: &[S; 6],
1677 coefficients: &DenseSymmetric3,
1678 scales: &[f64; 4],
1679 stacks: &[[f64; 5]; 4],
1680 workspace: &'arena S::Workspace,
1681 ) -> S {
1682 let nonlinear = [
1683 vars[0].product(&vars[1]),
1684 vars[2].affine_compose(scales[0], scales[1], stacks[0], workspace),
1685 vars[3].multiply_add(&vars[4], &vars[5]),
1686 ];
1687 let quadratic = S::symmetric_quadratic_form(&nonlinear, coefficients, 6, workspace);
1688 let linear = S::linear_combination(vars, &[0.2, -0.7, 1.1, 0.4, -0.3, 0.8], 6, workspace);
1689 let product = quadratic.product(&linear);
1690 S::affine_composed_sum(
1691 &[quadratic, linear, product, nonlinear[1].clone()],
1692 &[scales[0], scales[1], scales[2], scales[3]],
1693 stacks,
1694 6,
1695 workspace,
1696 )
1697 }
1698
1699 fn fused_expression<'arena, S: RuntimeJetScalar<'arena>>(
1700 vars: &[S; 6],
1701 scales: &[f64; 4],
1702 stacks: &[[f64; 5]; 4],
1703 workspace: &'arena S::Workspace,
1704 ) -> S {
1705 const N: usize = 10;
1706 let upstream = [
1707 vars[0].product(&vars[1]),
1708 vars[2].affine_compose(scales[0], scales[1], stacks[0], workspace),
1709 vars[3].multiply_add(&vars[4], &vars[5]),
1710 ];
1711 let repeated_left = upstream[1].clone();
1712 let mut lefts: [&S; N] = std::array::from_fn(|term| &upstream[term % upstream.len()]);
1713 lefts[9] = &repeated_left;
1714 let right = &upstream[1];
1715 let addend = &vars[2];
1716 let addend_scales: [f64; N] = std::array::from_fn(|term| match term % 4 {
1717 0 => 0.0,
1718 1 => 1.0,
1719 2 => -0.75,
1720 _ => 0.35,
1721 });
1722 let mut input_scales: [f64; N] = std::array::from_fn(|term| match term {
1723 0 => 0.0,
1724 1 => -1.25,
1725 _ => scales[term % scales.len()],
1726 });
1727 input_scales[9] = 1.25;
1728 let derivative_stacks: [[f64; 5]; N] =
1729 std::array::from_fn(|term| stacks[term % stacks.len()]);
1730 S::shared_multiply_add_affine_composed_sum(
1731 &lefts,
1732 right,
1733 addend,
1734 &addend_scales,
1735 &input_scales,
1736 &derivative_stacks,
1737 6,
1738 workspace,
1739 )
1740 }
1741
1742 #[test]
1743 fn compiled_graph_matches_eager_order2_randomized_full_vgh() {
1744 fn sample(state: &mut u64) -> f64 {
1745 *state ^= *state << 13;
1746 *state ^= *state >> 7;
1747 *state ^= *state << 17;
1748 let unit = (*state >> 11) as f64 * (1.0 / ((1_u64 << 53) as f64));
1749 2.0 * unit - 1.0
1750 }
1751
1752 fn close(actual: f64, expected: f64, case: usize, label: &str) {
1753 let tolerance = 5.0e-12 * actual.abs().max(expected.abs()).max(1.0);
1754 assert!(
1755 (actual - expected).abs() <= tolerance,
1756 "case {case} {label}: graph={actual:+.16e}, eager={expected:+.16e}, tolerance={tolerance:.3e}"
1757 );
1758 }
1759
1760 let mut state = 0x932d_a660_5eed_f00d_u64;
1761 let mut workspace = Order2GraphWorkspace::new();
1762 let mut fused_workspace = Order2GraphWorkspace::new();
1763
1764 fused_workspace.reset(6);
1765 let empty_terms: [&Order2Graph<'_, 6>; 0] = [];
1766 let empty_shared = Order2Graph::constant(1.0, 6, &fused_workspace);
1767 let empty_scales: [f64; 0] = [];
1768 let empty_stacks: [[f64; 5]; 0] = [];
1769 let empty = Order2Graph::shared_multiply_add_affine_composed_sum(
1770 &empty_terms,
1771 &empty_shared,
1772 &empty_shared,
1773 &empty_scales,
1774 &empty_scales,
1775 &empty_stacks,
1776 6,
1777 &fused_workspace,
1778 )
1779 .into_order2();
1780 assert_eq!(empty.value().to_bits(), 0.0_f64.to_bits());
1781 assert!(empty.g().iter().all(|&channel| channel == 0.0));
1782 assert!(empty.h().iter().flatten().all(|&channel| channel == 0.0));
1783
1784 for case in 0..256 {
1785 let values: [f64; 6] = std::array::from_fn(|_| sample(&mut state));
1786 let scales: [f64; 4] = std::array::from_fn(|_| sample(&mut state));
1787 let stacks: [[f64; 5]; 4] =
1788 std::array::from_fn(|_| std::array::from_fn(|_| sample(&mut state)));
1789 let raw: [[f64; 3]; 3] =
1790 std::array::from_fn(|_| std::array::from_fn(|_| sample(&mut state)));
1791 let coefficients = DenseSymmetric3([
1792 [raw[0][0], raw[0][1], raw[0][2]],
1793 [raw[0][1], raw[1][1], raw[1][2]],
1794 [raw[0][2], raw[1][2], raw[2][2]],
1795 ]);
1796
1797 let eager_vars: [FixedRuntimeJet<Order2<6>, 6>; 6] = std::array::from_fn(|axis| {
1798 FixedRuntimeJet::from_inner(Order2::variable(values[axis], axis))
1799 });
1800 let eager = expression(&eager_vars, &coefficients, &scales, &stacks, &()).into_inner();
1801 let eager_fused = fused_expression(&eager_vars, &scales, &stacks, &()).into_inner();
1802
1803 workspace.reset(6);
1804 let graph_vars: [Order2Graph<'_, 6>; 6] = std::array::from_fn(|axis| {
1805 Order2Graph::variable(values[axis], axis, 6, &workspace)
1806 });
1807 let graph =
1808 expression(&graph_vars, &coefficients, &scales, &stacks, &workspace).into_order2();
1809 fused_workspace.reset(6);
1810 let fused_graph_vars: [Order2Graph<'_, 6>; 6] = std::array::from_fn(|axis| {
1811 Order2Graph::variable(values[axis], axis, 6, &fused_workspace)
1812 });
1813 let graph_fused =
1814 fused_expression(&fused_graph_vars, &scales, &stacks, &fused_workspace)
1815 .into_order2();
1816
1817 close(graph.value(), eager.value(), case, "value");
1818 close(
1819 graph_fused.value(),
1820 eager_fused.value(),
1821 case,
1822 "fused value",
1823 );
1824 for primary in 0..6 {
1825 close(graph.g()[primary], eager.g()[primary], case, "gradient");
1826 close(
1827 graph_fused.g()[primary],
1828 eager_fused.g()[primary],
1829 case,
1830 "fused gradient",
1831 );
1832 for other in 0..6 {
1833 close(
1834 graph.h()[primary][other],
1835 eager.h()[primary][other],
1836 case,
1837 "Hessian",
1838 );
1839 close(
1840 graph_fused.h()[primary][other],
1841 eager_fused.h()[primary][other],
1842 case,
1843 "fused Hessian",
1844 );
1845 }
1846 }
1847 }
1848 }
1849}