Sleipnir C++ API
Loading...
Searching...
No Matches
expression_graph.hpp
1// Copyright (c) Sleipnir contributors
2
3#pragma once
4
5#include <ranges>
6#include <utility>
7
8#include <Eigen/SparseCore>
9#include <gch/small_vector.hpp>
10
11#include "sleipnir/autodiff/expression.hpp"
12#include "sleipnir/autodiff/expression_type.hpp"
13
14namespace slp::detail {
15
19template <typename Scalar>
20using ExpressionGraph = gch::small_vector<Expression<Scalar>*>;
21
28template <typename Scalar>
29ExpressionGraph<Scalar> topological_sort(const ExpressionPtr<Scalar>& root) {
30 ExpressionGraph<Scalar> list;
31
32 // If the root type is constant, updates are a no-op, so return an empty list
33 if (root == nullptr || root->type() == ExpressionType::CONSTANT) {
34 return list;
35 }
36
37 // Stack of nodes to explore
38 gch::small_vector<Expression<Scalar>*> stack;
39
40 // Enumerate incoming edges for each node via depth-first search
41 //
42 // NOTE: scratch counts incoming edges, offset by -1 so -1 means no edges.
43 stack.emplace_back(root.get());
44 while (!stack.empty()) {
45 auto node = stack.back();
46 stack.pop_back();
47
48 node->visit_args([&stack](const auto& arg) {
49 // If the node hasn't been explored yet, add it to the stack
50 if (++arg->scratch == 0) {
51 stack.push_back(arg);
52 }
53 });
54 }
55
56 // Generate topological sort of graph from parent to child.
57 //
58 // A node is only added to the stack after all its incoming edges have been
59 // traversed. Expression::scratch is a decrementing counter for tracking this.
60 //
61 // https://en.wikipedia.org/wiki/Topological_sorting
62 stack.emplace_back(root.get());
63 while (!stack.empty()) {
64 auto node = stack.back();
65 stack.pop_back();
66
67 list.emplace_back(node);
68
69 node->visit_args([&stack](const auto& arg) {
70 // If we traversed all this node's incoming edges, add it to the stack
71 if (--arg->scratch == -1) {
72 stack.push_back(arg);
73 }
74 });
75 }
76
77 return list;
78}
79
85template <typename Scalar>
86void update_values(const ExpressionGraph<Scalar>& list) {
87 // Traverse graph from child to parent and update values
88 for (auto& node : list | std::views::reverse) {
89 // Leaf nodes don't need updating
90 if (!node->is_leaf) {
91 node->val = node->value();
92 }
93 }
94}
95
104template <typename Scalar>
105void append_triplets(
106 const ExpressionGraph<Scalar>& top_list,
107 const gch::small_vector<std::pair<int, detail::Expression<Scalar>*>>&
108 output_list,
109 gch::small_vector<Eigen::Triplet<Scalar>>& triplets, int row) {
110 // Read docs/algorithms.md#Reverse_accumulation_automatic_differentiation
111 // for background on reverse accumulation automatic differentiation.
112
113 if (top_list.empty()) {
114 return;
115 }
116
117 // Set root node's adjoint to 1 since df/df is 1
118 top_list[0]->adj = Scalar(1);
119
120 // Zero the rest of the adjoints
121 for (auto& node : top_list | std::views::drop(1)) {
122 node->adj = Scalar(0);
123 }
124
125 // df/dx = (df/dy)(dy/dx). The adjoint of x is equal to the adjoint of y
126 // multiplied by dy/dx. If there are multiple "paths" from the root node to
127 // variable; the variable's adjoint is the sum of each path's adjoint
128 // contribution.
129 for (const auto& node : top_list) {
130 // Leaf nodes have no children to propagate adjoints to
131 if (!node->is_leaf) {
132 node->accumulate_adjoints();
133 }
134 }
135
136 // Exploit the row's sparsity pattern by only appending wrt adjoints that
137 // appear in the expression graph
138 for (const auto& [col, node] : output_list) {
139 // Append adjoints of wrt to sparse matrix triplets
140 triplets.emplace_back(row, col, node->adj);
141 }
142}
143
144} // namespace slp::detail