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 constant_like(&self, c: f64) -> Self {
603 Self::constant(c, K, self.workspace)
604 }
605
606 #[inline(always)]
607 fn with_value(&self, value: f64) -> Self {
608 self.unary(value, 1.0, 0.0)
609 }
610
611 #[inline(always)]
612 fn symmetric_quadratic_form<C: SymmetricQuadraticCoefficients>(
613 inputs: &[Self],
614 coefficients: &C,
615 dimension: usize,
616 workspace: &'arena Self::Workspace,
617 ) -> Self {
618 assert_eq!(dimension, K, "compiled graph dimension mismatch");
619 assert_eq!(inputs.len(), coefficients.dimension());
620 assert!(
621 inputs.len() <= MAX_QUADRATIC_ARITY,
622 "compiled graph quadratic arity exceeds graph capacity"
623 );
624 assert!(
625 inputs
626 .iter()
627 .all(|input| std::ptr::eq(input.workspace, workspace)),
628 "compiled graph quadratic inputs belong to different workspaces"
629 );
630
631 let input_dimension = inputs.len();
632 let mut values_storage = MaybeUninit::uninit();
633 let values = InlineScalars::initialize_zeros(&mut values_storage, input_dimension);
634 let mut support = 0_u16;
635 {
636 let tape = workspace.tape();
637 for (value, input) in values.as_mut_slice().iter_mut().zip(inputs) {
638 *value = tape.nodes[input.node].value;
639 support |= tape.nodes[input.node].support;
640 }
641 }
642 let mut input_primary_axes = [NO_PRIMARY_AXIS; MAX_QUADRATIC_ARITY];
648 let all_inputs_primary = {
649 let tape = workspace.tape();
650 inputs.iter().enumerate().all(|(position, input)| {
651 let primary_axis = tape.nodes[input.node].primary_axis;
652 if primary_axis == NO_PRIMARY_AXIS {
653 false
654 } else {
655 input_primary_axes[position] = primary_axis;
656 true
657 }
658 })
659 };
660 let mut projected_values_storage = MaybeUninit::uninit();
661 let projected_values =
662 InlineScalars::initialize_zeros(&mut projected_values_storage, input_dimension);
663 coefficients.multiply(values.as_slice(), projected_values.as_mut_slice());
664 let value = values
665 .as_slice()
666 .iter()
667 .zip(projected_values.as_slice())
668 .map(|(&input, &projected)| input * projected)
669 .sum();
670
671 let supported_dimension = support.count_ones() as usize;
672 let curvature_len = supported_dimension * (supported_dimension + 1) / 2;
673 let curvature_start = {
674 let tape = workspace.tape_mut();
675 assert!(
676 curvature_len <= MAX_PROJECTED_CURVATURE_VALUES - tape.projected_curvature_len,
677 "compiled graph projected-curvature capacity exceeded"
678 );
679 let start = tape.projected_curvature_len;
680 tape.projected_curvature_len += curvature_len;
681 start
682 };
683 let mut curvature_offset = 0;
684 if all_inputs_primary {
685 workspace.tape_mut().projected_curvatures
690 [curvature_start..curvature_start + curvature_len]
691 .fill(0.0);
692 let mut direction_storage = MaybeUninit::uninit();
693 let direction =
694 InlineScalars::initialize_zeros(&mut direction_storage, input_dimension);
695 let mut projected_direction_storage = MaybeUninit::uninit();
696 let projected_direction =
697 InlineScalars::initialize_zeros(&mut projected_direction_storage, input_dimension);
698 coefficients.visit_upper_triangle(
699 direction.as_mut_slice(),
700 projected_direction.as_mut_slice(),
701 |row, column, coefficient| {
702 let row_axis = input_primary_axes[row] as usize;
703 let column_axis = input_primary_axes[column] as usize;
704 let primary = row_axis.min(column_axis);
705 let other = row_axis.max(column_axis);
706 let offset = supported_upper_offset(support, primary, other);
707 let repeated_axis_multiplicity = if row != column && row_axis == column_axis {
708 2.0
709 } else {
710 1.0
711 };
712 workspace.tape_mut().projected_curvatures[curvature_start + offset] +=
713 2.0 * repeated_axis_multiplicity * coefficient;
714 },
715 );
716 curvature_offset = curvature_len;
717 } else {
718 let mut direction_storage = MaybeUninit::uninit();
723 let direction =
724 InlineScalars::initialize_zeros(&mut direction_storage, input_dimension);
725 let mut projected_direction_storage = MaybeUninit::uninit();
726 let projected_direction =
727 InlineScalars::initialize_zeros(&mut projected_direction_storage, input_dimension);
728 let mut supported_columns = support;
729 while supported_columns != 0 {
730 let other = supported_columns.trailing_zeros() as usize;
731 supported_columns &= supported_columns - 1;
732 {
733 let tape = workspace.tape();
734 for (channel, input) in direction.as_mut_slice().iter_mut().zip(inputs) {
735 *channel = tape.gradients[input.node * K + other];
736 }
737 }
738 coefficients.multiply(direction.as_slice(), projected_direction.as_mut_slice());
739 let mut supported_rows =
740 support & (u16::MAX >> (MAX_PRIMARY_DIMENSION - other - 1));
741 while supported_rows != 0 {
742 let primary = supported_rows.trailing_zeros() as usize;
743 supported_rows &= supported_rows - 1;
744 let curvature = {
745 let tape = workspace.tape();
746 inputs
747 .iter()
748 .zip(projected_direction.as_slice())
749 .map(|(input, &projected)| {
750 tape.gradients[input.node * K + primary] * projected
751 })
752 .sum::<f64>()
753 };
754 workspace.tape_mut().projected_curvatures[curvature_start + curvature_offset] =
755 2.0 * curvature;
756 curvature_offset += 1;
757 }
758 }
759 }
760 assert_eq!(
761 curvature_offset, curvature_len,
762 "compiled graph projected-curvature schedule must fill its exact packed range"
763 );
764
765 let tape = workspace.tape_mut();
766 let owner = tape.node_len;
767 let edge_start = tape.edge_len;
768 let mut gradient = [0.0; K];
769 for axis in 0..input_dimension {
770 let first = 2.0 * projected_values.as_slice()[axis];
771 Order2GraphWorkspace::push_edge(tape, inputs[axis].node, first);
772 if all_inputs_primary {
773 let primary = tape.nodes[inputs[axis].node].primary_axis as usize;
774 gradient[primary] += first;
775 } else {
776 for primary in 0..K {
777 gradient[primary] += first * tape.gradients[inputs[axis].node * K + primary];
778 }
779 }
780 }
781 Order2GraphWorkspace::push_event(
782 tape,
783 CurvatureEvent::Projected {
784 owner: owner as u8,
785 curvature_start: curvature_start as u16,
786 support,
787 },
788 );
789 let node = Order2GraphWorkspace::push(tape, value, support, gradient, edge_start);
790 Self { workspace, node }
791 }
792
793 #[inline(always)]
794 fn linear_combination(
795 inputs: &[Self],
796 weights: &[f64],
797 dimension: usize,
798 workspace: &'arena Self::Workspace,
799 ) -> Self {
800 assert_eq!(dimension, K, "compiled graph dimension mismatch");
801 assert_eq!(inputs.len(), weights.len());
802 assert!(
803 inputs
804 .iter()
805 .all(|input| std::ptr::eq(input.workspace, workspace)),
806 "compiled graph linear inputs belong to different workspaces"
807 );
808 let tape = workspace.tape_mut();
809 let all_inputs_primary = inputs
810 .iter()
811 .all(|input| tape.nodes[input.node].primary_axis != NO_PRIMARY_AXIS);
812 let edge_start = tape.edge_len;
813 let mut value = 0.0;
814 let mut gradient = [0.0; K];
815 let mut support = 0_u16;
816 for (input, &weight) in inputs.iter().zip(weights) {
817 value += weight * tape.nodes[input.node].value;
818 Order2GraphWorkspace::push_edge(tape, input.node, weight);
819 support |= tape.nodes[input.node].support;
820 if all_inputs_primary {
821 let primary = tape.nodes[input.node].primary_axis as usize;
822 gradient[primary] += weight;
823 } else {
824 for primary in 0..K {
825 gradient[primary] += weight * tape.gradients[input.node * K + primary];
826 }
827 }
828 }
829 let node = Order2GraphWorkspace::push(tape, value, support, gradient, edge_start);
830 Self { workspace, node }
831 }
832
833 #[inline(always)]
834 fn multiply_add(&self, right: &Self, addend: &Self) -> Self {
835 self.assert_compatible(right);
836 self.assert_compatible(addend);
837 let tape = self.workspace.tape_mut();
838 let left_value = tape.nodes[self.node].value;
839 let right_value = tape.nodes[right.node].value;
840 let mut gradient = [0.0; K];
841 for primary in 0..K {
842 gradient[primary] = left_value * tape.gradients[right.node * K + primary]
843 + tape.gradients[self.node * K + primary] * right_value
844 + tape.gradients[addend.node * K + primary];
845 }
846 let value = left_value * right_value + tape.nodes[addend.node].value;
847 let support = tape.nodes[self.node].support
848 | tape.nodes[right.node].support
849 | tape.nodes[addend.node].support;
850 let owner = tape.node_len;
851 let edge_start = tape.edge_len;
852 Order2GraphWorkspace::push_edge(tape, self.node, right_value);
853 Order2GraphWorkspace::push_edge(tape, right.node, left_value);
854 Order2GraphWorkspace::push_edge(tape, addend.node, 1.0);
855 Order2GraphWorkspace::push_event(
856 tape,
857 CurvatureEvent::Cross {
858 owner: owner as u8,
859 left: self.node as u8,
860 right: right.node as u8,
861 },
862 );
863 let node = Order2GraphWorkspace::push(tape, value, support, gradient, edge_start);
864 Self {
865 workspace: self.workspace,
866 node,
867 }
868 }
869
870 #[inline(always)]
871 fn composed_sum(
872 inputs: &[Self],
873 derivative_stacks: &[[f64; 5]],
874 dimension: usize,
875 workspace: &'arena Self::Workspace,
876 ) -> Self {
877 assert_eq!(dimension, K, "compiled graph dimension mismatch");
878 assert_eq!(inputs.len(), derivative_stacks.len());
879 assert!(
880 inputs
881 .iter()
882 .all(|input| std::ptr::eq(input.workspace, workspace)),
883 "compiled graph composed inputs belong to different workspaces"
884 );
885 let tape = workspace.tape_mut();
886 let owner = tape.node_len;
887 let edge_start = tape.edge_len;
888 let term_start = tape.diagonal_len;
889 let mut value = 0.0;
890 let mut gradient = [0.0; K];
891 let mut support = 0_u16;
892 for (input, stack) in inputs.iter().zip(derivative_stacks) {
893 value += stack[0];
894 Order2GraphWorkspace::push_edge(tape, input.node, stack[1]);
895 Order2GraphWorkspace::push_diagonal_term(tape, input.node, stack[2]);
896 support |= tape.nodes[input.node].support;
897 for primary in 0..K {
898 gradient[primary] += stack[1] * tape.gradients[input.node * K + primary];
899 }
900 }
901 Order2GraphWorkspace::push_event(
902 tape,
903 CurvatureEvent::Diagonal {
904 owner: owner as u8,
905 term_start: term_start as u16,
906 len: inputs.len() as u16,
907 },
908 );
909 let node = Order2GraphWorkspace::push(tape, value, support, gradient, edge_start);
910 Self { workspace, node }
911 }
912
913 #[inline(always)]
914 fn product(&self, right: &Self) -> Self {
915 self.mul(right)
916 }
917
918 #[inline(always)]
919 fn affine_compose(
920 &self,
921 input_scale: f64,
922 input_shift: f64,
923 derivative_stack: [f64; 5],
924 ) -> Self {
925 assert!(input_shift.is_finite(), "affine input shift must be finite");
926 self.unary(
927 derivative_stack[0],
928 derivative_stack[1] * input_scale,
929 derivative_stack[2] * input_scale * input_scale,
930 )
931 }
932
933 #[inline(always)]
934 fn affine_composed_sum(
935 inputs: &[Self],
936 input_scales: &[f64],
937 derivative_stacks: &[[f64; 5]],
938 dimension: usize,
939 workspace: &'arena Self::Workspace,
940 ) -> Self {
941 assert_eq!(dimension, K, "compiled graph dimension mismatch");
942 assert_eq!(inputs.len(), input_scales.len());
943 assert_eq!(inputs.len(), derivative_stacks.len());
944 assert!(
945 inputs
946 .iter()
947 .all(|input| std::ptr::eq(input.workspace, workspace)),
948 "compiled graph composed inputs belong to different workspaces"
949 );
950 let tape = workspace.tape_mut();
951 let owner = tape.node_len;
952 let edge_start = tape.edge_len;
953 let term_start = tape.diagonal_len;
954 let mut value = 0.0;
955 let mut gradient = [0.0; K];
956 let mut support = 0_u16;
957 for ((input, &input_scale), stack) in inputs.iter().zip(input_scales).zip(derivative_stacks)
958 {
959 let first = stack[1] * input_scale;
960 let second = stack[2] * input_scale * input_scale;
961 value += stack[0];
962 Order2GraphWorkspace::push_edge(tape, input.node, first);
963 Order2GraphWorkspace::push_diagonal_term(tape, input.node, second);
964 support |= tape.nodes[input.node].support;
965 for primary in 0..K {
966 gradient[primary] += first * tape.gradients[input.node * K + primary];
967 }
968 }
969 Order2GraphWorkspace::push_event(
970 tape,
971 CurvatureEvent::Diagonal {
972 owner: owner as u8,
973 term_start: term_start as u16,
974 len: inputs.len() as u16,
975 },
976 );
977 let node = Order2GraphWorkspace::push(tape, value, support, gradient, edge_start);
978 Self { workspace, node }
979 }
980
981 #[inline(always)]
982 fn shared_multiply_add_affine_composed_sum<const N: usize>(
983 lefts: &[&Self; N],
984 right: &Self,
985 addend: &Self,
986 addend_scales: &[f64; N],
987 input_scales: &[f64; N],
988 derivative_stacks: &[[f64; 5]; N],
989 dimension: usize,
990 workspace: &'arena Self::Workspace,
991 ) -> Self {
992 assert_eq!(dimension, K, "compiled graph dimension mismatch");
993 assert!(
994 lefts
995 .iter()
996 .all(|input| std::ptr::eq(input.workspace, workspace))
997 && (N == 0 || std::ptr::eq(right.workspace, workspace)),
998 "compiled fused product-composition inputs belong to different workspaces"
999 );
1000 let addend_live = addend_scales.iter().any(|&scale| scale != 0.0);
1001 assert!(
1002 !addend_live || std::ptr::eq(addend.workspace, workspace),
1003 "live compiled fused addends belong to different workspaces"
1004 );
1005
1006 let tape = workspace.tape_mut();
1007 let right_node = right.node;
1008 let addend_node = addend.node;
1009 let (representatives, term_sources, source_count) =
1010 canonical_shared_source_schedule::<N>(|term, representative| {
1011 lefts[term].node == lefts[representative].node
1012 && addend_scales[term] == addend_scales[representative]
1013 });
1014 let (value, source_derivatives) =
1015 aggregate_shared_source_derivatives(&term_sources, input_scales, derivative_stacks);
1016 let mut source_gradients = [[0.0; K]; N];
1017 let mut source_primary_axes = [NO_PRIMARY_AXIS; N];
1018 let mut gradient = [0.0; K];
1019 let mut support = 0_u16;
1020 let mut right_first = 0.0;
1021 let mut addend_first = 0.0;
1022 if N != 0 {
1023 support |= tape.nodes[right_node].support;
1024 }
1025 if addend_live {
1026 support |= tape.nodes[addend_node].support;
1027 }
1028 for source in 0..source_count {
1029 let term = representatives[source];
1030 let left = lefts[term].node;
1031 let left_value = tape.nodes[left].value;
1032 let right_value = tape.nodes[right_node].value;
1033 let first = source_derivatives[source][1];
1034 let left_primary_axis = tape.nodes[left].primary_axis;
1035 source_primary_axes[source] = left_primary_axis;
1036 support |= tape.nodes[left].support;
1037 right_first += first * left_value;
1038 addend_first += first * addend_scales[term];
1039 for primary in 0..K {
1040 let product_gradient = left_value * tape.gradients[right_node * K + primary];
1041 let inner_gradient = if addend_scales[term] == 0.0 {
1042 product_gradient
1043 } else if addend_scales[term] == 1.0 {
1044 product_gradient + tape.gradients[addend_node * K + primary]
1045 } else {
1046 product_gradient
1047 + addend_scales[term] * tape.gradients[addend_node * K + primary]
1048 };
1049 source_gradients[source][primary] = inner_gradient;
1050 }
1051 if left_primary_axis == NO_PRIMARY_AXIS {
1052 for primary in 0..K {
1053 let left_contribution = tape.gradients[left * K + primary] * right_value;
1054 source_gradients[source][primary] += left_contribution;
1055 gradient[primary] += first * left_contribution;
1056 }
1057 } else {
1058 let primary = left_primary_axis as usize;
1059 source_gradients[source][primary] += right_value;
1060 gradient[primary] += first * right_value;
1061 }
1062 }
1063 if N != 0 {
1064 for primary in 0..K {
1065 gradient[primary] += right_first * tape.gradients[right_node * K + primary];
1066 }
1067 }
1068 if addend_live {
1069 for primary in 0..K {
1070 gradient[primary] += addend_first * tape.gradients[addend_node * K + primary];
1071 }
1072 }
1073
1074 let supported_dimension = support.count_ones() as usize;
1075 let curvature_len = supported_dimension * (supported_dimension + 1) / 2;
1076 assert!(
1077 curvature_len <= MAX_PROJECTED_CURVATURE_VALUES - tape.projected_curvature_len,
1078 "compiled graph projected-curvature capacity exceeded"
1079 );
1080 let curvature_start = tape.projected_curvature_len;
1081 tape.projected_curvature_len += curvature_len;
1082 let mut curvature_offset = 0;
1083 for_each_supported_upper_by_column(support, |primary, other| {
1084 let mut channel = 0.0;
1085 for source in 0..source_count {
1086 channel += source_derivatives[source][2]
1087 * source_gradients[source][primary]
1088 * source_gradients[source][other];
1089 if source_primary_axes[source] == NO_PRIMARY_AXIS {
1090 let term = representatives[source];
1091 let left = lefts[term].node;
1092 let cross = tape.gradients[left * K + primary]
1093 * tape.gradients[right_node * K + other]
1094 + tape.gradients[left * K + other]
1095 * tape.gradients[right_node * K + primary];
1096 channel += source_derivatives[source][1] * cross;
1097 }
1098 }
1099 tape.projected_curvatures[curvature_start + curvature_offset] = channel;
1100 curvature_offset += 1;
1101 });
1102 for source in 0..source_count {
1103 let primary_axis = source_primary_axes[source];
1104 if primary_axis == NO_PRIMARY_AXIS {
1105 continue;
1106 }
1107 let primary_axis = primary_axis as usize;
1108 let first = source_derivatives[source][1];
1109 let mut right_support = tape.nodes[right_node].support;
1110 while right_support != 0 {
1111 let right_axis = right_support.trailing_zeros() as usize;
1112 right_support &= right_support - 1;
1113 let primary = primary_axis.min(right_axis);
1114 let other = primary_axis.max(right_axis);
1115 let offset = supported_upper_offset(support, primary, other);
1116 let diagonal_scale = if primary_axis == right_axis { 2.0 } else { 1.0 };
1117 let cross = diagonal_scale * tape.gradients[right_node * K + right_axis];
1118 tape.projected_curvatures[curvature_start + offset] += first * cross;
1119 }
1120 }
1121 assert_eq!(
1122 curvature_offset, curvature_len,
1123 "compiled fused product-composition must fill its packed curvature"
1124 );
1125
1126 let owner = tape.node_len;
1127 let edge_start = tape.edge_len;
1128 for source in 0..source_count {
1129 let term = representatives[source];
1130 let left = lefts[term].node;
1131 let left_first = source_derivatives[source][1] * tape.nodes[right_node].value;
1132 Order2GraphWorkspace::push_edge(tape, left, left_first);
1133 }
1134 if N != 0 {
1135 Order2GraphWorkspace::push_edge(tape, right_node, right_first);
1136 }
1137 if addend_live {
1138 Order2GraphWorkspace::push_edge(tape, addend_node, addend_first);
1139 }
1140 Order2GraphWorkspace::push_event(
1141 tape,
1142 CurvatureEvent::Projected {
1143 owner: owner as u8,
1144 curvature_start: curvature_start as u16,
1145 support,
1146 },
1147 );
1148 let node = Order2GraphWorkspace::push(tape, value, support, gradient, edge_start);
1149 Self { workspace, node }
1150 }
1151
1152 #[inline(always)]
1153 fn dimension(&self) -> usize {
1154 K
1155 }
1156
1157 #[inline(always)]
1158 fn value(&self) -> f64 {
1159 self.workspace.tape().nodes[self.node].value
1160 }
1161
1162 #[inline(always)]
1163 fn add(&self, other: &Self) -> Self {
1164 self.assert_compatible(other);
1165 let tape = self.workspace.tape_mut();
1166 let mut gradient = [0.0; K];
1167 for primary in 0..K {
1168 gradient[primary] =
1169 tape.gradients[self.node * K + primary] + tape.gradients[other.node * K + primary];
1170 }
1171 let value = tape.nodes[self.node].value + tape.nodes[other.node].value;
1172 let support = tape.nodes[self.node].support | tape.nodes[other.node].support;
1173 let edge_start = tape.edge_len;
1174 Order2GraphWorkspace::push_edge(tape, self.node, 1.0);
1175 Order2GraphWorkspace::push_edge(tape, other.node, 1.0);
1176 let node = Order2GraphWorkspace::push(tape, value, support, gradient, edge_start);
1177 Self {
1178 workspace: self.workspace,
1179 node,
1180 }
1181 }
1182
1183 #[inline(always)]
1184 fn sub(&self, other: &Self) -> Self {
1185 self.assert_compatible(other);
1186 let tape = self.workspace.tape_mut();
1187 let mut gradient = [0.0; K];
1188 for primary in 0..K {
1189 gradient[primary] =
1190 tape.gradients[self.node * K + primary] - tape.gradients[other.node * K + primary];
1191 }
1192 let value = tape.nodes[self.node].value - tape.nodes[other.node].value;
1193 let support = tape.nodes[self.node].support | tape.nodes[other.node].support;
1194 let edge_start = tape.edge_len;
1195 Order2GraphWorkspace::push_edge(tape, self.node, 1.0);
1196 Order2GraphWorkspace::push_edge(tape, other.node, -1.0);
1197 let node = Order2GraphWorkspace::push(tape, value, support, gradient, edge_start);
1198 Self {
1199 workspace: self.workspace,
1200 node,
1201 }
1202 }
1203
1204 #[inline(always)]
1205 fn mul(&self, other: &Self) -> Self {
1206 self.assert_compatible(other);
1207 let tape = self.workspace.tape_mut();
1208 let left_value = tape.nodes[self.node].value;
1209 let right_value = tape.nodes[other.node].value;
1210 let mut gradient = [0.0; K];
1211 for primary in 0..K {
1212 gradient[primary] = left_value * tape.gradients[other.node * K + primary]
1213 + tape.gradients[self.node * K + primary] * right_value;
1214 }
1215 let support = tape.nodes[self.node].support | tape.nodes[other.node].support;
1216 let owner = tape.node_len;
1217 let edge_start = tape.edge_len;
1218 Order2GraphWorkspace::push_edge(tape, self.node, right_value);
1219 Order2GraphWorkspace::push_edge(tape, other.node, left_value);
1220 Order2GraphWorkspace::push_event(
1221 tape,
1222 CurvatureEvent::Cross {
1223 owner: owner as u8,
1224 left: self.node as u8,
1225 right: other.node as u8,
1226 },
1227 );
1228 let node = Order2GraphWorkspace::push(
1229 tape,
1230 left_value * right_value,
1231 support,
1232 gradient,
1233 edge_start,
1234 );
1235 Self {
1236 workspace: self.workspace,
1237 node,
1238 }
1239 }
1240
1241 #[inline(always)]
1242 fn neg(&self) -> Self {
1243 self.unary(-self.value(), -1.0, 0.0)
1244 }
1245
1246 #[inline(always)]
1247 fn scale(&self, scale: f64) -> Self {
1248 let tape = self.workspace.tape_mut();
1249 let mut gradient = [0.0; K];
1250 for primary in 0..K {
1251 gradient[primary] = scale * tape.gradients[self.node * K + primary];
1252 }
1253 let value = scale * tape.nodes[self.node].value;
1254 let support = tape.nodes[self.node].support;
1255 let edge_start = tape.edge_len;
1256 Order2GraphWorkspace::push_edge(tape, self.node, scale);
1257 let node = Order2GraphWorkspace::push(tape, value, support, gradient, edge_start);
1258 Self {
1259 workspace: self.workspace,
1260 node,
1261 }
1262 }
1263
1264 #[inline(always)]
1265 fn compose_unary(&self, derivatives: [f64; 5]) -> Self {
1266 self.unary(derivatives[0], derivatives[1], derivatives[2])
1267 }
1268}
1269
1270#[cfg(test)]
1271mod tests {
1272 use super::*;
1273 use crate::jet_scalar::{DynamicJetArena, DynamicOrder2, FixedRuntimeJet, JetScalar};
1274 use crate::nested_dual::JetField;
1275 use std::cell::Cell;
1276
1277 #[test]
1278 fn lower_into_overwrites_every_channel_across_workspace_reset() {
1279 let mut workspace = Order2GraphWorkspace::new();
1280 workspace.reset(3);
1281 let x = Order2Graph::<3>::variable(0.5, 0, 3, &workspace);
1282 let y = Order2Graph::<3>::variable(-0.25, 1, 3, &workspace);
1283 let output = x.product(&y);
1284 let mut gradient = [f64::NAN; 3];
1285 let mut hessian = [17.0; 9];
1286
1287 assert_eq!(output.lower_into(&mut gradient, &mut hessian), -0.125);
1288 assert_eq!(gradient, [-0.25, 0.5, 0.0]);
1289 assert_eq!(hessian, [0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
1290
1291 workspace.reset(3);
1292 let x = Order2Graph::<3>::variable(0.25, 0, 3, &workspace);
1293 let z = Order2Graph::<3>::variable(0.5, 2, 3, &workspace);
1294 let output = Order2Graph::linear_combination(&[x, z], &[2.0, -4.0], 3, &workspace);
1295 gradient.fill(f64::NAN);
1296 hessian.fill(-9.0);
1297
1298 assert_eq!(output.lower_into(&mut gradient, &mut hessian), -1.5);
1299 assert_eq!(gradient, [2.0, 0.0, -4.0]);
1300 assert_eq!(hessian, [0.0; 9]);
1301 }
1302
1303 struct DenseSymmetric3([[f64; 3]; 3]);
1304
1305 impl SymmetricQuadraticCoefficients for DenseSymmetric3 {
1306 fn dimension(&self) -> usize {
1307 3
1308 }
1309
1310 fn multiply(&self, input: &[f64], output: &mut [f64]) {
1311 for row in 0..3 {
1312 output[row] = (0..3)
1313 .map(|column| self.0[row][column] * input[column])
1314 .sum();
1315 }
1316 }
1317
1318 fn coefficient(&self, row: usize, column: usize) -> f64 {
1319 self.0[row][column]
1320 }
1321 }
1322
1323 struct MatrixFreeDense3<'arena> {
1324 matrix: [[f64; 3]; 3],
1325 workspace: &'arena Order2GraphWorkspace,
1326 }
1327
1328 impl SymmetricQuadraticCoefficients for MatrixFreeDense3<'_> {
1329 fn dimension(&self) -> usize {
1330 3
1331 }
1332
1333 fn multiply(&self, input: &[f64], output: &mut [f64]) {
1334 assert!(self.workspace.tape().node_len != 0);
1335 for row in 0..3 {
1336 output[row] = (0..3)
1337 .map(|column| self.matrix[row][column] * input[column])
1338 .sum();
1339 }
1340 }
1341
1342 fn coefficient(&self, row: usize, column: usize) -> f64 {
1343 panic!(
1344 "compiled graph quadratic lowering must preserve matrix-free multiply, \
1345 but entry ({row}, {column}) was read densely"
1346 )
1347 }
1348 }
1349
1350 struct MatrixFreeIdentity32<'arena> {
1351 workspace: &'arena Order2GraphWorkspace,
1352 }
1353
1354 impl SymmetricQuadraticCoefficients for MatrixFreeIdentity32<'_> {
1355 fn dimension(&self) -> usize {
1356 MAX_QUADRATIC_ARITY
1357 }
1358
1359 fn multiply(&self, input: &[f64], output: &mut [f64]) {
1360 assert!(self.workspace.tape().node_len != 0);
1361 output.copy_from_slice(input);
1362 }
1363
1364 fn coefficient(&self, row: usize, column: usize) -> f64 {
1365 panic!(
1366 "compiled graph quadratic lowering must preserve matrix-free multiply, \
1367 but entry ({row}, {column}) was read densely"
1368 )
1369 }
1370 }
1371
1372 struct CountingIdentity3<'arena> {
1373 workspace: &'arena Order2GraphWorkspace,
1374 multiply_calls: Cell<usize>,
1375 }
1376
1377 impl SymmetricQuadraticCoefficients for CountingIdentity3<'_> {
1378 fn dimension(&self) -> usize {
1379 3
1380 }
1381
1382 fn multiply(&self, input: &[f64], output: &mut [f64]) {
1383 assert!(self.workspace.tape().node_len != 0);
1384 self.multiply_calls.set(self.multiply_calls.get() + 1);
1385 output.copy_from_slice(input);
1386 }
1387
1388 fn coefficient(&self, row: usize, column: usize) -> f64 {
1389 panic!(
1390 "compiled graph quadratic lowering must preserve matrix-free multiply, \
1391 but entry ({row}, {column}) was read densely"
1392 )
1393 }
1394 }
1395
1396 #[test]
1397 fn compiled_graph_quadratic_arity_is_independent_and_matrix_free() {
1398 let mut workspace = Order2GraphWorkspace::new();
1399 workspace.reset(2);
1400 let x = Order2Graph::<2>::variable(0.4, 0, 2, &workspace);
1401 let y = Order2Graph::<2>::variable(-0.7, 1, 2, &workspace);
1402 let xy = x.product(&y);
1403 assert_eq!(
1404 workspace.tape().nodes[xy.node].primary_axis,
1405 NO_PRIMARY_AXIS,
1406 "derived quadratic inputs must select the unrestricted Jacobian projection",
1407 );
1408 let graph_linear = Order2Graph::linear_combination(&[x, xy], &[0.3, -0.8], 2, &workspace);
1409 let coefficients = MatrixFreeDense3 {
1410 matrix: [[1.2, -0.3, 0.25], [-0.3, 0.8, 0.17], [0.25, 0.17, 1.4]],
1411 workspace: &workspace,
1412 };
1413 let graph =
1414 Order2Graph::<2>::symmetric_quadratic_form(&[x, y, xy], &coefficients, 2, &workspace)
1415 .into_order2();
1416
1417 let arena = DynamicJetArena::new();
1418 let eager_x = DynamicOrder2::variable(0.4, 0, 2, &arena);
1419 let eager_y = DynamicOrder2::variable(-0.7, 1, 2, &arena);
1420 let eager_xy = eager_x.product(&eager_y);
1421 let eager_linear =
1422 DynamicOrder2::linear_combination(&[eager_x, eager_xy], &[0.3, -0.8], 2, &arena);
1423 let eager = DynamicOrder2::symmetric_quadratic_form(
1424 &[eager_x, eager_y, eager_xy],
1425 &coefficients,
1426 2,
1427 &arena,
1428 );
1429
1430 let close = |actual: f64, expected: f64| {
1431 let tolerance = 2.0e-13 * actual.abs().max(expected.abs()).max(1.0);
1432 assert!((actual - expected).abs() <= tolerance);
1433 };
1434 let graph_linear = graph_linear.into_order2();
1435 close(graph_linear.value(), eager_linear.v);
1436 close(graph.value(), eager.v);
1437 for primary in 0..2 {
1438 close(graph_linear.g()[primary], eager_linear.g()[primary]);
1439 close(graph.g()[primary], eager.g()[primary]);
1440 }
1441 for row in 0..2 {
1442 for column in 0..2 {
1443 close(
1444 graph_linear.h()[row][column],
1445 eager_linear.h_at(row, column),
1446 );
1447 close(graph.h()[row][column], eager.h_at(row, column));
1448 }
1449 }
1450 }
1451
1452 #[test]
1453 fn compiled_graph_primary_quadratic_groups_repeated_permuted_sparse_axes() {
1454 let mut workspace = Order2GraphWorkspace::new();
1455 workspace.reset(4);
1456 let x = Order2Graph::<4>::variable(0.4, 0, 4, &workspace);
1457 let z = Order2Graph::<4>::variable(1.1, 2, 4, &workspace);
1458 let linear = Order2Graph::linear_combination(&[z, x, z], &[0.3, -0.7, 1.1], 4, &workspace)
1459 .into_order2();
1460 let close = |actual: f64, expected: f64| {
1461 let tolerance = 2.0e-13 * actual.abs().max(expected.abs()).max(1.0);
1462 assert!((actual - expected).abs() <= tolerance);
1463 };
1464 assert!((linear.value() - 1.26).abs() <= 2.0e-13);
1465 for (actual, expected) in linear.g().iter().zip([-0.7, 0.0, 1.4, 0.0]) {
1466 close(*actual, expected);
1467 }
1468 assert!(linear.h().iter().flatten().all(|&channel| channel == 0.0));
1469 let coefficients = MatrixFreeDense3 {
1470 matrix: [[1.2, -0.3, 0.25], [-0.3, 0.8, 0.17], [0.25, 0.17, 1.4]],
1471 workspace: &workspace,
1472 };
1473 let graph =
1474 Order2Graph::<4>::symmetric_quadratic_form(&[z, x, z], &coefficients, 4, &workspace)
1475 .into_order2();
1476
1477 let arena = DynamicJetArena::new();
1478 let eager_x = DynamicOrder2::variable(0.4, 0, 4, &arena);
1479 let eager_z = DynamicOrder2::variable(1.1, 2, 4, &arena);
1480 let eager = DynamicOrder2::symmetric_quadratic_form(
1481 &[eager_z, eager_x, eager_z],
1482 &coefficients,
1483 4,
1484 &arena,
1485 );
1486
1487 close(graph.value(), eager.v);
1488 for primary in 0..4 {
1489 close(graph.g()[primary], eager.g()[primary]);
1490 for other in 0..4 {
1491 close(graph.h()[primary][other], eager.h_at(primary, other));
1492 }
1493 }
1494 }
1495
1496 #[test]
1497 fn compiled_graph_accepts_maximum_quadratic_arity_plus_output_node() {
1498 let mut workspace = Order2GraphWorkspace::new();
1499 workspace.reset(MAX_PRIMARY_DIMENSION);
1500 let values: [f64; MAX_QUADRATIC_ARITY] =
1501 std::array::from_fn(|axis| 0.01 * (axis + 1) as f64);
1502 let vars: [Order2Graph<'_, MAX_PRIMARY_DIMENSION>; MAX_QUADRATIC_ARITY] =
1503 std::array::from_fn(|axis| {
1504 Order2Graph::variable(
1505 values[axis],
1506 axis % MAX_PRIMARY_DIMENSION,
1507 MAX_PRIMARY_DIMENSION,
1508 &workspace,
1509 )
1510 });
1511 let coefficients = MatrixFreeIdentity32 {
1512 workspace: &workspace,
1513 };
1514 let graph = Order2Graph::symmetric_quadratic_form(
1515 &vars,
1516 &coefficients,
1517 MAX_PRIMARY_DIMENSION,
1518 &workspace,
1519 )
1520 .into_order2();
1521
1522 let expected_value = values.iter().map(|value| value * value).sum::<f64>();
1523 let tolerance = 2.0e-13;
1524 assert!((graph.value() - expected_value).abs() <= tolerance);
1525 for primary in 0..MAX_PRIMARY_DIMENSION {
1526 let expected_gradient = 2.0 * (values[primary] + values[primary + 16]);
1527 assert!((graph.g()[primary] - expected_gradient).abs() <= tolerance);
1528 for other in 0..MAX_PRIMARY_DIMENSION {
1529 let expected_hessian = if primary == other { 4.0 } else { 0.0 };
1530 assert!((graph.h()[primary][other] - expected_hessian).abs() <= tolerance);
1531 }
1532 }
1533 }
1534
1535 #[test]
1536 fn compiled_graph_projects_only_sparse_supported_primary_directions() {
1537 let mut workspace = Order2GraphWorkspace::new();
1538 workspace.reset(MAX_PRIMARY_DIMENSION);
1539 let x = Order2Graph::<MAX_PRIMARY_DIMENSION>::variable(
1540 0.4,
1541 0,
1542 MAX_PRIMARY_DIMENSION,
1543 &workspace,
1544 );
1545 let y = Order2Graph::<MAX_PRIMARY_DIMENSION>::variable(
1546 -0.7,
1547 7,
1548 MAX_PRIMARY_DIMENSION,
1549 &workspace,
1550 );
1551 let z = Order2Graph::<MAX_PRIMARY_DIMENSION>::variable(
1552 1.1,
1553 15,
1554 MAX_PRIMARY_DIMENSION,
1555 &workspace,
1556 );
1557 let coefficients = CountingIdentity3 {
1558 workspace: &workspace,
1559 multiply_calls: Cell::new(0),
1560 };
1561 let graph = Order2Graph::symmetric_quadratic_form(
1562 &[x, y, z],
1563 &coefficients,
1564 MAX_PRIMARY_DIMENSION,
1565 &workspace,
1566 )
1567 .into_order2();
1568
1569 assert_eq!(coefficients.multiply_calls.get(), 4);
1570 for primary in 0..MAX_PRIMARY_DIMENSION {
1571 let active = matches!(primary, 0 | 7 | 15);
1572 let expected_gradient = match primary {
1573 0 => 0.8,
1574 7 => -1.4,
1575 15 => 2.2,
1576 _ => 0.0,
1577 };
1578 assert_eq!(graph.g()[primary], expected_gradient);
1579 for other in 0..MAX_PRIMARY_DIMENSION {
1580 assert_eq!(
1581 graph.h()[primary][other],
1582 if active && primary == other { 2.0 } else { 0.0 }
1583 );
1584 }
1585 }
1586 }
1587
1588 #[test]
1589 fn compiled_fused_addend_support_obeys_structural_coefficient() {
1590 let mut workspace = Order2GraphWorkspace::new();
1591 {
1592 workspace.reset(3);
1593 let left = Order2Graph::<3>::variable(0.4, 0, 3, &workspace);
1594 let right = Order2Graph::<3>::variable(-0.7, 1, 3, &workspace);
1595 let omitted = Order2Graph::<3>::variable(1.1, 2, 3, &workspace);
1596 let output = Order2Graph::shared_multiply_add_affine_composed_sum(
1597 &[&left],
1598 &right,
1599 &omitted,
1600 &[-0.0],
1601 &[1.0],
1602 &[[0.2, 0.0, 1.0, 0.0, 0.0]],
1603 3,
1604 &workspace,
1605 );
1606 assert_eq!(workspace.tape().nodes[output.node].support, 0b011);
1607 let channels = output.into_order2();
1608 assert!(channels.g()[2] == 0.0);
1609 assert!((0..3).all(|axis| channels.h()[axis][2] == 0.0));
1610 }
1611
1612 workspace.reset(3);
1613 let left = Order2Graph::<3>::variable(0.4, 0, 3, &workspace);
1614 let right = Order2Graph::<3>::variable(-0.7, 1, 3, &workspace);
1615 let live = Order2Graph::<3>::variable(1.1, 2, 3, &workspace);
1616 let output = Order2Graph::shared_multiply_add_affine_composed_sum(
1617 &[&left],
1618 &right,
1619 &live,
1620 &[1.0],
1621 &[1.0],
1622 &[[0.2, 0.0, 1.0, 0.0, 0.0]],
1623 3,
1624 &workspace,
1625 );
1626 assert_eq!(workspace.tape().nodes[output.node].support, 0b111);
1627 let channels = output.into_order2();
1628 assert_eq!(channels.g()[2], 0.0);
1629 assert_eq!(channels.h()[2][2], 1.0);
1630 }
1631
1632 fn mixed_primary_derived_fused_expression<'arena, S: RuntimeJetScalar<'arena>>(
1633 vars: &[S; 5],
1634 workspace: &'arena S::Workspace,
1635 ) -> S {
1636 let right =
1637 vars[4].affine_compose(1.2, -0.1, [0.7, -1.2, 0.45, 0.0, 0.0]);
1638 let derived_left = vars[2].multiply_add(&vars[3], &vars[0]);
1639 let addend = S::linear_combination(vars, &[0.3, -0.8, 0.5, 1.1, -0.4], 5, workspace);
1640 S::shared_multiply_add_affine_composed_sum(
1641 &[&vars[1], &vars[0], &vars[1], &derived_left],
1642 &right,
1643 &addend,
1644 &[1.0, 1.0, 1.0, -0.4],
1645 &[-1.0, -1.0, 1.0, 0.75],
1646 &[
1647 [0.4, -0.8, 0.3, 0.0, 0.0],
1648 [-0.2, 0.5, -0.7, 0.0, 0.0],
1649 [0.9, 1.1, 0.2, 0.0, 0.0],
1650 [-0.6, 0.4, 0.8, 0.0, 0.0],
1651 ],
1652 5,
1653 workspace,
1654 )
1655 }
1656
1657 #[test]
1658 fn compiled_fused_primary_scatter_matches_eager_with_mixed_lefts() {
1659 let values = [0.4, -0.7, 1.1, -0.3, 0.8];
1660 let eager_vars: [FixedRuntimeJet<Order2<5>, 5>; 5] = std::array::from_fn(|axis| {
1661 FixedRuntimeJet::from_inner(Order2::variable(values[axis], axis))
1662 });
1663 let eager = mixed_primary_derived_fused_expression(&eager_vars, &()).into_inner();
1664
1665 let mut workspace = Order2GraphWorkspace::new();
1666 workspace.reset(5);
1667 let graph_vars: [Order2Graph<'_, 5>; 5] =
1668 std::array::from_fn(|axis| Order2Graph::variable(values[axis], axis, 5, &workspace));
1669 let graph_output = mixed_primary_derived_fused_expression(&graph_vars, &workspace);
1670 let output_node = workspace.tape().nodes[graph_output.node];
1671 assert_eq!(output_node.edge_len, 5);
1672 let graph = graph_output.into_order2();
1673
1674 let close = |actual: f64, expected: f64| {
1675 let tolerance = 2.0e-12 * actual.abs().max(expected.abs()).max(1.0);
1676 assert!((actual - expected).abs() <= tolerance);
1677 };
1678 close(graph.value(), eager.value());
1679 for primary in 0..5 {
1680 close(graph.g()[primary], eager.g()[primary]);
1681 for other in 0..5 {
1682 close(graph.h()[primary][other], eager.h()[primary][other]);
1683 }
1684 }
1685 }
1686
1687 fn expression<'arena, S: RuntimeJetScalar<'arena>>(
1688 vars: &[S; 6],
1689 coefficients: &DenseSymmetric3,
1690 scales: &[f64; 4],
1691 stacks: &[[f64; 5]; 4],
1692 workspace: &'arena S::Workspace,
1693 ) -> S {
1694 let nonlinear = [
1695 vars[0].product(&vars[1]),
1696 vars[2].affine_compose(scales[0], scales[1], stacks[0]),
1697 vars[3].multiply_add(&vars[4], &vars[5]),
1698 ];
1699 let quadratic = S::symmetric_quadratic_form(&nonlinear, coefficients, 6, workspace);
1700 let linear = S::linear_combination(vars, &[0.2, -0.7, 1.1, 0.4, -0.3, 0.8], 6, workspace);
1701 let product = quadratic.product(&linear);
1702 S::affine_composed_sum(
1703 &[quadratic, linear, product, nonlinear[1].clone()],
1704 &[scales[0], scales[1], scales[2], scales[3]],
1705 stacks,
1706 6,
1707 workspace,
1708 )
1709 }
1710
1711 fn fused_expression<'arena, S: RuntimeJetScalar<'arena>>(
1712 vars: &[S; 6],
1713 scales: &[f64; 4],
1714 stacks: &[[f64; 5]; 4],
1715 workspace: &'arena S::Workspace,
1716 ) -> S {
1717 const N: usize = 10;
1718 let upstream = [
1719 vars[0].product(&vars[1]),
1720 vars[2].affine_compose(scales[0], scales[1], stacks[0]),
1721 vars[3].multiply_add(&vars[4], &vars[5]),
1722 ];
1723 let repeated_left = upstream[1].clone();
1724 let mut lefts: [&S; N] = std::array::from_fn(|term| &upstream[term % upstream.len()]);
1725 lefts[9] = &repeated_left;
1726 let right = &upstream[1];
1727 let addend = &vars[2];
1728 let addend_scales: [f64; N] = std::array::from_fn(|term| match term % 4 {
1729 0 => 0.0,
1730 1 => 1.0,
1731 2 => -0.75,
1732 _ => 0.35,
1733 });
1734 let mut input_scales: [f64; N] = std::array::from_fn(|term| match term {
1735 0 => 0.0,
1736 1 => -1.25,
1737 _ => scales[term % scales.len()],
1738 });
1739 input_scales[9] = 1.25;
1740 let derivative_stacks: [[f64; 5]; N] =
1741 std::array::from_fn(|term| stacks[term % stacks.len()]);
1742 S::shared_multiply_add_affine_composed_sum(
1743 &lefts,
1744 right,
1745 addend,
1746 &addend_scales,
1747 &input_scales,
1748 &derivative_stacks,
1749 6,
1750 workspace,
1751 )
1752 }
1753
1754 #[test]
1755 fn compiled_graph_matches_eager_order2_randomized_full_vgh() {
1756 fn sample(state: &mut u64) -> f64 {
1757 *state ^= *state << 13;
1758 *state ^= *state >> 7;
1759 *state ^= *state << 17;
1760 let unit = (*state >> 11) as f64 * (1.0 / ((1_u64 << 53) as f64));
1761 2.0 * unit - 1.0
1762 }
1763
1764 fn close(actual: f64, expected: f64, case: usize, label: &str) {
1765 let tolerance = 5.0e-12 * actual.abs().max(expected.abs()).max(1.0);
1766 assert!(
1767 (actual - expected).abs() <= tolerance,
1768 "case {case} {label}: graph={actual:+.16e}, eager={expected:+.16e}, tolerance={tolerance:.3e}"
1769 );
1770 }
1771
1772 let mut state = 0x932d_a660_5eed_f00d_u64;
1773 let mut workspace = Order2GraphWorkspace::new();
1774 let mut fused_workspace = Order2GraphWorkspace::new();
1775
1776 fused_workspace.reset(6);
1777 let empty_terms: [&Order2Graph<'_, 6>; 0] = [];
1778 let empty_shared = Order2Graph::constant(1.0, 6, &fused_workspace);
1779 let empty_scales: [f64; 0] = [];
1780 let empty_stacks: [[f64; 5]; 0] = [];
1781 let empty = Order2Graph::shared_multiply_add_affine_composed_sum(
1782 &empty_terms,
1783 &empty_shared,
1784 &empty_shared,
1785 &empty_scales,
1786 &empty_scales,
1787 &empty_stacks,
1788 6,
1789 &fused_workspace,
1790 )
1791 .into_order2();
1792 assert_eq!(empty.value().to_bits(), 0.0_f64.to_bits());
1793 assert!(empty.g().iter().all(|&channel| channel == 0.0));
1794 assert!(empty.h().iter().flatten().all(|&channel| channel == 0.0));
1795
1796 for case in 0..256 {
1797 let values: [f64; 6] = std::array::from_fn(|_| sample(&mut state));
1798 let scales: [f64; 4] = std::array::from_fn(|_| sample(&mut state));
1799 let stacks: [[f64; 5]; 4] =
1800 std::array::from_fn(|_| std::array::from_fn(|_| sample(&mut state)));
1801 let raw: [[f64; 3]; 3] =
1802 std::array::from_fn(|_| std::array::from_fn(|_| sample(&mut state)));
1803 let coefficients = DenseSymmetric3([
1804 [raw[0][0], raw[0][1], raw[0][2]],
1805 [raw[0][1], raw[1][1], raw[1][2]],
1806 [raw[0][2], raw[1][2], raw[2][2]],
1807 ]);
1808
1809 let eager_vars: [FixedRuntimeJet<Order2<6>, 6>; 6] = std::array::from_fn(|axis| {
1810 FixedRuntimeJet::from_inner(Order2::variable(values[axis], axis))
1811 });
1812 let eager = expression(&eager_vars, &coefficients, &scales, &stacks, &()).into_inner();
1813 let eager_fused = fused_expression(&eager_vars, &scales, &stacks, &()).into_inner();
1814
1815 workspace.reset(6);
1816 let graph_vars: [Order2Graph<'_, 6>; 6] = std::array::from_fn(|axis| {
1817 Order2Graph::variable(values[axis], axis, 6, &workspace)
1818 });
1819 let graph =
1820 expression(&graph_vars, &coefficients, &scales, &stacks, &workspace).into_order2();
1821 fused_workspace.reset(6);
1822 let fused_graph_vars: [Order2Graph<'_, 6>; 6] = std::array::from_fn(|axis| {
1823 Order2Graph::variable(values[axis], axis, 6, &fused_workspace)
1824 });
1825 let graph_fused =
1826 fused_expression(&fused_graph_vars, &scales, &stacks, &fused_workspace)
1827 .into_order2();
1828
1829 close(graph.value(), eager.value(), case, "value");
1830 close(
1831 graph_fused.value(),
1832 eager_fused.value(),
1833 case,
1834 "fused value",
1835 );
1836 for primary in 0..6 {
1837 close(graph.g()[primary], eager.g()[primary], case, "gradient");
1838 close(
1839 graph_fused.g()[primary],
1840 eager_fused.g()[primary],
1841 case,
1842 "fused gradient",
1843 );
1844 for other in 0..6 {
1845 close(
1846 graph.h()[primary][other],
1847 eager.h()[primary][other],
1848 case,
1849 "Hessian",
1850 );
1851 close(
1852 graph_fused.h()[primary][other],
1853 eager_fused.h()[primary][other],
1854 case,
1855 "fused Hessian",
1856 );
1857 }
1858 }
1859 }
1860 }
1861}