1use std::fmt::Debug;
2use scirs2_core::ndarray::{Array1, Array2};
8use scirs2_core::numeric::Float;
9use std::collections::HashMap;
10
11use crate::error::{OptimError, Result};
12
13#[derive(Debug, Clone)]
15pub struct DualNumber<T: Float + Debug + Send + Sync + 'static> {
16 pub value: T,
18
19 pub tangent: T,
21}
22
23#[derive(Debug, Clone)]
25pub struct VectorDual<T: Float + Debug + Send + Sync + 'static> {
26 pub value: Array1<T>,
28
29 pub tangent: Array1<T>,
31}
32
33pub struct ForwardModeEngine<
35 T: Float + Debug + Default + Clone + Send + Sync + std::iter::Sum + 'static,
36> {
37 tape: Vec<ForwardOperation<T>>,
39
40 variables: HashMap<String, usize>,
42
43 variable_order: Vec<String>,
51
52 seed_vectors: Vec<Array1<T>>,
54
55 higher_order: bool,
57}
58
59#[derive(Debug, Clone)]
61struct ForwardOperation<T: Float + Debug + Send + Sync + 'static> {
62 optype: ForwardOpType,
64
65 inputs: Vec<usize>,
67
68 metadata: ForwardOpMetadata<T>,
70}
71
72#[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#[derive(Debug, Clone)]
98struct ForwardOpMetadata<T: Float + Debug + Send + Sync + 'static> {
99 shape: Vec<usize>,
101
102 data: ForwardOpData<T>,
104}
105
106#[derive(Debug, Clone)]
108enum ForwardOpData<T: Float + Debug + Send + Sync + 'static> {
109 None,
110 ConstantVector(Array1<T>),
112 PowerExponent(T),
113 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 pub fn new(value: T, tangent: T) -> Self {
124 Self { value, tangent }
125 }
126
127 pub fn constant(value: T) -> Self {
129 Self::new(value, T::zero())
130 }
131
132 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 pub fn new(value: Array1<T>, tangent: Array1<T>) -> Self {
141 Self { value, tangent }
142 }
143
144 pub fn constant(value: Array1<T>) -> Self {
146 let tangent = Array1::zeros(value.len());
147 Self::new(value, tangent)
148 }
149
150 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 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 pub fn enable_higher_order(&mut self, enabled: bool) {
184 self.higher_order = enabled;
185 }
186
187 pub fn higher_order_enabled(&self) -> bool {
189 self.higher_order
190 }
191
192 pub fn set_seed_vectors(&mut self, seeds: Vec<Array1<T>>) {
194 self.seed_vectors = seeds;
195 }
196
197 pub fn seed_vectors(&self) -> &[Array1<T>] {
199 &self.seed_vectors
200 }
201
202 pub fn variable_order(&self) -> &[String] {
204 &self.variable_order
205 }
206
207 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 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 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 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 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 pub fn add(&mut self, lhs: usize, rhs: usize) -> Result<usize> {
294 self.binary_op(ForwardOpType::Add, lhs, rhs)
295 }
296
297 pub fn subtract(&mut self, lhs: usize, rhs: usize) -> Result<usize> {
299 self.binary_op(ForwardOpType::Subtract, lhs, rhs)
300 }
301
302 pub fn multiply(&mut self, lhs: usize, rhs: usize) -> Result<usize> {
304 self.binary_op(ForwardOpType::Multiply, lhs, rhs)
305 }
306
307 pub fn divide(&mut self, lhs: usize, rhs: usize) -> Result<usize> {
309 self.binary_op(ForwardOpType::Divide, lhs, rhs)
310 }
311
312 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(), data: ForwardOpData::PowerExponent(exponent),
322 },
323 };
324
325 self.tape.push(op);
326 Ok(output_id)
327 }
328
329 pub fn exp(&mut self, input: usize) -> Result<usize> {
331 self.unary_op(ForwardOpType::Exp, input)
332 }
333
334 pub fn log(&mut self, input: usize) -> Result<usize> {
336 self.unary_op(ForwardOpType::Log, input)
337 }
338
339 pub fn sin(&mut self, input: usize) -> Result<usize> {
341 self.unary_op(ForwardOpType::Sin, input)
342 }
343
344 pub fn cos(&mut self, input: usize) -> Result<usize> {
346 self.unary_op(ForwardOpType::Cos, input)
347 }
348
349 pub fn tanh(&mut self, input: usize) -> Result<usize> {
351 self.unary_op(ForwardOpType::Tanh, input)
352 }
353
354 pub fn sigmoid(&mut self, input: usize) -> Result<usize> {
356 self.unary_op(ForwardOpType::Sigmoid, input)
357 }
358
359 pub fn relu(&mut self, input: usize) -> Result<usize> {
361 self.unary_op(ForwardOpType::ReLU, input)
362 }
363
364 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 pub fn dot(&mut self, lhs: usize, rhs: usize) -> Result<usize> {
393 self.binary_op(ForwardOpType::Dot, lhs, rhs)
394 }
395
396 pub fn sum(&mut self, input: usize, _axis: Option<usize>) -> Result<usize> {
398 self.unary_op(ForwardOpType::Sum, input)
399 }
400
401 pub fn mean(&mut self, input: usize, _axis: Option<usize>) -> Result<usize> {
403 self.unary_op(ForwardOpType::Mean, input)
404 }
405
406 pub fn norm(&mut self, input: usize) -> Result<usize> {
408 self.unary_op(ForwardOpType::Norm, input)
409 }
410
411 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 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 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 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(), 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(), 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 self.tape.len()
814 }
815}
816
817fn 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#[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
847impl<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 #[test]
959 fn test_forward_mode_derivative_of_square() {
960 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 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 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 #[test]
986 fn test_forward_mode_matmul_jvp() {
987 let mut engine = ForwardModeEngine::<f64>::new();
988 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 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 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 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 #[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 assert!(engine.jacobian_matrix(&inputs, y_id, 5).is_err());
1051 }
1052
1053 #[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 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 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 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 #[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}