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 for (auto& arg : node->args) {
49 // If the node hasn't been explored yet, add it to the stack
50 if (arg != nullptr && ++arg->scratch == 0) {
51 stack.push_back(arg.get());
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 for (auto& arg : node->args) {
70 // If we traversed all this node's incoming edges, add it to the stack
71 if (arg != nullptr && --arg->scratch == -1) {
72 stack.push_back(arg.get());
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 auto& lhs = node->args[0];
90 auto& rhs = node->args[1];
91
92 if (lhs != nullptr) {
93 node->val = node->value(lhs->val, rhs ? rhs->val : Scalar(0));
94 }
95 }
96}
97
106template <typename Scalar>
107void append_triplets(
108 const ExpressionGraph<Scalar>& top_list,
109 const gch::small_vector<std::pair<int, detail::Expression<Scalar>*>>&
110 output_list,
111 gch::small_vector<Eigen::Triplet<Scalar>>& triplets, int row) {
112 // Read docs/algorithms.md#Reverse_accumulation_automatic_differentiation
113 // for background on reverse accumulation automatic differentiation.
114
115 if (top_list.empty()) {
116 return;
117 }
118
119 // Set root node's adjoint to 1 since df/df is 1
120 top_list[0]->adjoint = Scalar(1);
121
122 // Zero the rest of the adjoints
123 for (auto& node : top_list | std::views::drop(1)) {
124 node->adjoint = Scalar(0);
125 }
126
127 // df/dx = (df/dy)(dy/dx). The adjoint of x is equal to the adjoint of y
128 // multiplied by dy/dx. If there are multiple "paths" from the root node to
129 // variable; the variable's adjoint is the sum of each path's adjoint
130 // contribution.
131 for (const auto& node : top_list) {
132 auto& lhs = node->args[0];
133 auto& rhs = node->args[1];
134
135 if (lhs != nullptr) {
136 if (rhs != nullptr) {
137 // Binary operator
138 lhs->adjoint += node->grad_l(lhs->val, rhs->val);
139 rhs->adjoint += node->grad_r(lhs->val, rhs->val);
140 } else {
141 // Unary operator
142 lhs->adjoint += node->grad_l(lhs->val, Scalar(0));
143 }
144 }
145 }
146
147 // Exploit the row's sparsity pattern by only appending wrt adjoints that
148 // appear in the expression graph
149 for (const auto& [col, node] : output_list) {
150 // Append adjoints of wrt to sparse matrix triplets
151 triplets.emplace_back(row, col, node->adjoint);
152 }
153}
154
155} // namespace slp::detail