Skip to main content

optirs_learned/
reverse_mode.rs

1//! Reverse-mode automatic differentiation (backpropagation).
2//!
3//! This module implements a tape-based reverse-mode automatic differentiation
4//! engine used for gradient computation in the learned-optimizer and
5//! meta-learning code paths of this crate.
6//!
7//! # Design
8//!
9//! Every operation appends exactly one node to the tape and *immediately*
10//! evaluates its forward value, which is stored in [`ReverseModeEngine`]'s
11//! parallel `values` vector. Because a node's index always equals `tape.len()`
12//! at the moment it is created, every node's index is strictly greater than the
13//! indices of all of its inputs. That invariant is what makes a simple reverse
14//! iteration over the tape a valid topological order for backpropagation, and
15//! it is why an operation is recorded even when recording is disabled (in that
16//! case the node is recorded as a *constant*, which stops gradient flow without
17//! breaking the index invariant).
18//!
19//! Values are flat [`Array1`] buffers plus an explicit logical shape, so 2-D
20//! matrices are stored row-major and matrix multiplication is expressed through
21//! the recorded shapes.
22
23use 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
31/// Trait bundle required by the reverse-mode engine.
32///
33/// This exists purely to keep the (long) bound lists readable.
34pub 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
60/// Reverse-mode AD engine (gradient tape)
61pub struct ReverseModeEngine<T: ReverseScalar> {
62    /// Computation tape for the reverse pass
63    tape: Vec<ReverseOperation<T>>,
64
65    /// Forward value of every tape node (parallel to `tape`)
66    values: Vec<Array1<T>>,
67
68    /// Logical shape of every tape node (parallel to `tape`)
69    shapes: Vec<Vec<usize>>,
70
71    /// Whether each node is a differentiable leaf
72    requiresgrad: Vec<bool>,
73
74    /// Variable registry (name -> tape index)
75    variables: HashMap<String, usize>,
76
77    /// Gradient storage
78    gradients: Vec<Option<Array1<T>>>,
79
80    /// Current recording state
81    recording: bool,
82
83    /// Higher-order gradient tracking
84    higher_order: bool,
85
86    /// Set when a leaf value changed and the tape needs re-evaluation
87    dirty: bool,
88
89    /// Gradient computation cache
90    cache: HashMap<usize, Array1<T>>,
91}
92
93/// Reverse-mode operation on the tape
94#[derive(Debug, Clone)]
95struct ReverseOperation<T: Float + Debug + Send + Sync + 'static> {
96    /// Operation type
97    op_type: ReverseOpType,
98
99    /// Input variable indices
100    inputs: Vec<usize>,
101
102    /// Backward function for gradient computation
103    backward_fn: BackwardFunction<T>,
104
105    /// Detached nodes are evaluated in the forward pass but block gradient flow
106    detached: bool,
107}
108
109/// Reverse operation types
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111enum ReverseOpType {
112    /// Differentiable leaf
113    Variable,
114    /// Non-differentiable leaf (also used for detached nodes)
115    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/// Backward function for computing gradients
138#[derive(Debug, Clone)]
139enum BackwardFunction<T: Float + Debug + Send + Sync + 'static> {
140    /// Leaf node: no inputs, nothing to propagate
141    Leaf,
142
143    /// Addition backward: gradient flows through unchanged
144    AddBackward,
145
146    /// Subtraction backward: negate gradient for the second operand
147    SubtractBackward,
148
149    /// Element-wise multiplication backward
150    MultiplyBackward,
151
152    /// Element-wise division backward
153    DivideBackward,
154
155    /// Dot-product backward
156    DotBackward,
157
158    /// Power backward: multiply by the derivative
159    PowerBackward { exponent: T },
160
161    /// Exponential backward
162    ExpBackward,
163
164    /// Logarithm backward
165    LogBackward,
166
167    /// Trigonometric function backward
168    TrigBackward { function: TrigFunction },
169
170    /// Activation function backward
171    ActivationBackward { function: ActivationFunction },
172
173    /// Matrix multiplication backward
174    MatMulBackward,
175
176    /// Reduction backward
177    ReductionBackward { reduction_type: ReductionType },
178
179    /// Reshape backward (the target shape is needed to re-evaluate the node)
180    ReshapeBackward { newshape: Vec<usize> },
181}
182
183/// Trigonometric functions
184#[derive(Debug, Clone, Copy)]
185enum TrigFunction {
186    Sin,
187    Cos,
188}
189
190/// Activation functions
191#[derive(Debug, Clone, Copy)]
192enum ActivationFunction {
193    Tanh,
194    Sigmoid,
195    ReLU,
196    LeakyReLU { alpha: f64 },
197}
198
199/// Reduction types
200#[derive(Debug, Clone, Copy)]
201enum ReductionType {
202    Sum,
203    Mean,
204    Norm,
205}
206
207/// Gradient computation context
208#[derive(Debug, Clone)]
209pub struct GradientContext<T: Float + Debug + Send + Sync + 'static> {
210    /// Variables requiring gradients
211    pub requiresgrad: HashMap<usize, bool>,
212
213    /// Gradient accumulation mode
214    pub accumulate: bool,
215
216    /// Retain computation graph
217    pub retain_graph: bool,
218
219    /// Create computation graph
220    pub create_graph: bool,
221
222    /// Type parameter marker
223    _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    /// Create a new reverse-mode AD engine
246    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    /// Enable/disable gradient recording.
262    ///
263    /// While recording is disabled, subsequent operations are still evaluated
264    /// and appended to the tape, but they are recorded as *constants*: they
265    /// keep the tape's index invariant intact while blocking gradient flow.
266    pub fn set_recording(&mut self, recording: bool) {
267        self.recording = recording;
268    }
269
270    /// Check if recording is enabled
271    pub fn is_recording(&self) -> bool {
272        self.recording
273    }
274
275    /// Enable higher-order gradients
276    pub fn enable_higher_order(&mut self, enabled: bool) {
277        self.higher_order = enabled;
278    }
279
280    /// Whether higher-order gradient tracking is enabled
281    pub fn higher_order_enabled(&self) -> bool {
282        self.higher_order
283    }
284
285    /// Clear the computation tape
286    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    /// Number of nodes currently on the tape
298    pub fn tape_len(&self) -> usize {
299        self.tape.len()
300    }
301
302    // ---------------------------------------------------------------- leaves
303
304    /// Create a variable with gradient tracking (1-D, shape `[value.len()]`)
305    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    /// Create a variable with an explicit logical shape.
311    ///
312    /// `value` is the row-major flattening of a tensor whose logical shape is
313    /// `shape`; the product of `shape` must equal `value.len()`.
314    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    /// Create a constant (no gradient tracking)
333    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    /// Replace the value of an existing leaf variable.
376    ///
377    /// The tape is marked dirty; [`ReverseModeEngine::forward`] (called
378    /// automatically by [`ReverseModeEngine::backward`]) re-evaluates every
379    /// dependent node.
380    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    /// Replace the value of an existing leaf variable, by name
407    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    // ------------------------------------------------------------ operations
416
417    /// Addition operation
418    pub fn add(&mut self, lhs: usize, rhs: usize) -> Result<usize> {
419        self.binary_op(ReverseOpType::Add, lhs, rhs, BackwardFunction::AddBackward)
420    }
421
422    /// Subtraction operation
423    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    /// Element-wise multiplication operation
433    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    /// Element-wise division operation
443    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    /// Power operation
453    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    /// Exponential function
462    pub fn exp(&mut self, input: usize) -> Result<usize> {
463        self.unary_op(ReverseOpType::Exp, input, BackwardFunction::ExpBackward)
464    }
465
466    /// Natural logarithm
467    pub fn log(&mut self, input: usize) -> Result<usize> {
468        self.unary_op(ReverseOpType::Log, input, BackwardFunction::LogBackward)
469    }
470
471    /// Sine function
472    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    /// Cosine function
483    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    /// Hyperbolic tangent
494    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    /// Sigmoid function
505    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    /// ReLU function
516    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    /// Leaky ReLU function
527    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    /// Matrix multiplication.
538    ///
539    /// `lhs` must have logical shape `[m, k]`; `rhs` must have shape `[k, n]`
540    /// or `[k]` (interpreted as a `[k, 1]` column). The result has shape
541    /// `[m, n]` (or `[m]` when `rhs` was 1-D).
542    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    /// Dot product of two vectors of equal length; produces a scalar node.
552    pub fn dot(&mut self, lhs: usize, rhs: usize) -> Result<usize> {
553        self.binary_op(ReverseOpType::Dot, lhs, rhs, BackwardFunction::DotBackward)
554    }
555
556    /// Sum reduction over all elements
557    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    /// Mean reduction over all elements
568    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    /// L2 norm over all elements
579    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    /// Reshape operation (element count must be preserved)
590    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    // ----------------------------------------------------------- forward pass
608
609    /// Re-evaluate every non-leaf node of the tape from the current leaf values.
610    ///
611    /// Nodes are visited in tape order, which is a valid topological order
612    /// because every node's index exceeds all of its inputs' indices.
613    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    /// Forward value of a node
627    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    /// Forward value of a node, by variable name
634    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    /// Logical shape of a node
643    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    /// Scalar value of a node that holds exactly one element
651    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    // ---------------------------------------------------------- backward pass
663
664    /// Backward pass: compute gradients of `outputid` w.r.t. every leaf.
665    ///
666    /// If `gradient` is `None`, the output is seeded with ones matching the
667    /// output's length.
668    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        // Gradient storage must cover the whole tape: a node created *after*
681        // `outputid` is still visited by the reverse iteration below.
682        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        // Reverse pass through the tape.
709        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    /// Get gradient for a variable
754    pub fn get_gradient(&self, varid: usize) -> Option<&Array1<T>> {
755        self.gradients.get(varid)?.as_ref()
756    }
757
758    /// Get gradient by variable name
759    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    /// Zero all gradients
765    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    /// Get all named variable gradients
773    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    // -------------------------------------------------------------- internals
786
787    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        // When recording is off the node is *detached*: it is still evaluated
822        // and still records its inputs (so the forward pass stays correct and
823        // the index invariant holds), but the reverse sweep skips it, so no
824        // gradient flows through it.
825        self.tape.push(ReverseOperation {
826            op_type,
827            inputs: inputs.clone(),
828            backward_fn: backward_fn.clone(),
829            detached: !self.recording,
830        });
831        // Placeholder slots; overwritten by `evaluate_op` below.
832        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                // Roll the partially recorded node back so the tape stays consistent.
841                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    /// Re-evaluate node `idx` from its recorded inputs.
856    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    /// Resolve `(m, k, n, rhs_is_vector)` for a matmul node.
1093    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                // dL/dA = G · Bᵀ  (m×n · n×k -> m×k)
1227                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                // dL/dB = Aᵀ · G  (k×m · m×n -> k×n)
1238                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                            // d‖x‖/dx is undefined at the origin; the
1276                            // sub-gradient 0 is the conventional choice.
1277                            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    /// Get tape statistics
1300    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
1328/// Row-major matrix product of a flat `m x k` buffer with a flat `k x n` buffer.
1329fn 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/// Reverse-mode AD statistics
1350#[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
1359/// Gradient accumulation utilities
1360pub struct GradientAccumulator<T: Float + Debug + Send + Sync + 'static> {
1361    /// Accumulated gradients
1362    gradients: HashMap<String, Array1<T>>,
1363
1364    /// Accumulation count
1365    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    /// Accumulate gradients
1387    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    /// Get averaged gradients
1399    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    /// Clear accumulated gradients
1413    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    /// F17 regression: a composite expression needs every intermediate node's
1515    /// forward value, not just depth-1 leaves.
1516    ///
1517    /// f(x, y) = exp(x + y); df/dx = df/dy = exp(x + y).
1518    #[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    /// F19 regression: creating nodes *after* the node being differentiated
1550    /// used to index `gradients` out of bounds during the reverse sweep.
1551    #[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        // Extends the tape past `x_id` without extending gradient storage.
1557        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    /// F18 regression: matmul gradients checked against central finite
1568    /// differences of the scalar loss `L = sum(A · B)`.
1569    #[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]; // 2x3
1572        let b = vec![1.5, -0.5, 0.25, 2.0, -1.0, 0.75]; // 3x2
1573
1574        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        // Reference product, checked by hand for the first entry.
1589        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    /// Matrix times column vector, the shape used by the linear meta-models.
1624    #[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        // dL/dW = g xᵀ with g = [1, 1] and x = [1, 0, -1].
1648        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    /// Mismatched operand lengths must be a hard error, not a panic and not a
1656    /// silently wrong gradient.
1657    #[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        // The failed op must not have been left on the tape.
1664        assert_eq!(engine.tape_len(), 2);
1665    }
1666
1667    /// Re-running `forward` after assigning new leaf values must refresh every
1668    /// intermediate value and give fresh gradients.
1669    #[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}