Sleipnir C++ API
Loading...
Searching...
No Matches
bounds.hpp
1// Copyright (c) Sleipnir contributors
2
3#pragma once
4
5#include <algorithm>
6#include <limits>
7#include <span>
8#include <utility>
9
10#include <Eigen/Core>
11#include <Eigen/SparseCore>
12#include <gch/small_vector.hpp>
13
14#include "sleipnir/autodiff/expression_type.hpp"
15#include "sleipnir/autodiff/variable.hpp"
16#include "sleipnir/util/assert.hpp"
17
18// See docs/algorithms.md#Works_cited for citation definitions
19
20namespace slp {
21
25template <typename Scalar>
26struct Bounds {
28 Eigen::ArrayX<bool> bound_constraint_mask;
29
31 gch::small_vector<std::pair<Scalar, Scalar>> bounds;
32
35 gch::small_vector<std::pair<Eigen::Index, Eigen::Index>>
37};
38
54template <typename Scalar>
55Bounds<Scalar> get_bounds(
58 const Eigen::SparseMatrix<Scalar, Eigen::RowMajor>& A_i) {
59 // TODO: A blocked, out-of-place transpose should be much faster than
60 // traversing row major on a column major matrix unless we have few linear
61 // constraints (using a heuristic to choose between this and staying column
62 // major based on the number of constraints would be an easy performance
63 // improvement.)
64
65 // NB: Casting to long is unspecified if the size of decision_variable.size()
66 // is greater than the max long value, but then we wouldn't be able to fill
67 // A_i correctly anyway.
68 slp_assert(static_cast<Eigen::Index>(decision_variables.size()) ==
69 A_i.innerSize());
70 slp_assert(static_cast<Eigen::Index>(inequality_constraints.size()) ==
71 A_i.outerSize());
72
73 // Maps each decision variable's index to the indices of its upper and lower
74 // bounds if they exist, or NO_BOUND if they do not; used only for bookkeeping
75 // to compute conflicting bounds
76 constexpr Eigen::Index NO_BOUND = -1;
77 gch::small_vector<std::pair<Eigen::Index, Eigen::Index>>
80 // Lists pairs of indices of bound constraints in the inequality constraint
81 // list that conflict with each other
82 gch::small_vector<std::pair<Eigen::Index, Eigen::Index>>
83 conflicting_bound_indices;
84
85 // Maps each decision variable's index to its upper and lower bounds
86 gch::small_vector<std::pair<Scalar, Scalar>> decision_var_indices_to_bounds{
87 decision_variables.size(),
88 {-std::numeric_limits<Scalar>::infinity(),
89 std::numeric_limits<Scalar>::infinity()}};
90
91 // Vector corresponding to the inequality constraints where the i-th element
92 // is 1 if the i-th inequality constraint is a bound and 0 otherwise.
93 Eigen::ArrayX<bool> bound_constraint_mask{inequality_constraints.size()};
94 bound_constraint_mask.fill(false);
95
96 for (typename decltype(inequality_constraints)::size_type constraint_index =
97 0;
98 constraint_index < inequality_constraints.size(); ++constraint_index) {
99 // A constraint is a bound iff it is linear and its gradient has a
100 // single nonzero value.
101 if (inequality_constraints[constraint_index].type() !=
102 ExpressionType::LINEAR) {
103 continue;
104 }
105 const Eigen::SparseVector<Scalar>& row_A_i =
106 A_i.innerVector(constraint_index);
107 const auto non_zeros = row_A_i.nonZeros();
108 slp_assert(non_zeros != 0);
109 if (non_zeros > 1) {
110 // Constraint is in more than one variable and therefore not a bound.
111 continue;
112 }
113
114 // Claim: The bound given by a bound constraint is the constraint evaluated
115 // at zero divided by the nonzero element of the constraint's gradient.
116 //
117 // Proof: If c(x) is a bound constraint, then by definition c is a linear
118 // function in one variable, hence there exist a, b ∈ ℝ s.t. c(x) = axᵢ + b
119 // and a ≠ 0. The gradient of c is then aeᵢ (where eᵢ denotes the i-th basis
120 // element), and c(0) = b. If c(x) ≥ 0, then since either a < 0 or a > 0, we
121 // have either x ≤ -b/a or x ≥ -b/a, respectively. ∎
122 typename Eigen::SparseVector<Scalar>::InnerIterator row_iter(row_A_i);
123 const auto constraint_coefficient =
124 row_iter
125 .value(); // The nonzero value of the j-th constraint's gradient.
126 const auto decision_variable_index = row_iter.index();
127 const auto decision_variable_value =
128 decision_variables[decision_variable_index].value();
129 Scalar constraint_constant;
130 // We need to evaluate this constraint *exactly* at zero.
131 if (decision_variable_value != Scalar(0)) {
132 decision_variables[decision_variable_index].set_value(Scalar(0));
133 constraint_constant = inequality_constraints[constraint_index].value();
134 decision_variables[decision_variable_index].set_value(
135 decision_variable_value);
136 } else {
137 constraint_constant = inequality_constraints[constraint_index].value();
138 }
139
140 // Shouldn't happen since the constraint is supposed to be linear and not a
141 // constant.
142 slp_assert(constraint_coefficient != Scalar(0));
143
144 using std::isfinite;
145
146 // We should always get a finite value when evaluating the constraint at
147 // x = 0 since the constraint is linear.
148 slp_assert(isfinite(constraint_constant));
149
150 // This is possible if the user has provided a starting point at which their
151 // problem is ill-defined.
152 slp_assert(isfinite(constraint_coefficient));
153
154 // Update bounds; we assume constraints of the form c(x) ≥ 0.
155 auto& [lower_bound, upper_bound] =
156 decision_var_indices_to_bounds[decision_variable_index];
157 auto& [lower_index, upper_index] =
158 decision_var_indices_to_constraint_indices[decision_variable_index];
159 const auto detected_bound = -constraint_constant / constraint_coefficient;
160 if (constraint_coefficient < Scalar(0) && detected_bound < upper_bound) {
161 upper_bound = detected_bound;
162 upper_index = constraint_index;
163 } else if (constraint_coefficient > Scalar(0) &&
164 detected_bound > lower_bound) {
165 lower_bound = detected_bound;
166 lower_index = constraint_index;
167 }
168
169 // Update conflicting bounds
170 if (lower_bound > upper_bound) {
171 conflicting_bound_indices.emplace_back(lower_index, upper_index);
172 }
173
174 // Set the bound constraint mask appropriately.
175 bound_constraint_mask[constraint_index] = true;
176 }
177 return {bound_constraint_mask, decision_var_indices_to_bounds,
178 conflicting_bound_indices};
179}
180
193template <typename Derived>
194 requires(static_cast<bool>(Eigen::DenseBase<Derived>::IsVectorAtCompileTime))
195void project_onto_bounds(
196 Eigen::DenseBase<Derived>& x,
197 std::span<const std::pair<typename Eigen::DenseBase<Derived>::Scalar,
198 typename Eigen::DenseBase<Derived>::Scalar>>
199 decision_variable_indices_to_bounds,
200 const typename Eigen::DenseBase<Derived>::Scalar κ_1 =
201 typename Eigen::DenseBase<Derived>::Scalar(1e-2),
202 const typename Eigen::DenseBase<Derived>::Scalar κ_2 =
203 typename Eigen::DenseBase<Derived>::Scalar(1e-2)) {
204 using Scalar = typename Eigen::DenseBase<Derived>::Scalar;
205
206 using std::abs;
207 using std::isfinite;
208
209 slp_assert(κ_1 > Scalar(0) && κ_2 > Scalar(0) && κ_2 < Scalar(0.5));
210
211 Eigen::Index decision_variable_idx = 0;
212 for (const auto& [lower, upper] : decision_variable_indices_to_bounds) {
213 Scalar& x_i = x[decision_variable_idx++];
214
215 // We assume that bound infeasibility is handled elsewhere.
216 slp_assert(lower <= upper);
217
218 // See B.2 in [5] and section 3.6 in [2]
219 if (isfinite(lower) && isfinite(upper)) {
220 auto p_L = std::min(κ_1 * std::max(Scalar(1), abs(lower)),
221 κ_2 * (upper - lower));
222 auto p_U = std::min(κ_1 * std::max(Scalar(1), abs(upper)),
223 κ_2 * (upper - lower));
224 x_i = std::min(std::max(lower + p_L, x_i), upper - p_U);
225 } else if (isfinite(lower)) {
226 x_i = std::max(x_i, lower + κ_1 * std::max(Scalar(1), abs(lower)));
227 } else if (isfinite(upper)) {
228 x_i = std::min(x_i, upper - κ_1 * std::max(Scalar(1), abs(upper)));
229 }
230 }
231}
232} // namespace slp
Definition intrusive_shared_ptr.hpp:27
Definition bounds.hpp:26
gch::small_vector< std::pair< Scalar, Scalar > > bounds
The tightest bounds on each decision variable.
Definition bounds.hpp:31
gch::small_vector< std::pair< Eigen::Index, Eigen::Index > > conflicting_bound_indices
Definition bounds.hpp:36
Eigen::ArrayX< bool > bound_constraint_mask
Which constraints, if any, are bound constraints.
Definition bounds.hpp:28