Sleipnir C++ API
Loading...
Searching...
No Matches
expression.hpp
1// Copyright (c) Sleipnir contributors
2
3#pragma once
4
5#include <stdint.h>
6
7#include <algorithm>
8#include <array>
9#include <concepts>
10#include <cstddef>
11#include <memory>
12#include <string_view>
13#include <tuple>
14#include <type_traits>
15#include <utility>
16
17#include <gch/small_vector.hpp>
18
19#include "sleipnir/autodiff/expression_type.hpp"
20#include "sleipnir/util/force_inline.hpp"
21#include "sleipnir/util/function_ref.hpp"
22#include "sleipnir/util/intrusive_shared_ptr.hpp"
23#include "sleipnir/util/pool.hpp"
24
25namespace slp::detail {
26
27#ifdef _WIN32
28inline constexpr bool USE_POOL_ALLOCATOR = false;
29#else
30inline constexpr bool USE_POOL_ALLOCATOR = true;
31#endif
32
33template <typename Scalar>
34struct Expression;
35
37inline constexpr size_t MAX_ARGS = 4;
38
42template <typename T>
43using ArgArray = std::array<T, MAX_ARGS>;
44
48template <typename Scalar>
49using ExpressionPtr = IntrusiveSharedPtr<Expression<Scalar>>;
50
56template <typename T, typename... Args>
57static ExpressionPtr<typename T::Scalar> make_expression_ptr(Args&&... args) {
58 if constexpr (USE_POOL_ALLOCATOR) {
59 return allocate_intrusive_shared<T>(global_pool_allocator<T>(),
60 std::forward<Args>(args)...);
61 } else {
62 return make_intrusive_shared<T>(std::forward<Args>(args)...);
63 }
64}
65
66template <typename Scalar, ExpressionType T>
67struct BinaryMinusExpression;
68
69template <typename Scalar, ExpressionType T>
70struct BinaryPlusExpression;
71
72template <typename Scalar, ExpressionType T>
73struct DivExpression;
74
75template <typename Scalar, ExpressionType T>
76struct MultExpression;
77
78template <typename Scalar, ExpressionType T>
79struct UnaryMinusExpression;
80
89template <template <typename, ExpressionType> typename T, typename Scalar,
90 typename... Args>
91SLEIPNIR_FORCE_INLINE ExpressionPtr<Scalar> make_typed_expression_ptr(
92 ExpressionType type, const Args&... args) {
93 using enum ExpressionType;
94
95 if (type == LINEAR) {
96 return make_expression_ptr<T<Scalar, LINEAR>>(args...);
97 } else if (type == QUADRATIC) {
98 return make_expression_ptr<T<Scalar, QUADRATIC>>(args...);
99 } else {
100 return make_expression_ptr<T<Scalar, NONLINEAR>>(args...);
101 }
102}
103
108template <typename Scalar>
109ExpressionPtr<Scalar> constant_ptr(Scalar value);
110
120template <typename Value, typename Scalar>
121Value lit(Scalar value) {
122 if constexpr (std::same_as<Value, Scalar>) {
123 return value;
124 } else {
125 return constant_ptr(value);
126 }
127}
128
132template <typename Scalar_>
135 using Scalar = Scalar_;
136
139
142
146
156 int32_t scratch = -1;
157
159 uint32_t ref_count : 31 = 0;
160
166 uint32_t is_leaf : 1 = 0;
167
169 constexpr Expression() = default;
170
174 explicit constexpr Expression(Scalar value) : val{value}, is_leaf{1} {}
175
176 virtual ~Expression() = default;
177
182 SLEIPNIR_FORCE_INLINE constexpr bool is_constant(Scalar constant) const {
183 return type() == ExpressionType::CONSTANT && val == constant;
184 }
185
191 const ExpressionPtr<Scalar>& rhs) {
192 using enum ExpressionType;
193
194 // Prune expression
195 if (lhs->is_constant(Scalar(0))) {
196 // Return zero, which lhs currently is
197 return lhs;
198 } else if (rhs->is_constant(Scalar(0))) {
199 // Return zero, which rhs currently is
200 return rhs;
201 } else if (lhs->is_constant(Scalar(1))) {
202 // Return rhs unmodified
203 return rhs;
204 } else if (rhs->is_constant(Scalar(1))) {
205 // Return lhs unmodified
206 return lhs;
207 }
208
209 // Evaluate constant
210 if (lhs->type() == CONSTANT && rhs->type() == CONSTANT) {
211 return constant_ptr(lhs->val * rhs->val);
212 }
213
214 // Evaluate expression type. Scaling by a constant keeps the other
215 // operand's type.
216 auto type = NONLINEAR;
217 if (lhs->type() == CONSTANT || rhs->type() == CONSTANT) {
218 type = std::max(lhs->type(), rhs->type());
219 } else if (lhs->type() == LINEAR && rhs->type() == LINEAR) {
220 type = QUADRATIC;
221 }
222 return make_typed_expression_ptr<MultExpression, Scalar>(type, lhs, rhs);
223 }
224
230 const ExpressionPtr<Scalar>& rhs) {
231 using enum ExpressionType;
232
233 // Prune expression
234 if (lhs->is_constant(Scalar(0))) {
235 // Return zero, which lhs currently is
236 return lhs;
237 } else if (rhs->is_constant(Scalar(1))) {
238 // Return lhs unmodified
239 return lhs;
240 }
241
242 // Evaluate constant
243 if (lhs->type() == CONSTANT && rhs->type() == CONSTANT) {
244 return constant_ptr(lhs->val / rhs->val);
245 }
246
247 // Evaluate expression type. Dividing by a constant keeps the numerator's
248 // type.
249 auto type = rhs->type() == CONSTANT ? lhs->type() : NONLINEAR;
250 return make_typed_expression_ptr<DivExpression, Scalar>(type, lhs, rhs);
251 }
252
258 const ExpressionPtr<Scalar>& rhs) {
259 using enum ExpressionType;
260
261 // Prune expression. We check for nullptr because operator+ is used in
262 // adjoint accumulation, and child nodes can be null.
263 if (lhs == nullptr || lhs->is_constant(Scalar(0))) {
264 // Return rhs unmodified
265 return rhs;
266 } else if (rhs == nullptr || rhs->is_constant(Scalar(0))) {
267 // Return lhs unmodified
268 return lhs;
269 }
270
271 // Evaluate constant
272 if (lhs->type() == CONSTANT && rhs->type() == CONSTANT) {
273 return constant_ptr(lhs->val + rhs->val);
274 }
275
276 auto type = std::max(lhs->type(), rhs->type());
277 return make_typed_expression_ptr<BinaryPlusExpression, Scalar>(type, lhs,
278 rhs);
279 }
280
286 const ExpressionPtr<Scalar>& rhs) {
287 return lhs = lhs + rhs;
288 }
289
295 const ExpressionPtr<Scalar>& rhs) {
296 using enum ExpressionType;
297
298 // Prune expression
299 if (lhs->is_constant(Scalar(0))) {
300 if (rhs->is_constant(Scalar(0))) {
301 // Return zero, which rhs currently is
302 return rhs;
303 } else {
304 // Return rhs negated
305 return -rhs;
306 }
307 } else if (rhs->is_constant(Scalar(0))) {
308 // Return lhs unmodified
309 return lhs;
310 }
311
312 // Evaluate constant
313 if (lhs->type() == CONSTANT && rhs->type() == CONSTANT) {
314 return constant_ptr(lhs->val - rhs->val);
315 }
316
317 auto type = std::max(lhs->type(), rhs->type());
318 return make_typed_expression_ptr<BinaryMinusExpression, Scalar>(type, lhs,
319 rhs);
320 }
321
326 using enum ExpressionType;
327
328 // Prune expression
329 if (lhs->is_constant(Scalar(0))) {
330 // Return zero, which lhs currently is
331 return lhs;
332 }
333
334 // Evaluate constant
335 if (lhs->type() == CONSTANT) {
336 return constant_ptr(-lhs->val);
337 }
338
339 return make_typed_expression_ptr<UnaryMinusExpression, Scalar>(lhs->type(),
340 lhs);
341 }
342
347 return lhs;
348 }
349
353 virtual void visit_args(
354 [[maybe_unused]] function_ref<void(Expression<Scalar>* arg)> func) const {
355 }
356
362 virtual Scalar value() const = 0;
363
368 virtual ExpressionType type() const = 0;
369
373 virtual std::string_view name() const = 0;
374
376 virtual void accumulate_adjoints() const {}
377
379 virtual void accumulate_adjoints_expr() const {}
380};
381
403template <typename Derived, typename Scalar, size_t NumArgs,
404 ExpressionType Type = ExpressionType::NONLINEAR>
407 static constexpr size_t NUM_ARGS = NumArgs;
408
409 // Ensure ArgArray is big enough to hold one element per argument
410 static_assert(NumArgs <= MAX_ARGS, "Increase MAX_ARGS");
411
413 static constexpr bool CONSTANT_PARTIALS = false;
414
416 std::array<ExpressionPtr<Scalar>, NumArgs> args;
417
421 template <typename... Args>
422 requires(sizeof...(Args) == NumArgs &&
423 (std::same_as<std::remove_cvref_t<Args>, ExpressionPtr<Scalar>> &&
424 ...))
425 explicit constexpr NonLeafExpression(Args&&... args)
426 : args{std::forward<Args>(args)...} {}
427
432 function_ref<void(Expression<Scalar>* arg)> func) const final {
433 [&]<size_t... Is>(std::index_sequence<Is...>) {
434 (func(args[Is].get()), ...);
435 }(std::make_index_sequence<NumArgs>{});
436 }
437
441 Scalar value() const final {
442 return [&]<size_t... Is>(std::index_sequence<Is...>) {
443 return static_cast<const Derived&>(*this).eval(args[Is]->val...);
444 }(std::make_index_sequence<NumArgs>{});
445 }
446
450 ExpressionType type() const final { return Type; }
451
455 std::string_view name() const final { return Derived::NAME; }
456
458 void accumulate_adjoints() const final {
459 [&]<size_t... Is>(std::index_sequence<Is...>) {
460 // Evaluate gradients
461 ArgArray<Scalar> g{};
462 grad(g, this->val, args[Is]->val...);
463
464 // Accumulate adjoints
465 ((args[Is]->adj += this->adj * g[Is]), ...);
466 }(std::make_index_sequence<NumArgs>{});
467 }
468
470 void accumulate_adjoints_expr() const final {
471 [&]<size_t... Is>(std::index_sequence<Is...>) {
472 // If the partial derivatives are constant, evaluate them as Scalars
473 // instead of building them as expressions
474 if constexpr (Derived::CONSTANT_PARTIALS) {
475 // Evaluate gradients
476 ArgArray<Scalar> g{};
477 grad(g, this->val, args[Is]->val...);
478
479 // Accumulate adjoints
480 (
481 [&] {
482 // Fold the adjoint accumulation expression if the gradient is
483 // 1, -1, or 0
484 if (g[Is] == Scalar(1)) {
485 args[Is]->adj_expr += this->adj_expr;
486 } else if (g[Is] == Scalar(-1)) {
487 args[Is]->adj_expr += -this->adj_expr;
488 } else if (g[Is] != Scalar(0)) {
489 args[Is]->adj_expr += this->adj_expr * constant_ptr(g[Is]);
490 }
491 }(),
492 ...);
493 } else {
494 // Evaluate gradients
495 ArgArray<ExpressionPtr<Scalar>> g;
496 grad(g, ExpressionPtr<Scalar>{const_cast<NonLeafExpression*>(this)},
497 args[Is]...);
498
499 // Accumulate adjoints
500 (
501 [&] {
502 // A null partial derivative is structurally zero
503 if (g[Is] != nullptr) {
504 args[Is]->adj_expr += this->adj_expr * g[Is];
505 }
506 }(),
507 ...);
508 }
509 }(std::make_index_sequence<NumArgs>{});
510 }
511
512 private:
525 template <typename Value, typename... Args>
526 void grad(ArgArray<Value>& g, const Value& val, const Args&... x) const {
527 if constexpr (requires(const Derived& expr) {
528 expr.eval_grad(g, val, x...);
529 }) {
530 static_cast<const Derived&>(*this).eval_grad(g, val, x...);
531 }
532 }
533};
534
544template <typename T, typename... Args>
545ExpressionPtr<typename T::Scalar> fold_or_make(const Args&... args) {
546 using Scalar = typename T::Scalar;
547
548 if (((args->type() == ExpressionType::CONSTANT) && ...)) {
549 Scalar val = T::eval(args->val...);
550 if constexpr (sizeof...(Args) == 1) {
551 const auto& x = std::get<0>(std::forward_as_tuple(args...));
552 if (x->val == val) {
553 return x;
554 }
555 }
556 return constant_ptr(val);
557 }
558
559 return make_expression_ptr<T>(args...);
560}
561
566template <typename Scalar, ExpressionType T>
568 : NonLeafExpression<BinaryMinusExpression<Scalar, T>, Scalar, 2, T> {
569 using BinaryMinusExpression::NonLeafExpression::NonLeafExpression;
570
572 static constexpr std::string_view NAME = "binary minus";
573
575 static constexpr bool CONSTANT_PARTIALS = true;
576
577 static Scalar eval(Scalar lhs, Scalar rhs) { return lhs - rhs; }
578
579 template <typename Value>
580 static void eval_grad(ArgArray<Value>& g, [[maybe_unused]] const Value& val,
581 [[maybe_unused]] const Value& lhs,
582 [[maybe_unused]] const Value& rhs) {
583 g[0] = lit<Value>(Scalar(1));
584 g[1] = lit<Value>(Scalar(-1));
585 }
586};
587
592template <typename Scalar, ExpressionType T>
594 : NonLeafExpression<BinaryPlusExpression<Scalar, T>, Scalar, 2, T> {
595 using BinaryPlusExpression::NonLeafExpression::NonLeafExpression;
596
598 static constexpr std::string_view NAME = "binary plus";
599
601 static constexpr bool CONSTANT_PARTIALS = true;
602
603 static Scalar eval(Scalar lhs, Scalar rhs) { return lhs + rhs; }
604
605 template <typename Value>
606 static void eval_grad(ArgArray<Value>& g, [[maybe_unused]] const Value& val,
607 [[maybe_unused]] const Value& lhs,
608 [[maybe_unused]] const Value& rhs) {
609 g[0] = lit<Value>(Scalar(1));
610 g[1] = lit<Value>(Scalar(1));
611 }
612};
613
618template <typename Scalar, ExpressionType T>
619struct DivExpression final
620 : NonLeafExpression<DivExpression<Scalar, T>, Scalar, 2, T> {
621 using DivExpression::NonLeafExpression::NonLeafExpression;
622
624 static constexpr std::string_view NAME = "division";
625
626 static Scalar eval(Scalar lhs, Scalar rhs) { return lhs / rhs; }
627
628 template <typename Value>
629 static void eval_grad(ArgArray<Value>& g, [[maybe_unused]] const Value& val,
630 const Value& lhs, const Value& rhs) {
631 g[0] = lit<Value>(Scalar(1)) / rhs;
632 g[1] = -lhs / (rhs * rhs);
633 }
634};
635
640template <typename Scalar, ExpressionType T>
641struct MultExpression final
642 : NonLeafExpression<MultExpression<Scalar, T>, Scalar, 2, T> {
643 using MultExpression::NonLeafExpression::NonLeafExpression;
644
646 static constexpr std::string_view NAME = "multiplication";
647
648 static Scalar eval(Scalar lhs, Scalar rhs) { return lhs * rhs; }
649
650 template <typename Value>
651 static void eval_grad(ArgArray<Value>& g, [[maybe_unused]] const Value& val,
652 const Value& lhs, const Value& rhs) {
653 g[0] = rhs;
654 g[1] = lhs;
655 }
656};
657
662template <typename Scalar, ExpressionType T>
664 : NonLeafExpression<UnaryMinusExpression<Scalar, T>, Scalar, 1, T> {
665 using UnaryMinusExpression::NonLeafExpression::NonLeafExpression;
666
668 static constexpr std::string_view NAME = "unary minus";
669
671 static constexpr bool CONSTANT_PARTIALS = true;
672
673 static Scalar eval(Scalar lhs) { return -lhs; }
674
675 template <typename Value>
676 static void eval_grad(ArgArray<Value>& g, [[maybe_unused]] const Value& val,
677 [[maybe_unused]] const Value& lhs) {
678 g[0] = lit<Value>(Scalar(-1));
679 }
680};
681
686template <typename Scalar>
687constexpr void inc_ref_count(Expression<Scalar>* expr) {
688 ++expr->ref_count;
689}
690
696template <typename Scalar>
697void deallocate_expression(Expression<Scalar>* expr) {
698 // If a deeply nested tree is being deallocated all at once, calling the
699 // Expression destructor when expr's refcount reaches zero can cause a stack
700 // overflow. Instead, we iterate over its children to decrement their
701 // refcounts and deallocate them.
702 gch::small_vector<Expression<Scalar>*> stack;
703 stack.emplace_back(expr);
704
705 // Decrements a child's refcount and enqueues it for deallocation if it
706 // reaches zero
707 auto release = [&stack](Expression<Scalar>* child) {
708 if (--child->ref_count == 0) {
709 stack.emplace_back(child);
710 }
711 };
712
713 while (!stack.empty()) {
714 auto elem = stack.back();
715 stack.pop_back();
716
717 if (elem->adj_expr != nullptr) {
718 release(elem->adj_expr.get());
719 }
720 elem->visit_args(release);
721
722 // Not calling the destructor here is safe because it only decrements
723 // refcounts, which was already done above
724 if constexpr (USE_POOL_ALLOCATOR) {
725 auto alloc = global_pool_allocator<Expression<Scalar>>();
726 std::allocator_traits<decltype(alloc)>::deallocate(alloc, elem, 1);
727 } else {
728 operator delete(elem);
729 }
730 }
731}
732
737template <typename Scalar>
738constexpr void dec_ref_count(Expression<Scalar>* expr) {
739 if (--expr->ref_count == 0) {
740 deallocate_expression(expr);
741 }
742}
743
747template <typename Scalar>
748struct ConstantExpression final : Expression<Scalar> {
752 explicit constexpr ConstantExpression(Scalar value)
753 : Expression<Scalar>{value} {}
754
755 Scalar value() const override { return this->val; }
756
757 ExpressionType type() const override { return ExpressionType::CONSTANT; }
758
759 std::string_view name() const override { return "constant"; }
760};
761
762template <typename Scalar>
763ExpressionPtr<Scalar> constant_ptr(Scalar value) {
764 return make_expression_ptr<ConstantExpression<Scalar>>(value);
765}
766
770template <typename Scalar>
774
780
781 Scalar value() const override { return this->val; }
782
783 ExpressionType type() const override { return ExpressionType::LINEAR; }
784
785 std::string_view name() const override { return "decision variable"; }
786};
787
788} // namespace slp::detail
Definition intrusive_shared_ptr.hpp:27
Definition function_ref.hpp:13
Definition expression.hpp:568
static constexpr bool CONSTANT_PARTIALS
Whether the partial derivatives are the same at every argument value.
Definition expression.hpp:575
static constexpr std::string_view NAME
The expression's name.
Definition expression.hpp:572
Definition expression.hpp:594
static constexpr std::string_view NAME
The expression's name.
Definition expression.hpp:598
static constexpr bool CONSTANT_PARTIALS
Whether the partial derivatives are the same at every argument value.
Definition expression.hpp:601
Definition expression.hpp:748
ExpressionType type() const override
Definition expression.hpp:757
Scalar value() const override
Definition expression.hpp:755
std::string_view name() const override
Definition expression.hpp:759
constexpr ConstantExpression(Scalar value)
Definition expression.hpp:752
Definition expression.hpp:771
Scalar value() const override
Definition expression.hpp:781
std::string_view name() const override
Definition expression.hpp:785
constexpr DecisionVariableExpression(Scalar value)
Definition expression.hpp:778
ExpressionType type() const override
Definition expression.hpp:783
constexpr DecisionVariableExpression()
Constructs a decision variable expression with a value of zero.
Definition expression.hpp:773
Definition expression.hpp:620
static constexpr std::string_view NAME
The expression's name.
Definition expression.hpp:624
Definition expression.hpp:133
Scalar val
The value of the expression node.
Definition expression.hpp:138
virtual Scalar value() const =0
ExpressionPtr< Scalar > adj_expr
Definition expression.hpp:145
friend ExpressionPtr< Scalar > operator-(const ExpressionPtr< Scalar > &lhs)
Definition expression.hpp:325
virtual void accumulate_adjoints() const
Accumulates the child adjoints as Scalars.
Definition expression.hpp:376
uint32_t is_leaf
Definition expression.hpp:166
uint32_t ref_count
Reference count for intrusive shared pointer.
Definition expression.hpp:159
friend ExpressionPtr< Scalar > operator+(const ExpressionPtr< Scalar > &lhs, const ExpressionPtr< Scalar > &rhs)
Definition expression.hpp:257
int32_t scratch
Definition expression.hpp:156
Scalar adj
The adjoint of the expression node, used during autodiff.
Definition expression.hpp:141
constexpr Expression()=default
Constructs a constant expression with a value of zero.
Scalar_ Scalar
Scalar type alias.
Definition expression.hpp:135
virtual ExpressionType type() const =0
friend ExpressionPtr< Scalar > operator/(const ExpressionPtr< Scalar > &lhs, const ExpressionPtr< Scalar > &rhs)
Definition expression.hpp:229
friend ExpressionPtr< Scalar > operator-(const ExpressionPtr< Scalar > &lhs, const ExpressionPtr< Scalar > &rhs)
Definition expression.hpp:294
friend ExpressionPtr< Scalar > operator*(const ExpressionPtr< Scalar > &lhs, const ExpressionPtr< Scalar > &rhs)
Definition expression.hpp:190
virtual std::string_view name() const =0
virtual void accumulate_adjoints_expr() const
Accumulates the child adjoints as Expressions.
Definition expression.hpp:379
virtual void visit_args(function_ref< void(Expression< Scalar > *arg)> func) const
Definition expression.hpp:353
SLEIPNIR_FORCE_INLINE constexpr bool is_constant(Scalar constant) const
Definition expression.hpp:182
friend ExpressionPtr< Scalar > operator+=(ExpressionPtr< Scalar > &lhs, const ExpressionPtr< Scalar > &rhs)
Definition expression.hpp:285
constexpr Expression(Scalar value)
Definition expression.hpp:174
friend ExpressionPtr< Scalar > operator+(const ExpressionPtr< Scalar > &lhs)
Definition expression.hpp:346
Definition expression.hpp:642
static constexpr std::string_view NAME
The expression's name.
Definition expression.hpp:646
Definition expression.hpp:405
static constexpr bool CONSTANT_PARTIALS
Whether the partial derivatives are the same at every argument value.
Definition expression.hpp:413
constexpr NonLeafExpression(Args &&... args)
Definition expression.hpp:425
Scalar value() const final
Definition expression.hpp:441
void accumulate_adjoints_expr() const final
Accumulates the child adjoints as Expressions.
Definition expression.hpp:470
static constexpr size_t NUM_ARGS
The number of arguments.
Definition expression.hpp:407
std::array< ExpressionPtr< Scalar >, NumArgs > args
The arguments in visit_args() order.
Definition expression.hpp:416
ExpressionType type() const final
Definition expression.hpp:450
void accumulate_adjoints() const final
Accumulates the child adjoints as Scalars.
Definition expression.hpp:458
void visit_args(function_ref< void(Expression< Scalar > *arg)> func) const final
Definition expression.hpp:431
std::string_view name() const final
Definition expression.hpp:455
Definition expression.hpp:664
static constexpr std::string_view NAME
The expression's name.
Definition expression.hpp:668
static constexpr bool CONSTANT_PARTIALS
Whether the partial derivatives are the same at every argument value.
Definition expression.hpp:671