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