1use std::fmt::Debug;
24
25use scirs2_core::ndarray::Array1;
26use scirs2_core::numeric::Float;
27use std::collections::HashMap;
28
29use crate::error::{OptimError, Result};
30
31pub trait ReverseScalar:
35 Float
36 + Debug
37 + Default
38 + Clone
39 + Send
40 + Sync
41 + 'static
42 + std::iter::Sum
43 + scirs2_core::ndarray::ScalarOperand
44{
45}
46
47impl<T> ReverseScalar for T where
48 T: Float
49 + Debug
50 + Default
51 + Clone
52 + Send
53 + Sync
54 + 'static
55 + std::iter::Sum
56 + scirs2_core::ndarray::ScalarOperand
57{
58}
59
60pub struct ReverseModeEngine<T: ReverseScalar> {
62 tape: Vec<ReverseOperation<T>>,
64
65 values: Vec<Array1<T>>,
67
68 shapes: Vec<Vec<usize>>,
70
71 requiresgrad: Vec<bool>,
73
74 variables: HashMap<String, usize>,
76
77 gradients: Vec<Option<Array1<T>>>,
79
80 recording: bool,
82
83 higher_order: bool,
85
86 dirty: bool,
88
89 cache: HashMap<usize, Array1<T>>,
91}
92
93#[derive(Debug, Clone)]
95struct ReverseOperation<T: Float + Debug + Send + Sync + 'static> {
96 op_type: ReverseOpType,
98
99 inputs: Vec<usize>,
101
102 backward_fn: BackwardFunction<T>,
104
105 detached: bool,
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111enum ReverseOpType {
112 Variable,
114 Constant,
116 Add,
117 Subtract,
118 Multiply,
119 Divide,
120 Power,
121 Exp,
122 Log,
123 Sin,
124 Cos,
125 Tanh,
126 Sigmoid,
127 ReLU,
128 LeakyReLU,
129 MatMul,
130 Dot,
131 Sum,
132 Mean,
133 Norm,
134 Reshape,
135}
136
137#[derive(Debug, Clone)]
139enum BackwardFunction<T: Float + Debug + Send + Sync + 'static> {
140 Leaf,
142
143 AddBackward,
145
146 SubtractBackward,
148
149 MultiplyBackward,
151
152 DivideBackward,
154
155 DotBackward,
157
158 PowerBackward { exponent: T },
160
161 ExpBackward,
163
164 LogBackward,
166
167 TrigBackward { function: TrigFunction },
169
170 ActivationBackward { function: ActivationFunction },
172
173 MatMulBackward,
175
176 ReductionBackward { reduction_type: ReductionType },
178
179 ReshapeBackward { newshape: Vec<usize> },
181}
182
183#[derive(Debug, Clone, Copy)]
185enum TrigFunction {
186 Sin,
187 Cos,
188}
189
190#[derive(Debug, Clone, Copy)]
192enum ActivationFunction {
193 Tanh,
194 Sigmoid,
195 ReLU,
196 LeakyReLU { alpha: f64 },
197}
198
199#[derive(Debug, Clone, Copy)]
201enum ReductionType {
202 Sum,
203 Mean,
204 Norm,
205}
206
207#[derive(Debug, Clone)]
209pub struct GradientContext<T: Float + Debug + Send + Sync + 'static> {
210 pub requiresgrad: HashMap<usize, bool>,
212
213 pub accumulate: bool,
215
216 pub retain_graph: bool,
218
219 pub create_graph: bool,
221
222 _phantom: std::marker::PhantomData<T>,
224}
225
226impl<T: Float + Debug + Default + Clone + Send + Sync + 'static> Default for GradientContext<T> {
227 fn default() -> Self {
228 Self {
229 requiresgrad: HashMap::new(),
230 accumulate: true,
231 retain_graph: false,
232 create_graph: false,
233 _phantom: std::marker::PhantomData,
234 }
235 }
236}
237
238impl<T: ReverseScalar> Default for ReverseModeEngine<T> {
239 fn default() -> Self {
240 Self::new()
241 }
242}
243
244impl<T: ReverseScalar> ReverseModeEngine<T> {
245 pub fn new() -> Self {
247 Self {
248 tape: Vec::new(),
249 values: Vec::new(),
250 shapes: Vec::new(),
251 requiresgrad: Vec::new(),
252 variables: HashMap::new(),
253 gradients: Vec::new(),
254 recording: true,
255 higher_order: false,
256 dirty: false,
257 cache: HashMap::new(),
258 }
259 }
260
261 pub fn set_recording(&mut self, recording: bool) {
267 self.recording = recording;
268 }
269
270 pub fn is_recording(&self) -> bool {
272 self.recording
273 }
274
275 pub fn enable_higher_order(&mut self, enabled: bool) {
277 self.higher_order = enabled;
278 }
279
280 pub fn higher_order_enabled(&self) -> bool {
282 self.higher_order
283 }
284
285 pub fn clear_tape(&mut self) {
287 self.tape.clear();
288 self.values.clear();
289 self.shapes.clear();
290 self.requiresgrad.clear();
291 self.variables.clear();
292 self.gradients.clear();
293 self.cache.clear();
294 self.dirty = false;
295 }
296
297 pub fn tape_len(&self) -> usize {
299 self.tape.len()
300 }
301
302 pub fn create_variable(&mut self, name: &str, value: Array1<T>, requiresgrad: bool) -> usize {
306 let shape = vec![value.len()];
307 self.push_leaf(Some(name), value, shape, requiresgrad)
308 }
309
310 pub fn create_variable_with_shape(
315 &mut self,
316 name: &str,
317 value: Array1<T>,
318 shape: &[usize],
319 requiresgrad: bool,
320 ) -> Result<usize> {
321 let expected: usize = shape.iter().product();
322 if shape.is_empty() || expected != value.len() {
323 return Err(OptimError::InvalidConfig(format!(
324 "shape {:?} does not match value length {}",
325 shape,
326 value.len()
327 )));
328 }
329 Ok(self.push_leaf(Some(name), value, shape.to_vec(), requiresgrad))
330 }
331
332 pub fn create_constant(&mut self, value: Array1<T>) -> usize {
334 let shape = vec![value.len()];
335 self.push_leaf(None, value, shape, false)
336 }
337
338 fn push_leaf(
339 &mut self,
340 name: Option<&str>,
341 value: Array1<T>,
342 shape: Vec<usize>,
343 requiresgrad: bool,
344 ) -> usize {
345 let varid = self.tape.len();
346 let op_type = if requiresgrad {
347 ReverseOpType::Variable
348 } else {
349 ReverseOpType::Constant
350 };
351
352 self.tape.push(ReverseOperation {
353 op_type,
354 inputs: Vec::new(),
355 backward_fn: BackwardFunction::Leaf,
356 detached: !requiresgrad,
357 });
358 let len = value.len();
359 self.values.push(value);
360 self.shapes.push(shape);
361 self.requiresgrad.push(requiresgrad);
362 self.gradients.push(if requiresgrad {
363 Some(Array1::zeros(len))
364 } else {
365 None
366 });
367
368 if let Some(name) = name {
369 self.variables.insert(name.to_string(), varid);
370 }
371
372 varid
373 }
374
375 pub fn set_variable_value(&mut self, varid: usize, value: Array1<T>) -> Result<()> {
381 let op = self
382 .tape
383 .get(varid)
384 .ok_or_else(|| OptimError::InvalidConfig(format!("invalid variable id {varid}")))?;
385 if !op.inputs.is_empty() {
386 return Err(OptimError::InvalidConfig(format!(
387 "node {varid} is not a leaf and cannot be assigned"
388 )));
389 }
390 let shape = self.shapes.get(varid).ok_or_else(|| {
391 OptimError::ComputationError(format!("missing shape for node {varid}"))
392 })?;
393 let expected: usize = shape.iter().product();
394 if expected != value.len() {
395 return Err(OptimError::InvalidConfig(format!(
396 "assigned value length {} does not match node shape {:?}",
397 value.len(),
398 shape
399 )));
400 }
401 self.values[varid] = value;
402 self.dirty = true;
403 Ok(())
404 }
405
406 pub fn set_variable_value_by_name(&mut self, name: &str, value: Array1<T>) -> Result<()> {
408 let varid = *self
409 .variables
410 .get(name)
411 .ok_or_else(|| OptimError::InvalidConfig(format!("unknown variable '{name}'")))?;
412 self.set_variable_value(varid, value)
413 }
414
415 pub fn add(&mut self, lhs: usize, rhs: usize) -> Result<usize> {
419 self.binary_op(ReverseOpType::Add, lhs, rhs, BackwardFunction::AddBackward)
420 }
421
422 pub fn subtract(&mut self, lhs: usize, rhs: usize) -> Result<usize> {
424 self.binary_op(
425 ReverseOpType::Subtract,
426 lhs,
427 rhs,
428 BackwardFunction::SubtractBackward,
429 )
430 }
431
432 pub fn multiply(&mut self, lhs: usize, rhs: usize) -> Result<usize> {
434 self.binary_op(
435 ReverseOpType::Multiply,
436 lhs,
437 rhs,
438 BackwardFunction::MultiplyBackward,
439 )
440 }
441
442 pub fn divide(&mut self, lhs: usize, rhs: usize) -> Result<usize> {
444 self.binary_op(
445 ReverseOpType::Divide,
446 lhs,
447 rhs,
448 BackwardFunction::DivideBackward,
449 )
450 }
451
452 pub fn power(&mut self, base: usize, exponent: T) -> Result<usize> {
454 self.unary_op(
455 ReverseOpType::Power,
456 base,
457 BackwardFunction::PowerBackward { exponent },
458 )
459 }
460
461 pub fn exp(&mut self, input: usize) -> Result<usize> {
463 self.unary_op(ReverseOpType::Exp, input, BackwardFunction::ExpBackward)
464 }
465
466 pub fn log(&mut self, input: usize) -> Result<usize> {
468 self.unary_op(ReverseOpType::Log, input, BackwardFunction::LogBackward)
469 }
470
471 pub fn sin(&mut self, input: usize) -> Result<usize> {
473 self.unary_op(
474 ReverseOpType::Sin,
475 input,
476 BackwardFunction::TrigBackward {
477 function: TrigFunction::Sin,
478 },
479 )
480 }
481
482 pub fn cos(&mut self, input: usize) -> Result<usize> {
484 self.unary_op(
485 ReverseOpType::Cos,
486 input,
487 BackwardFunction::TrigBackward {
488 function: TrigFunction::Cos,
489 },
490 )
491 }
492
493 pub fn tanh(&mut self, input: usize) -> Result<usize> {
495 self.unary_op(
496 ReverseOpType::Tanh,
497 input,
498 BackwardFunction::ActivationBackward {
499 function: ActivationFunction::Tanh,
500 },
501 )
502 }
503
504 pub fn sigmoid(&mut self, input: usize) -> Result<usize> {
506 self.unary_op(
507 ReverseOpType::Sigmoid,
508 input,
509 BackwardFunction::ActivationBackward {
510 function: ActivationFunction::Sigmoid,
511 },
512 )
513 }
514
515 pub fn relu(&mut self, input: usize) -> Result<usize> {
517 self.unary_op(
518 ReverseOpType::ReLU,
519 input,
520 BackwardFunction::ActivationBackward {
521 function: ActivationFunction::ReLU,
522 },
523 )
524 }
525
526 pub fn leaky_relu(&mut self, input: usize, alpha: f64) -> Result<usize> {
528 self.unary_op(
529 ReverseOpType::LeakyReLU,
530 input,
531 BackwardFunction::ActivationBackward {
532 function: ActivationFunction::LeakyReLU { alpha },
533 },
534 )
535 }
536
537 pub fn matmul(&mut self, lhs: usize, rhs: usize) -> Result<usize> {
543 self.binary_op(
544 ReverseOpType::MatMul,
545 lhs,
546 rhs,
547 BackwardFunction::MatMulBackward,
548 )
549 }
550
551 pub fn dot(&mut self, lhs: usize, rhs: usize) -> Result<usize> {
553 self.binary_op(ReverseOpType::Dot, lhs, rhs, BackwardFunction::DotBackward)
554 }
555
556 pub fn sum(&mut self, input: usize, _axis: Option<usize>) -> Result<usize> {
558 self.unary_op(
559 ReverseOpType::Sum,
560 input,
561 BackwardFunction::ReductionBackward {
562 reduction_type: ReductionType::Sum,
563 },
564 )
565 }
566
567 pub fn mean(&mut self, input: usize, _axis: Option<usize>) -> Result<usize> {
569 self.unary_op(
570 ReverseOpType::Mean,
571 input,
572 BackwardFunction::ReductionBackward {
573 reduction_type: ReductionType::Mean,
574 },
575 )
576 }
577
578 pub fn norm(&mut self, input: usize) -> Result<usize> {
580 self.unary_op(
581 ReverseOpType::Norm,
582 input,
583 BackwardFunction::ReductionBackward {
584 reduction_type: ReductionType::Norm,
585 },
586 )
587 }
588
589 pub fn reshape(&mut self, input: usize, newshape: &[usize]) -> Result<usize> {
591 let inputlen = self.value(input)?.len();
592 let expected: usize = newshape.iter().product();
593 if newshape.is_empty() || expected != inputlen {
594 return Err(OptimError::InvalidConfig(format!(
595 "reshape to {newshape:?} is incompatible with {inputlen} elements"
596 )));
597 }
598 self.push_op(
599 ReverseOpType::Reshape,
600 vec![input],
601 BackwardFunction::ReshapeBackward {
602 newshape: newshape.to_vec(),
603 },
604 )
605 }
606
607 pub fn forward(&mut self) -> Result<()> {
614 for idx in 0..self.tape.len() {
615 if self.tape[idx].inputs.is_empty() {
616 continue;
617 }
618 let (value, shape) = self.evaluate_node(idx)?;
619 self.values[idx] = value;
620 self.shapes[idx] = shape;
621 }
622 self.dirty = false;
623 Ok(())
624 }
625
626 pub fn value(&self, varid: usize) -> Result<&Array1<T>> {
628 self.values
629 .get(varid)
630 .ok_or_else(|| OptimError::InvalidConfig(format!("invalid variable id {varid}")))
631 }
632
633 pub fn value_by_name(&self, name: &str) -> Result<&Array1<T>> {
635 let varid = *self
636 .variables
637 .get(name)
638 .ok_or_else(|| OptimError::InvalidConfig(format!("unknown variable '{name}'")))?;
639 self.value(varid)
640 }
641
642 pub fn shape(&self, varid: usize) -> Result<&[usize]> {
644 self.shapes
645 .get(varid)
646 .map(|s| s.as_slice())
647 .ok_or_else(|| OptimError::InvalidConfig(format!("invalid variable id {varid}")))
648 }
649
650 pub fn scalar_value(&self, varid: usize) -> Result<T> {
652 let value = self.value(varid)?;
653 if value.len() != 1 {
654 return Err(OptimError::ComputationError(format!(
655 "node {varid} holds {} elements, expected a scalar",
656 value.len()
657 )));
658 }
659 Ok(value[0])
660 }
661
662 pub fn backward(&mut self, outputid: usize, gradient: Option<Array1<T>>) -> Result<()> {
669 if outputid >= self.tape.len() {
670 return Err(OptimError::InvalidConfig(format!(
671 "invalid output id {outputid} (tape has {} nodes)",
672 self.tape.len()
673 )));
674 }
675
676 if self.dirty {
677 self.forward()?;
678 }
679
680 self.gradients.resize(self.tape.len(), None);
683 for (idx, slot) in self.gradients.iter_mut().enumerate() {
684 let is_leaf_with_grad = self.requiresgrad.get(idx).copied().unwrap_or(false);
685 if is_leaf_with_grad {
686 let len = self.values[idx].len();
687 *slot = Some(Array1::zeros(len));
688 } else {
689 *slot = None;
690 }
691 }
692
693 let output_len = self.values[outputid].len();
694 let output_grad = match gradient {
695 Some(g) => {
696 if g.len() != output_len {
697 return Err(OptimError::InvalidConfig(format!(
698 "seed gradient length {} does not match output length {output_len}",
699 g.len()
700 )));
701 }
702 g
703 }
704 None => Array1::ones(output_len),
705 };
706 self.gradients[outputid] = Some(output_grad);
707
708 for idx in (0..self.tape.len()).rev() {
710 let output_gradient = match self.gradients.get(idx).and_then(|g| g.clone()) {
711 Some(g) => g,
712 None => continue,
713 };
714 if self.tape[idx].inputs.is_empty() || self.tape[idx].detached {
715 continue;
716 }
717
718 let input_gradients = self.compute_backward_pass(idx, &output_gradient)?;
719 let inputs = self.tape[idx].inputs.clone();
720 if input_gradients.len() != inputs.len() {
721 return Err(OptimError::ComputationError(format!(
722 "backward for node {idx} produced {} gradients for {} inputs",
723 input_gradients.len(),
724 inputs.len()
725 )));
726 }
727
728 for (grad, input_id) in input_gradients.into_iter().zip(inputs) {
729 let expected = self.values.get(input_id).map(|v| v.len()).ok_or_else(|| {
730 OptimError::ComputationError(format!("missing value for node {input_id}"))
731 })?;
732 if grad.len() != expected {
733 return Err(OptimError::ComputationError(format!(
734 "gradient length {} does not match input {input_id} length {expected}",
735 grad.len()
736 )));
737 }
738 match self.gradients.get_mut(input_id) {
739 Some(Some(existing)) => *existing = &*existing + &grad,
740 Some(slot @ None) => *slot = Some(grad),
741 None => {
742 return Err(OptimError::ComputationError(format!(
743 "gradient slot {input_id} out of range"
744 )))
745 }
746 }
747 }
748 }
749
750 Ok(())
751 }
752
753 pub fn get_gradient(&self, varid: usize) -> Option<&Array1<T>> {
755 self.gradients.get(varid)?.as_ref()
756 }
757
758 pub fn get_gradient_by_name(&self, name: &str) -> Option<&Array1<T>> {
760 let varid = *self.variables.get(name)?;
761 self.get_gradient(varid)
762 }
763
764 pub fn zero_gradients(&mut self) {
766 for grad in self.gradients.iter_mut().flatten() {
767 grad.fill(T::zero());
768 }
769 self.cache.clear();
770 }
771
772 pub fn get_all_gradients(&self) -> HashMap<String, Array1<T>> {
774 let mut result = HashMap::new();
775
776 for (name, &varid) in &self.variables {
777 if let Some(grad) = self.get_gradient(varid) {
778 result.insert(name.clone(), grad.clone());
779 }
780 }
781
782 result
783 }
784
785 fn binary_op(
788 &mut self,
789 op_type: ReverseOpType,
790 lhs: usize,
791 rhs: usize,
792 backward_fn: BackwardFunction<T>,
793 ) -> Result<usize> {
794 self.push_op(op_type, vec![lhs, rhs], backward_fn)
795 }
796
797 fn unary_op(
798 &mut self,
799 op_type: ReverseOpType,
800 input: usize,
801 backward_fn: BackwardFunction<T>,
802 ) -> Result<usize> {
803 self.push_op(op_type, vec![input], backward_fn)
804 }
805
806 fn push_op(
807 &mut self,
808 op_type: ReverseOpType,
809 inputs: Vec<usize>,
810 backward_fn: BackwardFunction<T>,
811 ) -> Result<usize> {
812 for &input in &inputs {
813 if input >= self.tape.len() {
814 return Err(OptimError::InvalidConfig(format!(
815 "operand {input} is not a valid tape node"
816 )));
817 }
818 }
819
820 let outputid = self.tape.len();
821 self.tape.push(ReverseOperation {
826 op_type,
827 inputs: inputs.clone(),
828 backward_fn: backward_fn.clone(),
829 detached: !self.recording,
830 });
831 self.values.push(Array1::zeros(0));
833 self.shapes.push(vec![0]);
834 self.requiresgrad.push(false);
835 self.gradients.push(None);
836
837 let (value, shape) = match self.evaluate_op(op_type, &inputs, &backward_fn) {
838 Ok(v) => v,
839 Err(e) => {
840 self.tape.pop();
842 self.values.pop();
843 self.shapes.pop();
844 self.requiresgrad.pop();
845 self.gradients.pop();
846 return Err(e);
847 }
848 };
849 self.values[outputid] = value;
850 self.shapes[outputid] = shape;
851
852 Ok(outputid)
853 }
854
855 fn evaluate_node(&self, idx: usize) -> Result<(Array1<T>, Vec<usize>)> {
857 let op = &self.tape[idx];
858 self.evaluate_op(op.op_type, &op.inputs, &op.backward_fn)
859 }
860
861 fn evaluate_op(
862 &self,
863 op_type: ReverseOpType,
864 inputs: &[usize],
865 backward_fn: &BackwardFunction<T>,
866 ) -> Result<(Array1<T>, Vec<usize>)> {
867 match op_type {
868 ReverseOpType::Variable | ReverseOpType::Constant => Err(OptimError::ComputationError(
869 "leaf nodes carry their value directly and cannot be evaluated".to_string(),
870 )),
871
872 ReverseOpType::Add
873 | ReverseOpType::Subtract
874 | ReverseOpType::Multiply
875 | ReverseOpType::Divide => {
876 let (lhs, rhs) = self.binary_operands(inputs)?;
877 self.require_same_len(lhs, rhs, op_type)?;
878 let out = match op_type {
879 ReverseOpType::Add => lhs + rhs,
880 ReverseOpType::Subtract => lhs - rhs,
881 ReverseOpType::Multiply => lhs * rhs,
882 _ => {
883 for &d in rhs.iter() {
884 if d == T::zero() {
885 return Err(OptimError::ComputationError(
886 "division by zero in reverse-mode tape".to_string(),
887 ));
888 }
889 }
890 lhs / rhs
891 }
892 };
893 let shape = self.shape_of(inputs[0])?.to_vec();
894 Ok((out, shape))
895 }
896
897 ReverseOpType::Dot => {
898 let (lhs, rhs) = self.binary_operands(inputs)?;
899 self.require_same_len(lhs, rhs, op_type)?;
900 let acc = lhs
901 .iter()
902 .zip(rhs.iter())
903 .fold(T::zero(), |a, (&x, &y)| a + x * y);
904 Ok((Array1::from_elem(1, acc), vec![1]))
905 }
906
907 ReverseOpType::MatMul => {
908 let (m, k, n, rhs_is_vector) = self.matmul_dims(inputs)?;
909 let (lhs, rhs) = self.binary_operands(inputs)?;
910 let out = matmul_flat(lhs, m, k, rhs, n);
911 let shape = if rhs_is_vector { vec![m] } else { vec![m, n] };
912 Ok((out, shape))
913 }
914
915 ReverseOpType::Power => {
916 let x = self.unary_operand(inputs)?;
917 let exponent = match backward_fn {
918 BackwardFunction::PowerBackward { exponent } => *exponent,
919 _ => {
920 return Err(OptimError::ComputationError(
921 "power node is missing its exponent".to_string(),
922 ))
923 }
924 };
925 Ok((
926 x.mapv(|v| v.powf(exponent)),
927 self.shape_of(inputs[0])?.to_vec(),
928 ))
929 }
930
931 ReverseOpType::Exp => {
932 let x = self.unary_operand(inputs)?;
933 Ok((x.mapv(|v| v.exp()), self.shape_of(inputs[0])?.to_vec()))
934 }
935
936 ReverseOpType::Log => {
937 let x = self.unary_operand(inputs)?;
938 for &v in x.iter() {
939 if v <= T::zero() {
940 return Err(OptimError::ComputationError(
941 "log of a non-positive value in reverse-mode tape".to_string(),
942 ));
943 }
944 }
945 Ok((x.mapv(|v| v.ln()), self.shape_of(inputs[0])?.to_vec()))
946 }
947
948 ReverseOpType::Sin => {
949 let x = self.unary_operand(inputs)?;
950 Ok((x.mapv(|v| v.sin()), self.shape_of(inputs[0])?.to_vec()))
951 }
952
953 ReverseOpType::Cos => {
954 let x = self.unary_operand(inputs)?;
955 Ok((x.mapv(|v| v.cos()), self.shape_of(inputs[0])?.to_vec()))
956 }
957
958 ReverseOpType::Tanh => {
959 let x = self.unary_operand(inputs)?;
960 Ok((x.mapv(|v| v.tanh()), self.shape_of(inputs[0])?.to_vec()))
961 }
962
963 ReverseOpType::Sigmoid => {
964 let x = self.unary_operand(inputs)?;
965 Ok((
966 x.mapv(|v| T::one() / (T::one() + (-v).exp())),
967 self.shape_of(inputs[0])?.to_vec(),
968 ))
969 }
970
971 ReverseOpType::ReLU => {
972 let x = self.unary_operand(inputs)?;
973 Ok((
974 x.mapv(|v| if v > T::zero() { v } else { T::zero() }),
975 self.shape_of(inputs[0])?.to_vec(),
976 ))
977 }
978
979 ReverseOpType::LeakyReLU => {
980 let x = self.unary_operand(inputs)?;
981 let alpha = match backward_fn {
982 BackwardFunction::ActivationBackward {
983 function: ActivationFunction::LeakyReLU { alpha },
984 } => *alpha,
985 _ => {
986 return Err(OptimError::ComputationError(
987 "leaky relu node is missing its slope".to_string(),
988 ))
989 }
990 };
991 let alpha_t: T = scirs2_core::numeric::NumCast::from(alpha).ok_or_else(|| {
992 OptimError::ComputationError("failed to convert leaky-relu slope".to_string())
993 })?;
994 Ok((
995 x.mapv(|v| if v > T::zero() { v } else { alpha_t * v }),
996 self.shape_of(inputs[0])?.to_vec(),
997 ))
998 }
999
1000 ReverseOpType::Sum => {
1001 let x = self.unary_operand(inputs)?;
1002 let acc = x.iter().copied().fold(T::zero(), |a, b| a + b);
1003 Ok((Array1::from_elem(1, acc), vec![1]))
1004 }
1005
1006 ReverseOpType::Mean => {
1007 let x = self.unary_operand(inputs)?;
1008 if x.is_empty() {
1009 return Err(OptimError::ComputationError(
1010 "mean of an empty tensor".to_string(),
1011 ));
1012 }
1013 let n = T::from(x.len()).ok_or_else(|| {
1014 OptimError::ComputationError("failed to convert length".to_string())
1015 })?;
1016 let acc = x.iter().copied().fold(T::zero(), |a, b| a + b) / n;
1017 Ok((Array1::from_elem(1, acc), vec![1]))
1018 }
1019
1020 ReverseOpType::Norm => {
1021 let x = self.unary_operand(inputs)?;
1022 let acc = x.iter().fold(T::zero(), |a, &b| a + b * b).sqrt();
1023 Ok((Array1::from_elem(1, acc), vec![1]))
1024 }
1025
1026 ReverseOpType::Reshape => {
1027 let x = self.unary_operand(inputs)?;
1028 let newshape = match backward_fn {
1029 BackwardFunction::ReshapeBackward { newshape } => newshape.clone(),
1030 _ => {
1031 return Err(OptimError::ComputationError(
1032 "reshape node is missing its target shape".to_string(),
1033 ))
1034 }
1035 };
1036 let expected: usize = newshape.iter().product();
1037 if expected != x.len() {
1038 return Err(OptimError::ComputationError(format!(
1039 "reshape to {newshape:?} is incompatible with {} elements",
1040 x.len()
1041 )));
1042 }
1043 Ok((x.clone(), newshape))
1044 }
1045 }
1046 }
1047
1048 fn binary_operands(&self, inputs: &[usize]) -> Result<(&Array1<T>, &Array1<T>)> {
1049 if inputs.len() != 2 {
1050 return Err(OptimError::ComputationError(format!(
1051 "binary operation expects 2 operands, got {}",
1052 inputs.len()
1053 )));
1054 }
1055 Ok((self.value(inputs[0])?, self.value(inputs[1])?))
1056 }
1057
1058 fn unary_operand(&self, inputs: &[usize]) -> Result<&Array1<T>> {
1059 if inputs.len() != 1 {
1060 return Err(OptimError::ComputationError(format!(
1061 "unary operation expects 1 operand, got {}",
1062 inputs.len()
1063 )));
1064 }
1065 self.value(inputs[0])
1066 }
1067
1068 fn shape_of(&self, varid: usize) -> Result<&[usize]> {
1069 self.shapes
1070 .get(varid)
1071 .map(|s| s.as_slice())
1072 .ok_or_else(|| OptimError::InvalidConfig(format!("invalid variable id {varid}")))
1073 }
1074
1075 fn require_same_len(
1076 &self,
1077 lhs: &Array1<T>,
1078 rhs: &Array1<T>,
1079 op_type: ReverseOpType,
1080 ) -> Result<()> {
1081 if lhs.len() != rhs.len() {
1082 return Err(OptimError::InvalidConfig(format!(
1083 "{:?} requires equal operand lengths, got {} and {}",
1084 op_type,
1085 lhs.len(),
1086 rhs.len()
1087 )));
1088 }
1089 Ok(())
1090 }
1091
1092 fn matmul_dims(&self, inputs: &[usize]) -> Result<(usize, usize, usize, bool)> {
1094 if inputs.len() != 2 {
1095 return Err(OptimError::ComputationError(
1096 "matmul expects 2 operands".to_string(),
1097 ));
1098 }
1099 let lhs_shape = self.shape_of(inputs[0])?;
1100 let rhs_shape = self.shape_of(inputs[1])?;
1101 if lhs_shape.len() != 2 {
1102 return Err(OptimError::InvalidConfig(format!(
1103 "matmul left operand must be 2-D, got shape {lhs_shape:?}"
1104 )));
1105 }
1106 let (m, k) = (lhs_shape[0], lhs_shape[1]);
1107 let (n, rhs_is_vector) = match rhs_shape.len() {
1108 1 => (1usize, true),
1109 2 => (rhs_shape[1], false),
1110 _ => {
1111 return Err(OptimError::InvalidConfig(format!(
1112 "matmul right operand must be 1-D or 2-D, got shape {rhs_shape:?}"
1113 )))
1114 }
1115 };
1116 let rhs_rows = rhs_shape[0];
1117 if rhs_rows != k {
1118 return Err(OptimError::InvalidConfig(format!(
1119 "matmul inner dimensions disagree: {lhs_shape:?} x {rhs_shape:?}"
1120 )));
1121 }
1122 Ok((m, k, n, rhs_is_vector))
1123 }
1124
1125 fn compute_backward_pass(&self, idx: usize, output_grad: &Array1<T>) -> Result<Vec<Array1<T>>> {
1126 let op = &self.tape[idx];
1127 let inputs = &op.inputs;
1128
1129 match &op.backward_fn {
1130 BackwardFunction::Leaf => Ok(Vec::new()),
1131
1132 BackwardFunction::AddBackward => Ok(vec![output_grad.clone(), output_grad.clone()]),
1133
1134 BackwardFunction::SubtractBackward => {
1135 Ok(vec![output_grad.clone(), output_grad.mapv(|x| -x)])
1136 }
1137
1138 BackwardFunction::MultiplyBackward => {
1139 let (lhs, rhs) = self.binary_operands(inputs)?;
1140 Ok(vec![output_grad * rhs, output_grad * lhs])
1141 }
1142
1143 BackwardFunction::DivideBackward => {
1144 let (lhs, rhs) = self.binary_operands(inputs)?;
1145 let lhs_grad = output_grad / rhs;
1146 let rhs_grad = Array1::from_shape_fn(rhs.len(), |i| {
1147 -output_grad[i] * lhs[i] / (rhs[i] * rhs[i])
1148 });
1149 Ok(vec![lhs_grad, rhs_grad])
1150 }
1151
1152 BackwardFunction::DotBackward => {
1153 let (lhs, rhs) = self.binary_operands(inputs)?;
1154 if output_grad.len() != 1 {
1155 return Err(OptimError::ComputationError(
1156 "dot backward expects a scalar upstream gradient".to_string(),
1157 ));
1158 }
1159 let g = output_grad[0];
1160 Ok(vec![rhs.mapv(|v| v * g), lhs.mapv(|v| v * g)])
1161 }
1162
1163 BackwardFunction::PowerBackward { exponent } => {
1164 let x = self.unary_operand(inputs)?;
1165 let derivative = x.mapv(|v| *exponent * v.powf(*exponent - T::one()));
1166 Ok(vec![output_grad * &derivative])
1167 }
1168
1169 BackwardFunction::ExpBackward => {
1170 let x = self.unary_operand(inputs)?;
1171 let derivative = x.mapv(|v| v.exp());
1172 Ok(vec![output_grad * &derivative])
1173 }
1174
1175 BackwardFunction::LogBackward => {
1176 let x = self.unary_operand(inputs)?;
1177 Ok(vec![output_grad / x])
1178 }
1179
1180 BackwardFunction::TrigBackward { function } => {
1181 let x = self.unary_operand(inputs)?;
1182 let derivative = match function {
1183 TrigFunction::Sin => x.mapv(|v| v.cos()),
1184 TrigFunction::Cos => x.mapv(|v| -v.sin()),
1185 };
1186 Ok(vec![output_grad * &derivative])
1187 }
1188
1189 BackwardFunction::ActivationBackward { function } => {
1190 let x = self.unary_operand(inputs)?;
1191 let derivative = match function {
1192 ActivationFunction::Tanh => {
1193 let tanh_val = x.mapv(|v| v.tanh());
1194 tanh_val.mapv(|y| T::one() - y * y)
1195 }
1196 ActivationFunction::Sigmoid => {
1197 let sigmoid_val = x.mapv(|v| T::one() / (T::one() + (-v).exp()));
1198 sigmoid_val.mapv(|y| y * (T::one() - y))
1199 }
1200 ActivationFunction::ReLU => {
1201 x.mapv(|v| if v > T::zero() { T::one() } else { T::zero() })
1202 }
1203 ActivationFunction::LeakyReLU { alpha } => {
1204 let alpha_t: T =
1205 scirs2_core::numeric::NumCast::from(*alpha).ok_or_else(|| {
1206 OptimError::ComputationError(
1207 "failed to convert leaky-relu slope".to_string(),
1208 )
1209 })?;
1210 x.mapv(|v| if v > T::zero() { T::one() } else { alpha_t })
1211 }
1212 };
1213 Ok(vec![output_grad * &derivative])
1214 }
1215
1216 BackwardFunction::MatMulBackward => {
1217 let (m, k, n, _) = self.matmul_dims(inputs)?;
1218 let (lhs, rhs) = self.binary_operands(inputs)?;
1219 if output_grad.len() != m * n {
1220 return Err(OptimError::ComputationError(format!(
1221 "matmul upstream gradient has {} elements, expected {}",
1222 output_grad.len(),
1223 m * n
1224 )));
1225 }
1226 let mut lhs_grad = Array1::zeros(m * k);
1228 for i in 0..m {
1229 for p in 0..k {
1230 let mut acc = T::zero();
1231 for j in 0..n {
1232 acc = acc + output_grad[i * n + j] * rhs[p * n + j];
1233 }
1234 lhs_grad[i * k + p] = acc;
1235 }
1236 }
1237 let mut rhs_grad = Array1::zeros(k * n);
1239 for p in 0..k {
1240 for j in 0..n {
1241 let mut acc = T::zero();
1242 for i in 0..m {
1243 acc = acc + lhs[i * k + p] * output_grad[i * n + j];
1244 }
1245 rhs_grad[p * n + j] = acc;
1246 }
1247 }
1248 Ok(vec![lhs_grad, rhs_grad])
1249 }
1250
1251 BackwardFunction::ReductionBackward { reduction_type } => {
1252 let x = self.unary_operand(inputs)?;
1253 if output_grad.len() != 1 {
1254 return Err(OptimError::ComputationError(
1255 "reduction backward expects a scalar upstream gradient".to_string(),
1256 ));
1257 }
1258 let g = output_grad[0];
1259 match reduction_type {
1260 ReductionType::Sum => Ok(vec![Array1::from_elem(x.len(), g)]),
1261 ReductionType::Mean => {
1262 if x.is_empty() {
1263 return Err(OptimError::ComputationError(
1264 "mean backward on an empty tensor".to_string(),
1265 ));
1266 }
1267 let n = T::from(x.len()).ok_or_else(|| {
1268 OptimError::ComputationError("failed to convert length".to_string())
1269 })?;
1270 Ok(vec![Array1::from_elem(x.len(), g / n)])
1271 }
1272 ReductionType::Norm => {
1273 let norm = x.iter().fold(T::zero(), |a, &v| a + v * v).sqrt();
1274 if norm <= T::zero() {
1275 Ok(vec![Array1::zeros(x.len())])
1278 } else {
1279 Ok(vec![x.mapv(|v| v * g / norm)])
1280 }
1281 }
1282 }
1283 }
1284
1285 BackwardFunction::ReshapeBackward { .. } => {
1286 let x = self.unary_operand(inputs)?;
1287 if output_grad.len() != x.len() {
1288 return Err(OptimError::ComputationError(format!(
1289 "reshape backward received {} elements, expected {}",
1290 output_grad.len(),
1291 x.len()
1292 )));
1293 }
1294 Ok(vec![output_grad.clone()])
1295 }
1296 }
1297 }
1298
1299 pub fn get_tape_stats(&self) -> ReverseModeStats {
1301 ReverseModeStats {
1302 tape_length: self.tape.len(),
1303 num_variables: self.variables.len(),
1304 num_gradients: self.gradients.iter().filter(|g| g.is_some()).count(),
1305 memory_usage_estimate: self.estimate_memory_usage(),
1306 cache_size: self.cache.len(),
1307 }
1308 }
1309
1310 fn estimate_memory_usage(&self) -> usize {
1311 let tape_size = self.tape.len() * std::mem::size_of::<ReverseOperation<T>>();
1312 let value_size = self
1313 .values
1314 .iter()
1315 .map(|v| v.len() * std::mem::size_of::<T>())
1316 .sum::<usize>();
1317 let gradient_size = self
1318 .gradients
1319 .iter()
1320 .filter_map(|g| g.as_ref())
1321 .map(|g| g.len() * std::mem::size_of::<T>())
1322 .sum::<usize>();
1323
1324 tape_size + value_size + gradient_size
1325 }
1326}
1327
1328fn matmul_flat<T: Float + Clone>(
1330 lhs: &Array1<T>,
1331 m: usize,
1332 k: usize,
1333 rhs: &Array1<T>,
1334 n: usize,
1335) -> Array1<T> {
1336 let mut out = Array1::zeros(m * n);
1337 for i in 0..m {
1338 for j in 0..n {
1339 let mut acc = T::zero();
1340 for p in 0..k {
1341 acc = acc + lhs[i * k + p] * rhs[p * n + j];
1342 }
1343 out[i * n + j] = acc;
1344 }
1345 }
1346 out
1347}
1348
1349#[derive(Debug, Clone)]
1351pub struct ReverseModeStats {
1352 pub tape_length: usize,
1353 pub num_variables: usize,
1354 pub num_gradients: usize,
1355 pub memory_usage_estimate: usize,
1356 pub cache_size: usize,
1357}
1358
1359pub struct GradientAccumulator<T: Float + Debug + Send + Sync + 'static> {
1361 gradients: HashMap<String, Array1<T>>,
1363
1364 count: usize,
1366}
1367
1368impl<T: Float + Debug + Default + Clone + scirs2_core::ndarray::ScalarOperand + Send + Sync> Default
1369 for GradientAccumulator<T>
1370{
1371 fn default() -> Self {
1372 Self::new()
1373 }
1374}
1375
1376impl<T: Float + Debug + Default + Clone + scirs2_core::ndarray::ScalarOperand + Send + Sync>
1377 GradientAccumulator<T>
1378{
1379 pub fn new() -> Self {
1380 Self {
1381 gradients: HashMap::new(),
1382 count: 0,
1383 }
1384 }
1385
1386 pub fn accumulate(&mut self, gradients: HashMap<String, Array1<T>>) {
1388 for (name, grad) in gradients {
1389 if let Some(existing) = self.gradients.get_mut(&name) {
1390 *existing = &*existing + &grad;
1391 } else {
1392 self.gradients.insert(name, grad);
1393 }
1394 }
1395 self.count += 1;
1396 }
1397
1398 pub fn get_averaged_gradients(&self) -> HashMap<String, Array1<T>> {
1400 if self.count == 0 {
1401 return HashMap::new();
1402 }
1403
1404 let count_t: T =
1405 scirs2_core::numeric::NumCast::from(self.count).unwrap_or_else(|| T::one());
1406 self.gradients
1407 .iter()
1408 .map(|(name, grad)| (name.clone(), grad / count_t))
1409 .collect()
1410 }
1411
1412 pub fn clear(&mut self) {
1414 self.gradients.clear();
1415 self.count = 0;
1416 }
1417}
1418
1419#[cfg(test)]
1420mod tests {
1421 use super::*;
1422
1423 #[test]
1424 fn test_reverse_mode_engine_creation() {
1425 let engine = ReverseModeEngine::<f64>::new();
1426 assert!(engine.is_recording());
1427 assert_eq!(engine.tape_len(), 0);
1428 }
1429
1430 #[test]
1431 fn test_variable_creation() {
1432 let mut engine = ReverseModeEngine::new();
1433 let x_val = Array1::from_vec(vec![2.0, 3.0]);
1434 let x_id = engine.create_variable("x", x_val, true);
1435
1436 assert_eq!(x_id, 0);
1437 assert!(engine.value_by_name("x").is_ok());
1438 assert!(engine.get_gradient(x_id).is_some());
1439 }
1440
1441 #[test]
1442 fn test_simple_backward_pass() {
1443 let mut engine = ReverseModeEngine::new();
1444
1445 let x_id = engine.create_variable("x", Array1::from_vec(vec![2.0]), true);
1446 let y_id = engine.create_variable("y", Array1::from_vec(vec![3.0]), true);
1447
1448 let sum_id = engine.add(x_id, y_id).expect("add op");
1449 assert!((engine.scalar_value(sum_id).expect("value") - 5.0).abs() < 1e-12);
1450
1451 engine
1452 .backward(sum_id, Some(Array1::from_vec(vec![1.0])))
1453 .expect("backward");
1454
1455 let x_grad = engine.get_gradient(x_id).expect("x gradient");
1456 let y_grad = engine.get_gradient(y_id).expect("y gradient");
1457
1458 assert_eq!(x_grad[0], 1.0);
1459 assert_eq!(y_grad[0], 1.0);
1460 }
1461
1462 #[test]
1463 fn test_gradient_accumulator() {
1464 let mut accumulator = GradientAccumulator::<f64>::new();
1465
1466 let mut grad1 = HashMap::new();
1467 grad1.insert("x".to_string(), Array1::from_vec(vec![1.0, 2.0]));
1468
1469 let mut grad2 = HashMap::new();
1470 grad2.insert("x".to_string(), Array1::from_vec(vec![3.0, 4.0]));
1471
1472 accumulator.accumulate(grad1);
1473 accumulator.accumulate(grad2);
1474
1475 let averaged = accumulator.get_averaged_gradients();
1476 let x_avg = &averaged["x"];
1477
1478 assert_eq!(x_avg[0], 2.0);
1479 assert_eq!(x_avg[1], 3.0);
1480 }
1481
1482 #[test]
1483 fn test_tape_statistics() {
1484 let mut engine = ReverseModeEngine::new();
1485
1486 let x_id = engine.create_variable("x", Array1::from_vec(vec![1.0]), true);
1487 let _exp_id = engine.exp(x_id).expect("exp op");
1488
1489 let stats = engine.get_tape_stats();
1490 assert_eq!(stats.tape_length, 2);
1491 assert_eq!(stats.num_variables, 1);
1492 }
1493
1494 #[test]
1495 fn test_reverse_mode_gradient_of_product() {
1496 let mut engine = ReverseModeEngine::<f64>::new();
1497
1498 let x_id = engine.create_variable("x", Array1::from_vec(vec![5.0]), true);
1499 let y_id = engine.create_variable("y", Array1::from_vec(vec![7.0]), true);
1500
1501 let prod_id = engine.multiply(x_id, y_id).expect("multiply op");
1502
1503 engine
1504 .backward(prod_id, Some(Array1::from_vec(vec![1.0])))
1505 .expect("backward");
1506
1507 let x_grad = engine.get_gradient(x_id).expect("x gradient");
1508 let y_grad = engine.get_gradient(y_id).expect("y gradient");
1509
1510 approx::assert_abs_diff_eq!(x_grad[0], 7.0, epsilon = 1e-10);
1511 approx::assert_abs_diff_eq!(y_grad[0], 5.0, epsilon = 1e-10);
1512 }
1513
1514 #[test]
1519 fn test_forward_values_materialised_for_composites() {
1520 let mut engine = ReverseModeEngine::<f64>::new();
1521
1522 let x_id = engine.create_variable("x", Array1::from_vec(vec![0.5]), true);
1523 let y_id = engine.create_variable("y", Array1::from_vec(vec![0.25]), true);
1524
1525 let sum_id = engine.add(x_id, y_id).expect("add");
1526 let exp_id = engine.exp(sum_id).expect("exp");
1527
1528 let expected = 0.75f64.exp();
1529 approx::assert_abs_diff_eq!(
1530 engine.scalar_value(exp_id).expect("value"),
1531 expected,
1532 epsilon = 1e-12
1533 );
1534
1535 engine.backward(exp_id, None).expect("backward");
1536
1537 approx::assert_abs_diff_eq!(
1538 engine.get_gradient(x_id).expect("x grad")[0],
1539 expected,
1540 epsilon = 1e-10
1541 );
1542 approx::assert_abs_diff_eq!(
1543 engine.get_gradient(y_id).expect("y grad")[0],
1544 expected,
1545 epsilon = 1e-10
1546 );
1547 }
1548
1549 #[test]
1552 fn test_backward_from_earlier_node_does_not_panic() {
1553 let mut engine = ReverseModeEngine::<f64>::new();
1554
1555 let x_id = engine.create_variable("x", Array1::from_vec(vec![1.0]), true);
1556 let _exp_id = engine.exp(x_id).expect("exp");
1558
1559 engine
1560 .backward(x_id, None)
1561 .expect("backward must not panic");
1562
1563 let x_grad = engine.get_gradient(x_id).expect("x grad");
1564 approx::assert_abs_diff_eq!(x_grad[0], 1.0, epsilon = 1e-12);
1565 }
1566
1567 #[test]
1570 fn test_matmul_gradient_matches_finite_difference() {
1571 let a = vec![0.5, -1.5, 2.0, 0.25, 1.0, -0.75]; let b = vec![1.5, -0.5, 0.25, 2.0, -1.0, 0.75]; let build = |a: &[f64], b: &[f64]| -> (ReverseModeEngine<f64>, usize, usize, usize) {
1575 let mut engine = ReverseModeEngine::<f64>::new();
1576 let a_id = engine
1577 .create_variable_with_shape("a", Array1::from_vec(a.to_vec()), &[2, 3], true)
1578 .expect("2x3 variable `a` registers");
1579 let b_id = engine
1580 .create_variable_with_shape("b", Array1::from_vec(b.to_vec()), &[3, 2], true)
1581 .expect("3x2 variable `b` registers");
1582 let mm = engine.matmul(a_id, b_id).expect("matmul");
1583 let loss = engine.sum(mm, None).expect("sum");
1584 (engine, a_id, b_id, loss)
1585 };
1586
1587 let (mut engine, a_id, b_id, loss_id) = build(&a, &b);
1588 let product = engine
1590 .value(engine.tape_len() - 2)
1591 .expect("product")
1592 .clone();
1593 approx::assert_abs_diff_eq!(product[0], 0.5 * 1.5 + (-1.5) * 0.25 - 2.0, epsilon = 1e-12);
1594
1595 engine.backward(loss_id, None).expect("backward");
1596 let grad_a = engine.get_gradient(a_id).expect("grad a").clone();
1597 let grad_b = engine.get_gradient(b_id).expect("grad b").clone();
1598
1599 let loss_at = |a: &[f64], b: &[f64]| -> f64 {
1600 let (engine, _, _, loss_id) = build(a, b);
1601 engine.scalar_value(loss_id).expect("loss")
1602 };
1603
1604 let h = 1e-6;
1605 for i in 0..a.len() {
1606 let mut ap = a.clone();
1607 let mut am = a.clone();
1608 ap[i] += h;
1609 am[i] -= h;
1610 let fd = (loss_at(&ap, &b) - loss_at(&am, &b)) / (2.0 * h);
1611 approx::assert_abs_diff_eq!(grad_a[i], fd, epsilon = 1e-6);
1612 }
1613 for i in 0..b.len() {
1614 let mut bp = b.clone();
1615 let mut bm = b.clone();
1616 bp[i] += h;
1617 bm[i] -= h;
1618 let fd = (loss_at(&a, &bp) - loss_at(&a, &bm)) / (2.0 * h);
1619 approx::assert_abs_diff_eq!(grad_b[i], fd, epsilon = 1e-6);
1620 }
1621 }
1622
1623 #[test]
1625 fn test_matmul_matrix_vector() {
1626 let mut engine = ReverseModeEngine::<f64>::new();
1627 let w = engine
1628 .create_variable_with_shape(
1629 "w",
1630 Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]),
1631 &[2, 3],
1632 true,
1633 )
1634 .expect("2x3 variable `w` registers");
1635 let x = engine.create_variable("x", Array1::from_vec(vec![1.0, 0.0, -1.0]), false);
1636 let y = engine.matmul(w, x).expect("matmul");
1637
1638 let value = engine.value(y).expect("value");
1639 assert_eq!(value.len(), 2);
1640 approx::assert_abs_diff_eq!(value[0], 1.0 - 3.0, epsilon = 1e-12);
1641 approx::assert_abs_diff_eq!(value[1], 4.0 - 6.0, epsilon = 1e-12);
1642
1643 engine
1644 .backward(y, Some(Array1::from_vec(vec![1.0, 1.0])))
1645 .expect("backward");
1646 let gw = engine.get_gradient(w).expect("grad w");
1647 for row in 0..2 {
1649 approx::assert_abs_diff_eq!(gw[row * 3], 1.0, epsilon = 1e-12);
1650 approx::assert_abs_diff_eq!(gw[row * 3 + 1], 0.0, epsilon = 1e-12);
1651 approx::assert_abs_diff_eq!(gw[row * 3 + 2], -1.0, epsilon = 1e-12);
1652 }
1653 }
1654
1655 #[test]
1658 fn test_mismatched_lengths_error() {
1659 let mut engine = ReverseModeEngine::<f64>::new();
1660 let a = engine.create_variable("a", Array1::from_vec(vec![1.0, 2.0]), true);
1661 let b = engine.create_variable("b", Array1::from_vec(vec![1.0]), true);
1662 assert!(engine.add(a, b).is_err());
1663 assert_eq!(engine.tape_len(), 2);
1665 }
1666
1667 #[test]
1670 fn test_set_variable_value_and_reforward() {
1671 let mut engine = ReverseModeEngine::<f64>::new();
1672 let x = engine.create_variable("x", Array1::from_vec(vec![2.0]), true);
1673 let sq = engine.multiply(x, x).expect("square");
1674
1675 engine.backward(sq, None).expect("backward");
1676 approx::assert_abs_diff_eq!(
1677 engine.get_gradient(x).expect("grad")[0],
1678 4.0,
1679 epsilon = 1e-12
1680 );
1681
1682 engine
1683 .set_variable_value_by_name("x", Array1::from_vec(vec![5.0]))
1684 .expect("assign");
1685 engine.backward(sq, None).expect("backward again");
1686 approx::assert_abs_diff_eq!(
1687 engine.scalar_value(sq).expect("value"),
1688 25.0,
1689 epsilon = 1e-12
1690 );
1691 approx::assert_abs_diff_eq!(
1692 engine.get_gradient(x).expect("grad")[0],
1693 10.0,
1694 epsilon = 1e-12
1695 );
1696 }
1697}