Skip to main content

optirs_learned/
forward_mode.rs

1use std::fmt::Debug;
2// Forward-mode automatic differentiation
3//
4// This module implements forward-mode automatic differentiation for computing
5// directional derivatives and Jacobian-vector products efficiently.
6
7use scirs2_core::ndarray::{Array1, Array2};
8use scirs2_core::numeric::Float;
9use std::collections::HashMap;
10
11use crate::error::{OptimError, Result};
12
13/// Dual number for forward-mode automatic differentiation
14#[derive(Debug, Clone)]
15pub struct DualNumber<T: Float + Debug + Send + Sync + 'static> {
16    /// Primal value
17    pub value: T,
18
19    /// Tangent (derivative) value
20    pub tangent: T,
21}
22
23/// Multi-dimensional dual number for vector-valued functions
24#[derive(Debug, Clone)]
25pub struct VectorDual<T: Float + Debug + Send + Sync + 'static> {
26    /// Primal value (vector)
27    pub value: Array1<T>,
28
29    /// Tangent matrix (Jacobian-vector product)
30    pub tangent: Array1<T>,
31}
32
33/// Forward-mode AD engine
34pub struct ForwardModeEngine<
35    T: Float + Debug + Default + Clone + Send + Sync + std::iter::Sum + 'static,
36> {
37    /// Computation graph
38    tape: Vec<ForwardOperation<T>>,
39
40    /// Variable registry (name -> tape index)
41    variables: HashMap<String, usize>,
42
43    /// Variable names in creation order.
44    ///
45    /// The seed/tangent space is the concatenation of the variables' values in
46    /// **this** order, so a variable's seed ordinal is its position here — it
47    /// is stable and independent of how many intermediate operations were
48    /// recorded between two variable creations (which is what the tape index
49    /// would have measured).
50    variable_order: Vec<String>,
51
52    /// Current seed vectors for directional derivatives
53    seed_vectors: Vec<Array1<T>>,
54
55    /// Enable higher-order derivatives
56    higher_order: bool,
57}
58
59/// Forward-mode operation
60#[derive(Debug, Clone)]
61struct ForwardOperation<T: Float + Debug + Send + Sync + 'static> {
62    /// Operation type
63    optype: ForwardOpType,
64
65    /// Input variable indices
66    inputs: Vec<usize>,
67
68    /// Operation metadata
69    metadata: ForwardOpMetadata<T>,
70}
71
72/// Forward operation types
73#[derive(Debug, Clone)]
74enum ForwardOpType {
75    Variable,
76    Constant,
77    Add,
78    Subtract,
79    Multiply,
80    Divide,
81    Power,
82    Exp,
83    Log,
84    Sin,
85    Cos,
86    Tanh,
87    Sigmoid,
88    ReLU,
89    MatMul,
90    Dot,
91    Sum,
92    Mean,
93    Norm,
94}
95
96/// Operation metadata for forward mode
97#[derive(Debug, Clone)]
98struct ForwardOpMetadata<T: Float + Debug + Send + Sync + 'static> {
99    /// Logical shape of the operation's output
100    shape: Vec<usize>,
101
102    /// Operation-specific data
103    data: ForwardOpData<T>,
104}
105
106/// Operation-specific data
107#[derive(Debug, Clone)]
108enum ForwardOpData<T: Float + Debug + Send + Sync + 'static> {
109    None,
110    /// The full constant tensor (flat, row-major)
111    ConstantVector(Array1<T>),
112    PowerExponent(T),
113    /// `lhs` is `m x k`, `rhs` is `k x n`, output is `m x n`
114    MatMulDims {
115        m: usize,
116        k: usize,
117        n: usize,
118    },
119}
120
121impl<T: Float + Debug + Default + Clone + Send + Sync + 'static> DualNumber<T> {
122    /// Create a new dual number
123    pub fn new(value: T, tangent: T) -> Self {
124        Self { value, tangent }
125    }
126
127    /// Create a constant (zero tangent)
128    pub fn constant(value: T) -> Self {
129        Self::new(value, T::zero())
130    }
131
132    /// Create a variable (unit tangent)
133    pub fn variable(value: T) -> Self {
134        Self::new(value, T::one())
135    }
136}
137
138impl<T: Float + Debug + Default + Clone + Send + Sync + 'static> VectorDual<T> {
139    /// Create a new vector dual number
140    pub fn new(value: Array1<T>, tangent: Array1<T>) -> Self {
141        Self { value, tangent }
142    }
143
144    /// Create a constant vector
145    pub fn constant(value: Array1<T>) -> Self {
146        let tangent = Array1::zeros(value.len());
147        Self::new(value, tangent)
148    }
149
150    /// Create a variable vector with unit tangent in direction i
151    pub fn variable(value: Array1<T>, direction: usize) -> Self {
152        let mut tangent = Array1::zeros(value.len());
153        if direction < tangent.len() {
154            tangent[direction] = T::one();
155        }
156        Self::new(value, tangent)
157    }
158}
159
160impl<T: Float + Debug + Default + Clone + Send + Sync + std::iter::Sum + 'static> Default
161    for ForwardModeEngine<T>
162{
163    fn default() -> Self {
164        Self::new()
165    }
166}
167
168impl<T: Float + Debug + Default + Clone + Send + Sync + std::iter::Sum + 'static>
169    ForwardModeEngine<T>
170{
171    /// Create a new forward-mode AD engine
172    pub fn new() -> Self {
173        Self {
174            tape: Vec::new(),
175            variables: HashMap::new(),
176            variable_order: Vec::new(),
177            seed_vectors: Vec::new(),
178            higher_order: false,
179        }
180    }
181
182    /// Enable higher-order derivatives
183    pub fn enable_higher_order(&mut self, enabled: bool) {
184        self.higher_order = enabled;
185    }
186
187    /// Whether higher-order derivative tracking is enabled
188    pub fn higher_order_enabled(&self) -> bool {
189        self.higher_order
190    }
191
192    /// Set seed vectors for computing directional derivatives
193    pub fn set_seed_vectors(&mut self, seeds: Vec<Array1<T>>) {
194        self.seed_vectors = seeds;
195    }
196
197    /// Seed vectors previously registered with [`Self::set_seed_vectors`]
198    pub fn seed_vectors(&self) -> &[Array1<T>] {
199        &self.seed_vectors
200    }
201
202    /// Variable names in creation order (the seed-space layout)
203    pub fn variable_order(&self) -> &[String] {
204        &self.variable_order
205    }
206
207    /// Declared logical shape of a tape node, when the builder recorded one.
208    ///
209    /// Leaves and matrix products carry an explicit shape; element-wise
210    /// operations infer theirs from their operands during the forward pass and
211    /// therefore report `None`.
212    pub fn declared_shape(&self, node_id: usize) -> Result<Option<&[usize]>> {
213        let op = self
214            .tape
215            .get(node_id)
216            .ok_or_else(|| OptimError::InvalidConfig(format!("invalid node id {node_id}")))?;
217        if op.metadata.shape.is_empty() {
218            Ok(None)
219        } else {
220            Ok(Some(op.metadata.shape.as_slice()))
221        }
222    }
223
224    /// Create a variable.
225    ///
226    /// Re-creating a variable under an existing name rebinds the name to the
227    /// new tape node but keeps its original seed ordinal.
228    pub fn create_variable(&mut self, name: &str, value: Array1<T>) -> usize {
229        let var_id = self.tape.len();
230
231        let op = ForwardOperation {
232            optype: ForwardOpType::Variable,
233            inputs: Vec::new(),
234            metadata: ForwardOpMetadata {
235                shape: value.shape().to_vec(),
236                data: ForwardOpData::None,
237            },
238        };
239
240        self.tape.push(op);
241        if self.variables.insert(name.to_string(), var_id).is_none() {
242            self.variable_order.push(name.to_string());
243        }
244        var_id
245    }
246
247    /// Create a constant
248    pub fn create_constant(&mut self, value: Array1<T>) -> usize {
249        let const_id = self.tape.len();
250
251        let op = ForwardOperation {
252            optype: ForwardOpType::Constant,
253            inputs: Vec::new(),
254            metadata: ForwardOpMetadata {
255                shape: value.shape().to_vec(),
256                data: ForwardOpData::ConstantVector(value),
257            },
258        };
259
260        self.tape.push(op);
261        const_id
262    }
263
264    /// Total dimension of the seed / tangent space for the given inputs.
265    ///
266    /// This is the sum of the input lengths taken in variable-creation order.
267    pub fn input_dimension(&self, inputs: &HashMap<String, Array1<T>>) -> Result<usize> {
268        let mut total = 0usize;
269        for name in &self.variable_order {
270            let value = inputs.get(name).ok_or_else(|| {
271                OptimError::InvalidConfig(format!("input value for '{name}' not provided"))
272            })?;
273            total += value.len();
274        }
275        Ok(total)
276    }
277
278    /// Offset of each variable inside the seed / tangent space.
279    fn seed_offsets(&self, inputs: &HashMap<String, Array1<T>>) -> Result<HashMap<String, usize>> {
280        let mut offsets = HashMap::new();
281        let mut cursor = 0usize;
282        for name in &self.variable_order {
283            let value = inputs.get(name).ok_or_else(|| {
284                OptimError::InvalidConfig(format!("input value for '{name}' not provided"))
285            })?;
286            offsets.insert(name.clone(), cursor);
287            cursor += value.len();
288        }
289        Ok(offsets)
290    }
291
292    /// Add two variables
293    pub fn add(&mut self, lhs: usize, rhs: usize) -> Result<usize> {
294        self.binary_op(ForwardOpType::Add, lhs, rhs)
295    }
296
297    /// Subtract two variables
298    pub fn subtract(&mut self, lhs: usize, rhs: usize) -> Result<usize> {
299        self.binary_op(ForwardOpType::Subtract, lhs, rhs)
300    }
301
302    /// Multiply two variables
303    pub fn multiply(&mut self, lhs: usize, rhs: usize) -> Result<usize> {
304        self.binary_op(ForwardOpType::Multiply, lhs, rhs)
305    }
306
307    /// Divide two variables
308    pub fn divide(&mut self, lhs: usize, rhs: usize) -> Result<usize> {
309        self.binary_op(ForwardOpType::Divide, lhs, rhs)
310    }
311
312    /// Raise to power
313    pub fn power(&mut self, base: usize, exponent: T) -> Result<usize> {
314        let output_id = self.tape.len();
315
316        let op = ForwardOperation {
317            optype: ForwardOpType::Power,
318            inputs: vec![base],
319            metadata: ForwardOpMetadata {
320                shape: Vec::new(), // Inferred from the operand during the forward pass
321                data: ForwardOpData::PowerExponent(exponent),
322            },
323        };
324
325        self.tape.push(op);
326        Ok(output_id)
327    }
328
329    /// Exponential function
330    pub fn exp(&mut self, input: usize) -> Result<usize> {
331        self.unary_op(ForwardOpType::Exp, input)
332    }
333
334    /// Natural logarithm
335    pub fn log(&mut self, input: usize) -> Result<usize> {
336        self.unary_op(ForwardOpType::Log, input)
337    }
338
339    /// Sine function
340    pub fn sin(&mut self, input: usize) -> Result<usize> {
341        self.unary_op(ForwardOpType::Sin, input)
342    }
343
344    /// Cosine function
345    pub fn cos(&mut self, input: usize) -> Result<usize> {
346        self.unary_op(ForwardOpType::Cos, input)
347    }
348
349    /// Hyperbolic tangent
350    pub fn tanh(&mut self, input: usize) -> Result<usize> {
351        self.unary_op(ForwardOpType::Tanh, input)
352    }
353
354    /// Sigmoid function
355    pub fn sigmoid(&mut self, input: usize) -> Result<usize> {
356        self.unary_op(ForwardOpType::Sigmoid, input)
357    }
358
359    /// ReLU function
360    pub fn relu(&mut self, input: usize) -> Result<usize> {
361        self.unary_op(ForwardOpType::ReLU, input)
362    }
363
364    /// Matrix multiplication.
365    ///
366    /// `dims` is `(m, k, n)`: the left operand is the row-major flattening of
367    /// an `m x k` matrix, the right operand of a `k x n` matrix, and the
368    /// output is `m x n`.
369    pub fn matmul(&mut self, lhs: usize, rhs: usize, dims: (usize, usize, usize)) -> Result<usize> {
370        let (m, k, n) = dims;
371        if m == 0 || k == 0 || n == 0 {
372            return Err(OptimError::InvalidConfig(format!(
373                "matmul dimensions must be non-zero, got {dims:?}"
374            )));
375        }
376        let output_id = self.tape.len();
377
378        let op = ForwardOperation {
379            optype: ForwardOpType::MatMul,
380            inputs: vec![lhs, rhs],
381            metadata: ForwardOpMetadata {
382                shape: vec![m, n],
383                data: ForwardOpData::MatMulDims { m, k, n },
384            },
385        };
386
387        self.tape.push(op);
388        Ok(output_id)
389    }
390
391    /// Dot product
392    pub fn dot(&mut self, lhs: usize, rhs: usize) -> Result<usize> {
393        self.binary_op(ForwardOpType::Dot, lhs, rhs)
394    }
395
396    /// Sum reduction over all elements
397    pub fn sum(&mut self, input: usize, _axis: Option<usize>) -> Result<usize> {
398        self.unary_op(ForwardOpType::Sum, input)
399    }
400
401    /// Mean reduction over all elements
402    pub fn mean(&mut self, input: usize, _axis: Option<usize>) -> Result<usize> {
403        self.unary_op(ForwardOpType::Mean, input)
404    }
405
406    /// L2 norm
407    pub fn norm(&mut self, input: usize) -> Result<usize> {
408        self.unary_op(ForwardOpType::Norm, input)
409    }
410
411    /// Compute the forward pass with dual numbers.
412    ///
413    /// `seed_direction` is a vector in the concatenated input space: its length
414    /// must equal [`Self::input_dimension`], and the slice belonging to a
415    /// variable seeds that variable's tangent component-wise.
416    pub fn forward_pass(
417        &self,
418        inputs: &HashMap<String, Array1<T>>,
419        seed_direction: &Array1<T>,
420    ) -> Result<Vec<VectorDual<T>>> {
421        let input_dim = self.input_dimension(inputs)?;
422        if seed_direction.len() != input_dim {
423            return Err(OptimError::InvalidConfig(format!(
424                "seed direction has {} elements but the input space has {input_dim}",
425                seed_direction.len()
426            )));
427        }
428        let offsets = self.seed_offsets(inputs)?;
429        let mut values: Vec<VectorDual<T>> = Vec::with_capacity(self.tape.len());
430
431        for (idx, op) in self.tape.iter().enumerate() {
432            match op.optype {
433                ForwardOpType::Variable => {
434                    let var_name = self
435                        .variables
436                        .iter()
437                        .find(|(_, &id)| id == idx)
438                        .map(|(name_, _)| name_.clone())
439                        .ok_or_else(|| {
440                            OptimError::InvalidConfig(format!(
441                                "tape node {idx} is a variable with no registered name"
442                            ))
443                        })?;
444
445                    let value = inputs
446                        .get(&var_name)
447                        .ok_or_else(|| {
448                            OptimError::InvalidConfig(format!(
449                                "input value for '{var_name}' not provided"
450                            ))
451                        })?
452                        .clone();
453
454                    let offset = *offsets.get(&var_name).ok_or_else(|| {
455                        OptimError::ComputationError(format!(
456                            "no seed offset registered for '{var_name}'"
457                        ))
458                    })?;
459                    let tangent =
460                        Array1::from_shape_fn(value.len(), |i| seed_direction[offset + i]);
461
462                    values.push(VectorDual::new(value, tangent));
463                }
464                ForwardOpType::Constant => {
465                    if let ForwardOpData::ConstantVector(ref val) = op.metadata.data {
466                        let tangent = Array1::zeros(val.len());
467                        values.push(VectorDual::new(val.clone(), tangent));
468                    } else {
469                        return Err(OptimError::InvalidConfig(
470                            "constant node is missing its value".to_string(),
471                        ));
472                    }
473                }
474                _ => {
475                    let result = self.compute_forward_operation(op, &values)?;
476                    values.push(result);
477                }
478            }
479        }
480
481        Ok(values)
482    }
483
484    /// Compute Jacobian-vector product
485    pub fn jacobian_vector_product(
486        &self,
487        inputs: &HashMap<String, Array1<T>>,
488        output_id: usize,
489        direction: &Array1<T>,
490    ) -> Result<Array1<T>> {
491        let results = self.forward_pass(inputs, direction)?;
492
493        if output_id >= results.len() {
494            return Err(OptimError::InvalidConfig("Invalid output ID".to_string()));
495        }
496
497        Ok(results[output_id].tangent.clone())
498    }
499
500    /// Compute the full Jacobian of `output_id` with respect to the inputs.
501    ///
502    /// The result has shape `(output_dim, input_dim)` where `output_dim` is the
503    /// number of elements of the selected output node and `input_dim` is
504    /// [`Self::input_dimension`]. `input_size` must agree with `input_dim`.
505    pub fn jacobian_matrix(
506        &self,
507        inputs: &HashMap<String, Array1<T>>,
508        output_id: usize,
509        input_size: usize,
510    ) -> Result<Array2<T>> {
511        let input_dim = self.input_dimension(inputs)?;
512        if input_size != input_dim {
513            return Err(OptimError::InvalidConfig(format!(
514                "requested input size {input_size} does not match the input space dimension {input_dim}"
515            )));
516        }
517        if input_dim == 0 {
518            return Err(OptimError::InvalidConfig(
519                "cannot build a Jacobian for an empty input space".to_string(),
520            ));
521        }
522
523        // One JVP per input coordinate; the first also fixes the output size.
524        let mut columns: Vec<Array1<T>> = Vec::with_capacity(input_dim);
525        for i in 0..input_dim {
526            let mut direction = Array1::zeros(input_dim);
527            direction[i] = T::one();
528            columns.push(self.jacobian_vector_product(inputs, output_id, &direction)?);
529        }
530
531        let output_dim = columns
532            .first()
533            .map(|c| c.len())
534            .ok_or_else(|| OptimError::ComputationError("no Jacobian columns".to_string()))?;
535        let mut jacobian = Array2::zeros((output_dim, input_dim));
536        for (i, column) in columns.iter().enumerate() {
537            if column.len() != output_dim {
538                return Err(OptimError::ComputationError(
539                    "Jacobian columns have inconsistent lengths".to_string(),
540                ));
541            }
542            for (j, &val) in column.iter().enumerate() {
543                jacobian[[j, i]] = val;
544            }
545        }
546
547        Ok(jacobian)
548    }
549
550    fn binary_op(&mut self, optype: ForwardOpType, lhs: usize, rhs: usize) -> Result<usize> {
551        let output_id = self.tape.len();
552
553        let op = ForwardOperation {
554            optype,
555            inputs: vec![lhs, rhs],
556            metadata: ForwardOpMetadata {
557                shape: Vec::new(), // Inferred from the operands during the forward pass
558                data: ForwardOpData::None,
559            },
560        };
561
562        self.tape.push(op);
563        Ok(output_id)
564    }
565
566    fn unary_op(&mut self, optype: ForwardOpType, input: usize) -> Result<usize> {
567        let output_id = self.tape.len();
568
569        let op = ForwardOperation {
570            optype,
571            inputs: vec![input],
572            metadata: ForwardOpMetadata {
573                shape: Vec::new(), // Inferred from the operand during the forward pass
574                data: ForwardOpData::None,
575            },
576        };
577
578        self.tape.push(op);
579        Ok(output_id)
580    }
581
582    fn compute_forward_operation(
583        &self,
584        op: &ForwardOperation<T>,
585        values: &[VectorDual<T>],
586    ) -> Result<VectorDual<T>> {
587        // Checked operand access: an operand index must exist on the tape and
588        // must already have been evaluated (guaranteed by tape ordering).
589        let fetch = |slot: usize| -> Result<&VectorDual<T>> {
590            let idx = *op.inputs.get(slot).ok_or_else(|| {
591                OptimError::ComputationError(format!("{:?} is missing operand {slot}", op.optype))
592            })?;
593            values.get(idx).ok_or_else(|| {
594                OptimError::ComputationError(format!("operand {idx} has not been evaluated"))
595            })
596        };
597        let require_same_len = |lhs: &VectorDual<T>, rhs: &VectorDual<T>| -> Result<()> {
598            if lhs.value.len() != rhs.value.len() {
599                return Err(OptimError::InvalidConfig(format!(
600                    "{:?} requires equal operand lengths, got {} and {}",
601                    op.optype,
602                    lhs.value.len(),
603                    rhs.value.len()
604                )));
605            }
606            Ok(())
607        };
608
609        match op.optype {
610            ForwardOpType::Add => {
611                let lhs = fetch(0)?;
612                let rhs = fetch(1)?;
613                require_same_len(lhs, rhs)?;
614                let value = &lhs.value + &rhs.value;
615                let tangent = &lhs.tangent + &rhs.tangent;
616                Ok(VectorDual::new(value, tangent))
617            }
618            ForwardOpType::Subtract => {
619                let lhs = fetch(0)?;
620                let rhs = fetch(1)?;
621                require_same_len(lhs, rhs)?;
622                let value = &lhs.value - &rhs.value;
623                let tangent = &lhs.tangent - &rhs.tangent;
624                Ok(VectorDual::new(value, tangent))
625            }
626            ForwardOpType::Multiply => {
627                let lhs = fetch(0)?;
628                let rhs = fetch(1)?;
629                require_same_len(lhs, rhs)?;
630
631                // Element-wise multiplication: (u*v)' = u'*v + u*v'
632                let value = &lhs.value * &rhs.value;
633                let tangent = &lhs.tangent * &rhs.value + &lhs.value * &rhs.tangent;
634                Ok(VectorDual::new(value, tangent))
635            }
636            ForwardOpType::Divide => {
637                let lhs = fetch(0)?;
638                let rhs = fetch(1)?;
639                require_same_len(lhs, rhs)?;
640
641                // Division rule: (u/v)' = (u'*v - u*v') / v^2
642                let value = &lhs.value / &rhs.value;
643                let numerator = &lhs.tangent * &rhs.value - &lhs.value * &rhs.tangent;
644                let denominator = &rhs.value * &rhs.value;
645                let tangent = numerator / denominator;
646                Ok(VectorDual::new(value, tangent))
647            }
648            ForwardOpType::Power => {
649                let base = fetch(0)?;
650                if let ForwardOpData::PowerExponent(exp) = op.metadata.data {
651                    // Power rule: (u^n)' = n * u^(n-1) * u'
652                    let value = base.value.mapv(|x| x.powf(exp));
653                    let derivative = base.value.mapv(|x| exp * x.powf(exp - T::one()));
654                    let tangent = derivative * &base.tangent;
655                    Ok(VectorDual::new(value, tangent))
656                } else {
657                    Err(OptimError::InvalidConfig(
658                        "Invalid power operation".to_string(),
659                    ))
660                }
661            }
662            ForwardOpType::Exp => {
663                let input = fetch(0)?;
664                // (e^u)' = e^u * u'
665                let value = input.value.mapv(|x| x.exp());
666                let tangent = &value * &input.tangent;
667                Ok(VectorDual::new(value, tangent))
668            }
669            ForwardOpType::Log => {
670                let input = fetch(0)?;
671                // (ln(u))' = u' / u
672                let value = input.value.mapv(|x| x.ln());
673                let tangent = &input.tangent / &input.value;
674                Ok(VectorDual::new(value, tangent))
675            }
676            ForwardOpType::Sin => {
677                let input = fetch(0)?;
678                // (sin(u))' = cos(u) * u'
679                let value = input.value.mapv(|x| x.sin());
680                let derivative = input.value.mapv(|x| x.cos());
681                let tangent = derivative * &input.tangent;
682                Ok(VectorDual::new(value, tangent))
683            }
684            ForwardOpType::Cos => {
685                let input = fetch(0)?;
686                // (cos(u))' = -sin(u) * u'
687                let value = input.value.mapv(|x| x.cos());
688                let derivative = input.value.mapv(|x| -x.sin());
689                let tangent = derivative * &input.tangent;
690                Ok(VectorDual::new(value, tangent))
691            }
692            ForwardOpType::Tanh => {
693                let input = fetch(0)?;
694                // (tanh(u))' = sech^2(u) * u' = (1 - tanh^2(u)) * u'
695                let value = input.value.mapv(|x| x.tanh());
696                let derivative = value.mapv(|y| T::one() - y * y);
697                let tangent = derivative * &input.tangent;
698                Ok(VectorDual::new(value, tangent))
699            }
700            ForwardOpType::Sigmoid => {
701                let input = fetch(0)?;
702                // (sigmoid(u))' = sigmoid(u) * (1 - sigmoid(u)) * u'
703                let value = input.value.mapv(|x| T::one() / (T::one() + (-x).exp()));
704                let derivative = value.mapv(|y| y * (T::one() - y));
705                let tangent = derivative * &input.tangent;
706                Ok(VectorDual::new(value, tangent))
707            }
708            ForwardOpType::ReLU => {
709                let input = fetch(0)?;
710                // (ReLU(u))' = u' if u > 0, else 0
711                let value = input
712                    .value
713                    .mapv(|x| if x > T::zero() { x } else { T::zero() });
714                let derivative = input
715                    .value
716                    .mapv(|x| if x > T::zero() { T::one() } else { T::zero() });
717                let tangent = derivative * &input.tangent;
718                Ok(VectorDual::new(value, tangent))
719            }
720            ForwardOpType::Dot => {
721                let lhs = fetch(0)?;
722                let rhs = fetch(1)?;
723                require_same_len(lhs, rhs)?;
724                // (u·v)' = u'·v + u·v'
725                let value = Array1::from_elem(1, lhs.value.dot(&rhs.value));
726                let tangent =
727                    Array1::from_elem(1, lhs.tangent.dot(&rhs.value) + lhs.value.dot(&rhs.tangent));
728                Ok(VectorDual::new(value, tangent))
729            }
730            ForwardOpType::Sum => {
731                let input = fetch(0)?;
732                // Sum derivative is sum of input derivatives
733                let value = Array1::from_elem(1, input.value.sum());
734                let tangent = Array1::from_elem(1, input.tangent.sum());
735                Ok(VectorDual::new(value, tangent))
736            }
737            ForwardOpType::Mean => {
738                let input = fetch(0)?;
739                // Mean derivative is mean of input derivatives
740                let n =
741                    scirs2_core::numeric::NumCast::from(input.value.len()).unwrap_or_else(T::one);
742                let value = Array1::from_elem(1, input.value.sum() / n);
743                let tangent = Array1::from_elem(1, input.tangent.sum() / n);
744                Ok(VectorDual::new(value, tangent))
745            }
746            ForwardOpType::Norm => {
747                let input = fetch(0)?;
748                // ||u||' = (u·u')/ ||u||
749                let norm = input.value.iter().map(|&x| x * x).sum::<T>().sqrt();
750                let value = Array1::from_elem(1, norm);
751                let tangent = if norm > T::zero() {
752                    let dot_product = input.value.dot(&input.tangent);
753                    Array1::from_elem(1, dot_product / norm)
754                } else {
755                    Array1::zeros(1)
756                };
757                Ok(VectorDual::new(value, tangent))
758            }
759            ForwardOpType::MatMul => {
760                let lhs = fetch(0)?;
761                let rhs = fetch(1)?;
762                let (m, k, n) = match op.metadata.data {
763                    ForwardOpData::MatMulDims { m, k, n } => (m, k, n),
764                    _ => {
765                        return Err(OptimError::InvalidConfig(
766                            "matmul node is missing its dimensions".to_string(),
767                        ))
768                    }
769                };
770                if lhs.value.len() != m * k {
771                    return Err(OptimError::InvalidConfig(format!(
772                        "matmul left operand has {} elements, expected {}",
773                        lhs.value.len(),
774                        m * k
775                    )));
776                }
777                if rhs.value.len() != k * n {
778                    return Err(OptimError::InvalidConfig(format!(
779                        "matmul right operand has {} elements, expected {}",
780                        rhs.value.len(),
781                        k * n
782                    )));
783                }
784                // (A · B)' = A' · B + A · B'
785                let value = matmul_flat(&lhs.value, m, k, &rhs.value, n);
786                let left_term = matmul_flat(&lhs.tangent, m, k, &rhs.value, n);
787                let right_term = matmul_flat(&lhs.value, m, k, &rhs.tangent, n);
788                let tangent = left_term + right_term;
789                Ok(VectorDual::new(value, tangent))
790            }
791            ForwardOpType::Variable | ForwardOpType::Constant => Err(OptimError::InvalidConfig(
792                "leaf nodes are seeded directly and must not be recomputed".to_string(),
793            )),
794        }
795    }
796
797    /// Get computation graph statistics
798    pub fn get_graph_stats(&self) -> ForwardModeStats {
799        ForwardModeStats {
800            num_operations: self.tape.len(),
801            num_variables: self.variables.len(),
802            memory_usage_estimate: self.estimate_memory_usage(),
803            max_depth: self.compute_max_depth(),
804        }
805    }
806
807    fn estimate_memory_usage(&self) -> usize {
808        self.tape.len() * std::mem::size_of::<ForwardOperation<T>>()
809    }
810
811    fn compute_max_depth(&self) -> usize {
812        // Simplified depth computation
813        self.tape.len()
814    }
815}
816
817/// Row-major matrix product of a flat `m x k` buffer with a flat `k x n` buffer.
818fn matmul_flat<T: Float + Clone>(
819    lhs: &Array1<T>,
820    m: usize,
821    k: usize,
822    rhs: &Array1<T>,
823    n: usize,
824) -> Array1<T> {
825    let mut out = Array1::zeros(m * n);
826    for i in 0..m {
827        for j in 0..n {
828            let mut acc = T::zero();
829            for p in 0..k {
830                acc = acc + lhs[i * k + p] * rhs[p * n + j];
831            }
832            out[i * n + j] = acc;
833        }
834    }
835    out
836}
837
838/// Forward-mode AD statistics
839#[derive(Debug, Clone)]
840pub struct ForwardModeStats {
841    pub num_operations: usize,
842    pub num_variables: usize,
843    pub memory_usage_estimate: usize,
844    pub max_depth: usize,
845}
846
847// Implement arithmetic operations for dual numbers
848impl<T: Float + Debug + Default + Clone + Send + Sync + 'static> std::ops::Add for DualNumber<T> {
849    type Output = Self;
850
851    fn add(self, rhs: Self) -> Self {
852        Self {
853            value: self.value + rhs.value,
854            tangent: self.tangent + rhs.tangent,
855        }
856    }
857}
858
859impl<T: Float + Debug + Default + Clone + Send + Sync + 'static> std::ops::Sub for DualNumber<T> {
860    type Output = Self;
861
862    fn sub(self, rhs: Self) -> Self {
863        Self {
864            value: self.value - rhs.value,
865            tangent: self.tangent - rhs.tangent,
866        }
867    }
868}
869
870impl<T: Float + Debug + Default + Clone + Send + Sync + 'static> std::ops::Mul for DualNumber<T> {
871    type Output = Self;
872
873    fn mul(self, rhs: Self) -> Self {
874        Self {
875            value: self.value * rhs.value,
876            tangent: self.tangent * rhs.value + self.value * rhs.tangent,
877        }
878    }
879}
880
881impl<T: Float + Debug + Default + Clone + Send + Sync + 'static> std::ops::Div for DualNumber<T> {
882    type Output = Self;
883
884    fn div(self, rhs: Self) -> Self {
885        Self {
886            value: self.value / rhs.value,
887            tangent: (self.tangent * rhs.value - self.value * rhs.tangent)
888                / (rhs.value * rhs.value),
889        }
890    }
891}
892
893#[cfg(test)]
894mod tests {
895    use super::*;
896
897    #[test]
898    fn test_dual_number_creation() {
899        let dual = DualNumber::new(2.0, 1.0);
900        assert_eq!(dual.value, 2.0);
901        assert_eq!(dual.tangent, 1.0);
902    }
903
904    #[test]
905    fn test_dual_number_arithmetic() {
906        let x = DualNumber::new(3.0, 1.0);
907        let y = DualNumber::new(2.0, 0.0);
908
909        let sum = x.clone() + y.clone();
910        assert_eq!(sum.value, 5.0);
911        assert_eq!(sum.tangent, 1.0);
912
913        let product = x * y;
914        assert_eq!(product.value, 6.0);
915        assert_eq!(product.tangent, 2.0);
916    }
917
918    #[test]
919    fn test_forward_mode_engine() {
920        let mut engine = ForwardModeEngine::<f64>::new();
921
922        let x_val = Array1::from_vec(vec![2.0]);
923        let x_id = engine.create_variable("x", x_val.clone());
924
925        let y_val = Array1::from_vec(vec![3.0]);
926        let y_id = engine.create_variable("y", y_val.clone());
927
928        let sum_id = engine.add(x_id, y_id).expect("add should succeed");
929
930        let mut inputs = HashMap::new();
931        inputs.insert("x".to_string(), x_val);
932        inputs.insert("y".to_string(), y_val);
933
934        let direction = Array1::from_vec(vec![1.0, 0.0]);
935        let results = engine
936            .forward_pass(&inputs, &direction)
937            .expect("forward_pass should succeed");
938
939        assert!(results.len() > sum_id);
940        assert_eq!(results[sum_id].value[0], 5.0);
941    }
942
943    #[test]
944    fn test_vector_dual_operations() {
945        let value1 = Array1::from_vec(vec![1.0, 2.0, 3.0]);
946        let tangent1 = Array1::from_vec(vec![1.0, 0.0, 0.0]);
947        let dual1 = VectorDual::new(value1, tangent1);
948
949        assert_eq!(dual1.value.len(), 3);
950        assert_eq!(dual1.tangent.len(), 3);
951        assert_eq!(dual1.tangent[0], 1.0);
952    }
953
954    /// Smoke test: forward-mode AD computes the correct derivative.
955    ///
956    /// For f(x) = x^2, f'(x) = 2x. Propagating a unit tangent through the
957    /// `power` operation at x = 3 must yield the tangent 2 * 3 = 6.
958    #[test]
959    fn test_forward_mode_derivative_of_square() {
960        // Direct DualNumber check: f(x) = x * x at x = 3 -> tangent 6.
961        let x = DualNumber::variable(3.0_f64);
962        let y = x.clone() * x;
963        approx::assert_abs_diff_eq!(y.value, 9.0, epsilon = 1e-10);
964        approx::assert_abs_diff_eq!(y.tangent, 6.0, epsilon = 1e-10);
965
966        // Engine-level JVP check via the `power` op builder.
967        let mut engine = ForwardModeEngine::<f64>::new();
968        let x_val = Array1::from_vec(vec![3.0]);
969        let x_id = engine.create_variable("x", x_val.clone());
970        let sq_id = engine.power(x_id, 2.0).expect("power op");
971
972        let mut inputs = HashMap::new();
973        inputs.insert("x".to_string(), x_val);
974
975        // Seed direction selects variable 0 (x) with unit tangent.
976        let direction = Array1::from_vec(vec![1.0]);
977        let jvp = engine
978            .jacobian_vector_product(&inputs, sq_id, &direction)
979            .expect("jvp");
980        approx::assert_abs_diff_eq!(jvp[0], 6.0, epsilon = 1e-10);
981    }
982
983    /// F41: matrix multiplication used to fall through to "unsupported
984    /// operation". The JVP obeys the product rule (A·B)' = A'·B + A·B'.
985    #[test]
986    fn test_forward_mode_matmul_jvp() {
987        let mut engine = ForwardModeEngine::<f64>::new();
988        // A is 2x3, B is 3x2 -> C is 2x2.
989        let a = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
990        let b = Array1::from_vec(vec![7.0, 8.0, 9.0, 10.0, 11.0, 12.0]);
991        let a_id = engine.create_variable("a", a.clone());
992        let b_id = engine.create_variable("b", b.clone());
993        let c_id = engine.matmul(a_id, b_id, (2, 3, 2)).expect("matmul");
994
995        let mut inputs = HashMap::new();
996        inputs.insert("a".to_string(), a.clone());
997        inputs.insert("b".to_string(), b.clone());
998
999        assert_eq!(engine.input_dimension(&inputs).expect("dim"), 12);
1000        assert_eq!(
1001            engine.declared_shape(c_id).expect("shape"),
1002            Some(&[2, 2][..])
1003        );
1004
1005        // Seed only a[0] (the first coordinate of the concatenated space).
1006        let mut direction = Array1::zeros(12);
1007        direction[0] = 1.0;
1008        let results = engine.forward_pass(&inputs, &direction).expect("forward");
1009        let c = &results[c_id];
1010
1011        // C = A·B computed by hand.
1012        approx::assert_abs_diff_eq!(
1013            c.value[0],
1014            1.0 * 7.0 + 2.0 * 9.0 + 3.0 * 11.0,
1015            epsilon = 1e-10
1016        );
1017        approx::assert_abs_diff_eq!(
1018            c.value[3],
1019            4.0 * 8.0 + 5.0 * 10.0 + 6.0 * 12.0,
1020            epsilon = 1e-10
1021        );
1022        // dC/da[0,0] = row 0 of B: [7, 8] into C[0,0], C[0,1]; zero elsewhere.
1023        approx::assert_abs_diff_eq!(c.tangent[0], 7.0, epsilon = 1e-10);
1024        approx::assert_abs_diff_eq!(c.tangent[1], 8.0, epsilon = 1e-10);
1025        approx::assert_abs_diff_eq!(c.tangent[2], 0.0, epsilon = 1e-10);
1026        approx::assert_abs_diff_eq!(c.tangent[3], 0.0, epsilon = 1e-10);
1027    }
1028
1029    /// F41: the Jacobian must be `(output_dim, input_dim)`.
1030    #[test]
1031    fn test_jacobian_matrix_dimensions_and_values() {
1032        let mut engine = ForwardModeEngine::<f64>::new();
1033        let x = Array1::from_vec(vec![2.0, 3.0, 4.0]);
1034        let x_id = engine.create_variable("x", x.clone());
1035        let y_id = engine.exp(x_id).expect("exp");
1036
1037        let mut inputs = HashMap::new();
1038        inputs.insert("x".to_string(), x.clone());
1039
1040        let jac = engine.jacobian_matrix(&inputs, y_id, 3).expect("jacobian");
1041        assert_eq!(jac.shape(), &[3, 3]);
1042        for i in 0..3 {
1043            for j in 0..3 {
1044                let expected = if i == j { x[i].exp() } else { 0.0 };
1045                approx::assert_abs_diff_eq!(jac[[i, j]], expected, epsilon = 1e-9);
1046            }
1047        }
1048
1049        // A mismatched requested size is an error, not a silently wrong matrix.
1050        assert!(engine.jacobian_matrix(&inputs, y_id, 5).is_err());
1051    }
1052
1053    /// F41: seed ordinals follow variable-creation order, not tape indices, so
1054    /// intermediate operations between two variables do not shift the seeds.
1055    #[test]
1056    fn test_seed_ordinals_are_per_variable() {
1057        let mut engine = ForwardModeEngine::<f64>::new();
1058        let x = Array1::from_vec(vec![1.0]);
1059        let x_id = engine.create_variable("x", x.clone());
1060        // Several intermediate nodes push the tape index of `y` far past 1.
1061        let t1 = engine.exp(x_id).expect("exp");
1062        let _t2 = engine.sin(t1).expect("sin");
1063        let y = Array1::from_vec(vec![5.0]);
1064        let y_id = engine.create_variable("y", y.clone());
1065        let prod = engine.multiply(x_id, y_id).expect("multiply");
1066
1067        let mut inputs = HashMap::new();
1068        inputs.insert("x".to_string(), x);
1069        inputs.insert("y".to_string(), y);
1070
1071        assert_eq!(engine.variable_order(), &["x".to_string(), "y".to_string()]);
1072
1073        // Seed the second variable (ordinal 1): d(xy)/dy = x = 1.
1074        let direction = Array1::from_vec(vec![0.0, 1.0]);
1075        let jvp = engine
1076            .jacobian_vector_product(&inputs, prod, &direction)
1077            .expect("jvp");
1078        approx::assert_abs_diff_eq!(jvp[0], 1.0, epsilon = 1e-12);
1079
1080        // Seed the first variable (ordinal 0): d(xy)/dx = y = 5.
1081        let direction = Array1::from_vec(vec![1.0, 0.0]);
1082        let jvp = engine
1083            .jacobian_vector_product(&inputs, prod, &direction)
1084            .expect("jvp");
1085        approx::assert_abs_diff_eq!(jvp[0], 5.0, epsilon = 1e-12);
1086    }
1087
1088    /// A seed direction whose length disagrees with the input space is an error.
1089    #[test]
1090    fn test_seed_length_validation() {
1091        let mut engine = ForwardModeEngine::<f64>::new();
1092        let x = Array1::from_vec(vec![1.0, 2.0]);
1093        let x_id = engine.create_variable("x", x.clone());
1094        let y_id = engine.tanh(x_id).expect("tanh");
1095
1096        let mut inputs = HashMap::new();
1097        inputs.insert("x".to_string(), x);
1098
1099        let bad = Array1::from_vec(vec![1.0]);
1100        assert!(engine.jacobian_vector_product(&inputs, y_id, &bad).is_err());
1101    }
1102}