Skip to main content

ipfrs_tensorlogic/
autograd.rs

1//! Reverse-mode automatic differentiation (autograd) for scalar-output
2//! functions over f64 tensor operations.
3//!
4//! This module tracks computational graphs and computes gradients via
5//! backpropagation.
6
7use std::collections::HashMap;
8
9/// Unique identifier for a node in the autograd graph.
10pub type NodeId = u64;
11
12/// Describes the operation that produced an [`AutogradNode`].
13#[derive(Clone, Debug, PartialEq)]
14pub enum AutogradOp {
15    /// Leaf node — no parents (input or constant).
16    Input,
17    /// Element-wise addition: `z = x + y`.
18    Add { lhs: NodeId, rhs: NodeId },
19    /// Element-wise multiplication: `z = x * y`.
20    Mul { lhs: NodeId, rhs: NodeId },
21    /// Negation: `z = -x`.
22    Neg { input: NodeId },
23    /// Natural exponential: `z = e^x`.
24    Exp { input: NodeId },
25    /// Natural logarithm: `z = ln(x)`.
26    Ln { input: NodeId },
27    /// Power with constant exponent: `z = x^e`.
28    Pow { base: NodeId, exponent: f64 },
29}
30
31/// A single node in the autograd computational graph.
32#[derive(Clone, Debug)]
33pub struct AutogradNode {
34    /// Unique identifier.
35    pub id: NodeId,
36    /// Operation that produced this node.
37    pub op: AutogradOp,
38    /// Forward-pass value.
39    pub value: f64,
40    /// Accumulated gradient (populated after `backward`).
41    pub grad: f64,
42    /// Whether gradients flow through this node.
43    pub requires_grad: bool,
44}
45
46/// Reverse-mode automatic differentiation graph.
47///
48/// Build a computational graph by calling [`AutogradGraph::input`],
49/// [`AutogradGraph::add`], [`AutogradGraph::mul`], etc., then call
50/// [`AutogradGraph::backward`] on the scalar output node to populate
51/// `.grad` on every node that has `requires_grad = true`.
52pub struct AutogradGraph {
53    /// All nodes keyed by their [`NodeId`].
54    pub nodes: HashMap<NodeId, AutogradNode>,
55    next_id: NodeId,
56}
57
58impl AutogradGraph {
59    /// Create an empty graph.
60    pub fn new() -> Self {
61        Self {
62            nodes: HashMap::new(),
63            next_id: 0,
64        }
65    }
66
67    // -----------------------------------------------------------------------
68    // Internal helpers
69    // -----------------------------------------------------------------------
70
71    fn alloc_id(&mut self) -> NodeId {
72        let id = self.next_id;
73        self.next_id += 1;
74        id
75    }
76
77    fn insert(&mut self, op: AutogradOp, value: f64, requires_grad: bool) -> NodeId {
78        let id = self.alloc_id();
79        self.nodes.insert(
80            id,
81            AutogradNode {
82                id,
83                op,
84                value,
85                grad: 0.0,
86                requires_grad,
87            },
88        );
89        id
90    }
91
92    // -----------------------------------------------------------------------
93    // Node constructors
94    // -----------------------------------------------------------------------
95
96    /// Create a leaf input node with `requires_grad = true`.
97    pub fn input(&mut self, value: f64) -> NodeId {
98        self.insert(AutogradOp::Input, value, true)
99    }
100
101    /// Create a constant leaf node with `requires_grad = false`.
102    /// Gradients do not flow through constants.
103    pub fn constant(&mut self, value: f64) -> NodeId {
104        self.insert(AutogradOp::Input, value, false)
105    }
106
107    /// `z = lhs + rhs`.
108    pub fn add(&mut self, lhs: NodeId, rhs: NodeId) -> NodeId {
109        let value = self.nodes[&lhs].value + self.nodes[&rhs].value;
110        self.insert(AutogradOp::Add { lhs, rhs }, value, true)
111    }
112
113    /// `z = lhs * rhs`.
114    pub fn mul(&mut self, lhs: NodeId, rhs: NodeId) -> NodeId {
115        let value = self.nodes[&lhs].value * self.nodes[&rhs].value;
116        self.insert(AutogradOp::Mul { lhs, rhs }, value, true)
117    }
118
119    /// `z = -input`.
120    pub fn neg(&mut self, input: NodeId) -> NodeId {
121        let value = -self.nodes[&input].value;
122        self.insert(AutogradOp::Neg { input }, value, true)
123    }
124
125    /// `z = e^input`.
126    pub fn exp(&mut self, input: NodeId) -> NodeId {
127        let value = self.nodes[&input].value.exp();
128        self.insert(AutogradOp::Exp { input }, value, true)
129    }
130
131    /// `z = ln(input)`.
132    pub fn ln(&mut self, input: NodeId) -> NodeId {
133        let value = self.nodes[&input].value.ln();
134        self.insert(AutogradOp::Ln { input }, value, true)
135    }
136
137    /// `z = base^exponent` where `exponent` is a scalar constant.
138    pub fn pow(&mut self, base: NodeId, exponent: f64) -> NodeId {
139        let value = self.nodes[&base].value.powf(exponent);
140        self.insert(AutogradOp::Pow { base, exponent }, value, true)
141    }
142
143    // -----------------------------------------------------------------------
144    // Backpropagation
145    // -----------------------------------------------------------------------
146
147    /// Run reverse-mode backpropagation starting from `output_id`.
148    ///
149    /// Sets `nodes[output_id].grad = 1.0`, performs a topological sort
150    /// (reverse post-order DFS), and propagates gradients backwards through
151    /// the graph.  Only nodes with `requires_grad = true` accumulate gradients.
152    pub fn backward(&mut self, output_id: NodeId) {
153        // Set seed gradient on the output.
154        if let Some(node) = self.nodes.get_mut(&output_id) {
155            node.grad = 1.0;
156        }
157
158        // Topological order via iterative post-order DFS.
159        let topo = self.topo_sort(output_id);
160
161        // Propagate in reverse topological order (from output → inputs).
162        for &nid in topo.iter().rev() {
163            // Gather what we need without holding a borrow on `self.nodes`.
164            let (op, grad_out, out_val, rg) = {
165                let node = match self.nodes.get(&nid) {
166                    Some(n) => n,
167                    None => continue,
168                };
169                (node.op.clone(), node.grad, node.value, node.requires_grad)
170            };
171
172            if !rg {
173                continue;
174            }
175
176            match op {
177                AutogradOp::Input => {
178                    // Leaf — nothing to propagate further.
179                }
180                AutogradOp::Add { lhs, rhs } => {
181                    // dz/dlhs = 1, dz/drhs = 1
182                    self.accumulate_grad(lhs, grad_out);
183                    self.accumulate_grad(rhs, grad_out);
184                }
185                AutogradOp::Mul { lhs, rhs } => {
186                    // dz/dlhs = rhs_val, dz/drhs = lhs_val
187                    let lhs_val = self.nodes[&lhs].value;
188                    let rhs_val = self.nodes[&rhs].value;
189                    self.accumulate_grad(lhs, grad_out * rhs_val);
190                    self.accumulate_grad(rhs, grad_out * lhs_val);
191                }
192                AutogradOp::Neg { input } => {
193                    // dz/dinput = -1
194                    self.accumulate_grad(input, -grad_out);
195                }
196                AutogradOp::Exp { input } => {
197                    // dz/dinput = e^input = out_val
198                    self.accumulate_grad(input, grad_out * out_val);
199                }
200                AutogradOp::Ln { input } => {
201                    // dz/dinput = 1 / input_val
202                    let input_val = self.nodes[&input].value;
203                    self.accumulate_grad(input, grad_out / input_val);
204                }
205                AutogradOp::Pow { base, exponent } => {
206                    // dz/dbase = exponent * base^(exponent - 1)
207                    let base_val = self.nodes[&base].value;
208                    let grad = grad_out * exponent * base_val.powf(exponent - 1.0);
209                    self.accumulate_grad(base, grad);
210                }
211            }
212        }
213    }
214
215    /// Accumulate `delta` into `node_id.grad` only if the node exists and
216    /// has `requires_grad = true`.
217    fn accumulate_grad(&mut self, node_id: NodeId, delta: f64) {
218        if let Some(node) = self.nodes.get_mut(&node_id) {
219            if node.requires_grad {
220                node.grad += delta;
221            }
222        }
223    }
224
225    /// Iterative post-order DFS to produce a topological ordering of nodes
226    /// reachable from `start`.
227    fn topo_sort(&self, start: NodeId) -> Vec<NodeId> {
228        let mut order: Vec<NodeId> = Vec::new();
229        let mut visited: HashMap<NodeId, bool> = HashMap::new(); // false = in-stack, true = done
230        let mut stack: Vec<(NodeId, bool)> = vec![(start, false)];
231
232        while let Some((nid, processed)) = stack.pop() {
233            if processed {
234                order.push(nid);
235                continue;
236            }
237            if visited.contains_key(&nid) {
238                continue; // already processed
239            }
240            // Mark as in-progress and push "return" frame.
241            visited.insert(nid, false);
242            stack.push((nid, true)); // post-process frame
243
244            // Push children (parents in the graph sense).
245            if let Some(node) = self.nodes.get(&nid) {
246                match &node.op {
247                    AutogradOp::Input => {}
248                    AutogradOp::Add { lhs, rhs } => {
249                        if !visited.contains_key(lhs) {
250                            stack.push((*lhs, false));
251                        }
252                        if !visited.contains_key(rhs) {
253                            stack.push((*rhs, false));
254                        }
255                    }
256                    AutogradOp::Mul { lhs, rhs } => {
257                        if !visited.contains_key(lhs) {
258                            stack.push((*lhs, false));
259                        }
260                        if !visited.contains_key(rhs) {
261                            stack.push((*rhs, false));
262                        }
263                    }
264                    AutogradOp::Neg { input }
265                    | AutogradOp::Exp { input }
266                    | AutogradOp::Ln { input } => {
267                        if !visited.contains_key(input) {
268                            stack.push((*input, false));
269                        }
270                    }
271                    AutogradOp::Pow { base, .. } => {
272                        if !visited.contains_key(base) {
273                            stack.push((*base, false));
274                        }
275                    }
276                }
277            }
278        }
279        order
280    }
281
282    // -----------------------------------------------------------------------
283    // Accessors
284    // -----------------------------------------------------------------------
285
286    /// Return the gradient of `node_id` if it exists and has
287    /// `requires_grad = true`.
288    pub fn grad(&self, node_id: NodeId) -> Option<f64> {
289        self.nodes
290            .get(&node_id)
291            .and_then(|n| if n.requires_grad { Some(n.grad) } else { None })
292    }
293
294    /// Return the forward-pass value of `node_id`, or `None` if not found.
295    pub fn value(&self, node_id: NodeId) -> Option<f64> {
296        self.nodes.get(&node_id).map(|n| n.value)
297    }
298
299    /// Reset all accumulated gradients to `0.0`.
300    pub fn zero_grad(&mut self) {
301        for node in self.nodes.values_mut() {
302            node.grad = 0.0;
303        }
304    }
305}
306
307impl Default for AutogradGraph {
308    fn default() -> Self {
309        Self::new()
310    }
311}
312
313// ---------------------------------------------------------------------------
314// Tests
315// ---------------------------------------------------------------------------
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320
321    const EPS: f64 = 1e-9;
322
323    fn approx_eq(a: f64, b: f64) -> bool {
324        (a - b).abs() < EPS
325    }
326
327    // ------------------------------------------------------------------
328    // Forward pass
329    // ------------------------------------------------------------------
330
331    #[test]
332    fn test_input_node_value() {
333        let mut g = AutogradGraph::new();
334        let x = g.input(std::f64::consts::PI);
335        assert!(approx_eq(
336            g.value(x).expect("test: should succeed"),
337            std::f64::consts::PI
338        ));
339    }
340
341    #[test]
342    fn test_add_forward() {
343        let mut g = AutogradGraph::new();
344        let x = g.input(2.0);
345        let y = g.input(3.0);
346        let z = g.add(x, y);
347        assert!(approx_eq(g.value(z).expect("test: should succeed"), 5.0));
348    }
349
350    #[test]
351    fn test_mul_forward() {
352        let mut g = AutogradGraph::new();
353        let x = g.input(4.0);
354        let y = g.input(5.0);
355        let z = g.mul(x, y);
356        assert!(approx_eq(g.value(z).expect("test: should succeed"), 20.0));
357    }
358
359    #[test]
360    fn test_neg_forward() {
361        let mut g = AutogradGraph::new();
362        let x = g.input(7.0);
363        let z = g.neg(x);
364        assert!(approx_eq(g.value(z).expect("test: should succeed"), -7.0));
365    }
366
367    #[test]
368    fn test_exp_forward() {
369        let mut g = AutogradGraph::new();
370        let x = g.input(1.0);
371        let z = g.exp(x);
372        assert!(approx_eq(
373            g.value(z).expect("test: should succeed"),
374            std::f64::consts::E
375        ));
376    }
377
378    #[test]
379    fn test_ln_forward() {
380        let mut g = AutogradGraph::new();
381        let x = g.input(std::f64::consts::E);
382        let z = g.ln(x);
383        assert!(approx_eq(g.value(z).expect("test: should succeed"), 1.0));
384    }
385
386    #[test]
387    fn test_pow_forward() {
388        let mut g = AutogradGraph::new();
389        let x = g.input(3.0);
390        let z = g.pow(x, 3.0);
391        assert!(approx_eq(g.value(z).expect("test: should succeed"), 27.0));
392    }
393
394    // ------------------------------------------------------------------
395    // Backward: individual ops
396    // ------------------------------------------------------------------
397
398    #[test]
399    fn test_backward_add_grad_splits() {
400        // z = x + y  =>  dz/dx = 1, dz/dy = 1
401        let mut g = AutogradGraph::new();
402        let x = g.input(2.0);
403        let y = g.input(3.0);
404        let z = g.add(x, y);
405        g.backward(z);
406        assert!(approx_eq(g.grad(x).expect("test: should succeed"), 1.0));
407        assert!(approx_eq(g.grad(y).expect("test: should succeed"), 1.0));
408    }
409
410    #[test]
411    fn test_backward_mul_chain_rule() {
412        // z = x * y  =>  dz/dx = y, dz/dy = x
413        let mut g = AutogradGraph::new();
414        let x = g.input(3.0);
415        let y = g.input(4.0);
416        let z = g.mul(x, y);
417        g.backward(z);
418        assert!(approx_eq(g.grad(x).expect("test: should succeed"), 4.0));
419        assert!(approx_eq(g.grad(y).expect("test: should succeed"), 3.0));
420    }
421
422    #[test]
423    fn test_backward_neg_grad() {
424        // z = -x  =>  dz/dx = -1
425        let mut g = AutogradGraph::new();
426        let x = g.input(5.0);
427        let z = g.neg(x);
428        g.backward(z);
429        assert!(approx_eq(g.grad(x).expect("test: should succeed"), -1.0));
430    }
431
432    #[test]
433    fn test_backward_exp_grad() {
434        // z = e^x  =>  dz/dx = e^x = z_val
435        let mut g = AutogradGraph::new();
436        let x = g.input(2.0);
437        let z = g.exp(x);
438        g.backward(z);
439        let expected = 2.0_f64.exp();
440        assert!(approx_eq(
441            g.grad(x).expect("test: should succeed"),
442            expected
443        ));
444    }
445
446    #[test]
447    fn test_backward_ln_grad() {
448        // z = ln(x)  =>  dz/dx = 1/x
449        let mut g = AutogradGraph::new();
450        let x = g.input(4.0);
451        let z = g.ln(x);
452        g.backward(z);
453        assert!(approx_eq(g.grad(x).expect("test: should succeed"), 0.25));
454    }
455
456    #[test]
457    fn test_backward_pow_grad() {
458        // z = x^3  =>  dz/dx = 3x^2 = 3*4 = 12
459        let mut g = AutogradGraph::new();
460        let x = g.input(2.0);
461        let z = g.pow(x, 3.0);
462        g.backward(z);
463        assert!(approx_eq(g.grad(x).expect("test: should succeed"), 12.0));
464    }
465
466    // ------------------------------------------------------------------
467    // Chain rule tests
468    // ------------------------------------------------------------------
469
470    #[test]
471    fn test_chain_rule_x_squared() {
472        // z = x * x  =>  dz/dx = 2x = 2*3 = 6
473        let mut g = AutogradGraph::new();
474        let x = g.input(3.0);
475        let z = g.mul(x, x);
476        g.backward(z);
477        assert!(approx_eq(g.grad(x).expect("test: should succeed"), 6.0));
478    }
479
480    #[test]
481    fn test_chain_rule_x_plus_y_times_x() {
482        // z = (x + y) * x
483        // dz/dx = (x+y) + x = 2x+y; dz/dy = x
484        // x=2, y=3 => dz/dx = 7, dz/dy = 2
485        let mut g = AutogradGraph::new();
486        let x = g.input(2.0);
487        let y = g.input(3.0);
488        let s = g.add(x, y);
489        let z = g.mul(s, x);
490        g.backward(z);
491        assert!(approx_eq(g.grad(x).expect("test: should succeed"), 7.0));
492        assert!(approx_eq(g.grad(y).expect("test: should succeed"), 2.0));
493    }
494
495    #[test]
496    fn test_chain_rule_exp_of_mul() {
497        // z = exp(x * y)
498        // dz/dx = exp(x*y) * y
499        // x=1, y=2 => z=e^2, dz/dx = 2*e^2
500        let mut g = AutogradGraph::new();
501        let x = g.input(1.0);
502        let y = g.input(2.0);
503        let p = g.mul(x, y);
504        let z = g.exp(p);
505        g.backward(z);
506        let expected = 2.0 * 2.0_f64.exp();
507        assert!(approx_eq(
508            g.grad(x).expect("test: should succeed"),
509            expected
510        ));
511    }
512
513    #[test]
514    fn test_chain_rule_ln_of_pow() {
515        // z = ln(x^2), x=3 => z = ln(9), dz/dx = 2/x = 2/3
516        let mut g = AutogradGraph::new();
517        let x = g.input(3.0);
518        let p = g.pow(x, 2.0);
519        let z = g.ln(p);
520        g.backward(z);
521        assert!(approx_eq(
522            g.grad(x).expect("test: should succeed"),
523            2.0 / 3.0
524        ));
525    }
526
527    #[test]
528    fn test_chain_rule_neg_of_add() {
529        // z = -(x + y), x=1, y=2 => dz/dx = -1, dz/dy = -1
530        let mut g = AutogradGraph::new();
531        let x = g.input(1.0);
532        let y = g.input(2.0);
533        let s = g.add(x, y);
534        let z = g.neg(s);
535        g.backward(z);
536        assert!(approx_eq(g.grad(x).expect("test: should succeed"), -1.0));
537        assert!(approx_eq(g.grad(y).expect("test: should succeed"), -1.0));
538    }
539
540    // ------------------------------------------------------------------
541    // Constant nodes — no gradient
542    // ------------------------------------------------------------------
543
544    #[test]
545    fn test_constant_node_no_grad() {
546        let mut g = AutogradGraph::new();
547        let c = g.constant(5.0);
548        let x = g.input(3.0);
549        let z = g.mul(x, c);
550        g.backward(z);
551        // constant node: grad should be None
552        assert!(g.grad(c).is_none());
553        // x should have gradient = c_val = 5.0
554        assert!(approx_eq(g.grad(x).expect("test: should succeed"), 5.0));
555    }
556
557    #[test]
558    fn test_constant_value_accessible() {
559        let mut g = AutogradGraph::new();
560        let c = g.constant(42.0);
561        assert!(approx_eq(g.value(c).expect("test: should succeed"), 42.0));
562    }
563
564    // ------------------------------------------------------------------
565    // zero_grad
566    // ------------------------------------------------------------------
567
568    #[test]
569    fn test_zero_grad_resets() {
570        let mut g = AutogradGraph::new();
571        let x = g.input(2.0);
572        let y = g.input(3.0);
573        let z = g.add(x, y);
574        g.backward(z);
575        assert!(approx_eq(g.grad(x).expect("test: should succeed"), 1.0));
576        g.zero_grad();
577        assert!(approx_eq(g.grad(x).expect("test: should succeed"), 0.0));
578        assert!(approx_eq(g.grad(y).expect("test: should succeed"), 0.0));
579    }
580
581    #[test]
582    fn test_zero_grad_then_recompute() {
583        let mut g = AutogradGraph::new();
584        let x = g.input(3.0);
585        let z = g.pow(x, 2.0);
586        g.backward(z);
587        g.zero_grad();
588        // Run backward again — should produce correct grad
589        g.backward(z);
590        assert!(approx_eq(g.grad(x).expect("test: should succeed"), 6.0));
591    }
592
593    // ------------------------------------------------------------------
594    // Edge cases
595    // ------------------------------------------------------------------
596
597    #[test]
598    fn test_grad_none_for_nonexistent_node() {
599        let g = AutogradGraph::new();
600        assert!(g.grad(9999).is_none());
601    }
602
603    #[test]
604    fn test_value_none_for_nonexistent_node() {
605        let g = AutogradGraph::new();
606        assert!(g.value(9999).is_none());
607    }
608
609    #[test]
610    fn test_backward_single_input_node() {
611        // backward on a raw input: grad should be 1.0
612        let mut g = AutogradGraph::new();
613        let x = g.input(5.0);
614        g.backward(x);
615        assert!(approx_eq(g.grad(x).expect("test: should succeed"), 1.0));
616    }
617
618    #[test]
619    fn test_multiple_uses_accumulate_correctly() {
620        // z = x * x + x  => dz/dx = 2x + 1 = 2*3+1 = 7
621        let mut g = AutogradGraph::new();
622        let x = g.input(3.0);
623        let x2 = g.mul(x, x);
624        let z = g.add(x2, x);
625        g.backward(z);
626        assert!(approx_eq(g.grad(x).expect("test: should succeed"), 7.0));
627    }
628
629    #[test]
630    fn test_deep_chain_add() {
631        // z = ((x + x) + x), x=1 => z=3, dz/dx = 3
632        let mut g = AutogradGraph::new();
633        let x = g.input(1.0);
634        let a = g.add(x, x);
635        let z = g.add(a, x);
636        g.backward(z);
637        assert!(approx_eq(g.grad(x).expect("test: should succeed"), 3.0));
638    }
639
640    #[test]
641    fn test_pow_fractional_exponent() {
642        // z = x^0.5 = sqrt(x), x=4 => dz/dx = 0.5 * x^(-0.5) = 0.25
643        let mut g = AutogradGraph::new();
644        let x = g.input(4.0);
645        let z = g.pow(x, 0.5);
646        g.backward(z);
647        assert!(approx_eq(g.grad(x).expect("test: should succeed"), 0.25));
648    }
649
650    #[test]
651    fn test_output_grad_is_one() {
652        // The output node itself should have grad = 1.0 after backward.
653        let mut g = AutogradGraph::new();
654        let x = g.input(2.0);
655        let z = g.exp(x);
656        g.backward(z);
657        assert!(approx_eq(g.nodes[&z].grad, 1.0));
658    }
659
660    #[test]
661    fn test_default_graph_is_empty() {
662        let g = AutogradGraph::default();
663        assert!(g.nodes.is_empty());
664    }
665
666    #[test]
667    fn test_autograd_op_clone_and_eq() {
668        let op1 = AutogradOp::Add { lhs: 0, rhs: 1 };
669        let op2 = op1.clone();
670        assert_eq!(op1, op2);
671
672        let op3 = AutogradOp::Pow {
673            base: 2,
674            exponent: 3.0,
675        };
676        assert_ne!(op1, op3);
677    }
678
679    #[test]
680    fn test_ln_of_exp_identity() {
681        // z = ln(exp(x)) = x => dz/dx = 1
682        let mut g = AutogradGraph::new();
683        let x = g.input(2.5);
684        let e = g.exp(x);
685        let z = g.ln(e);
686        // forward: should be ≈ 2.5
687        assert!((g.value(z).expect("test: should succeed") - 2.5).abs() < 1e-9);
688        g.backward(z);
689        assert!(approx_eq(g.grad(x).expect("test: should succeed"), 1.0));
690    }
691}