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
52template <typename Scalar>
55 const Eigen::SparseMatrix<Scalar>& A_i) {
56 // NB: Casting to long is unspecified if the size of decision_variable.size()
57 // is greater than the max long value, but then we wouldn't be able to fill
58 // A_i correctly anyway.
59 slp_assert(static_cast<Eigen::Index>(decision_variables.size()) ==
60 A_i.cols());
61 slp_assert(static_cast<Eigen::Index>(inequality_constraints.size()) ==
62 A_i.rows());
63
64 // For each row of A_i, count its structural nonzeros and record the column
65 // and value of its most recently visited nonzero. If the count is 1, that's
66 // the row's only nonzero; otherwise, the recorded column and value are
67 // meaningless, but those rows aren't bound constraints, so they're never
68 // read.
69 gch::small_vector<int> row_num_nonzeros(A_i.rows(), 0);
70 gch::small_vector<int> row_nonzero_col(A_i.rows());
71 gch::small_vector<Scalar> row_nonzero_value(A_i.rows());
72 for (int col = 0; col < A_i.outerSize(); ++col) {
73 for (typename Eigen::SparseMatrix<Scalar>::InnerIterator it{A_i, col}; it;
74 ++it) {
75 ++row_num_nonzeros[it.row()];
76 row_nonzero_col[it.row()] = it.col();
77 row_nonzero_value[it.row()] = it.value();
78 }
79 }
80
81 // Maps each decision variable's index to the indices of its upper and lower
82 // bounds if they exist, or NO_BOUND if they do not; used only for bookkeeping
83 // to compute conflicting bounds
84 constexpr Eigen::Index NO_BOUND = -1;
85 gch::small_vector<std::pair<Eigen::Index, Eigen::Index>>
86 decision_var_indices_to_constraint_indices{decision_variables.size(),
87 {NO_BOUND, NO_BOUND}};
88 // Lists pairs of indices of bound constraints in the inequality constraint
89 // list that conflict with each other
90 gch::small_vector<std::pair<Eigen::Index, Eigen::Index>>
91 conflicting_bound_indices;
92
93 // Maps each decision variable's index to its upper and lower bounds
94 gch::small_vector<std::pair<Scalar, Scalar>> decision_var_indices_to_bounds{
95 decision_variables.size(),
96 {-std::numeric_limits<Scalar>::infinity(),
97 std::numeric_limits<Scalar>::infinity()}};
98
99 // Vector corresponding to the inequality constraints where the i-th element
100 // is 1 if the i-th inequality constraint is a bound and 0 otherwise.
101 Eigen::ArrayX<bool> bound_constraint_mask{inequality_constraints.size()};
102 bound_constraint_mask.fill(false);
103
104 for (typename decltype(inequality_constraints)::size_type constraint_index =
105 0;
106 constraint_index < inequality_constraints.size(); ++constraint_index) {
107 // A constraint is a bound iff it is linear and its gradient has a
108 // single nonzero value.
109 if (inequality_constraints[constraint_index].type() !=
110 ExpressionType::LINEAR) {
111 continue;
112 }
113 if (row_num_nonzeros[constraint_index] != 1) {
114 // Constraint is not a bound because either:
115 //
116 // 1. It's in more than one variable
117 // 2. It's in no decision variables (e.g., it only depends on variables
118 // that aren't decision variables of this problem)
119 continue;
120 }
121
122 // Claim: The bound given by a bound constraint is the constraint evaluated
123 // at zero divided by the nonzero element of the constraint's gradient.
124 //
125 // Proof: If c(x) is a bound constraint, then by definition c is a linear
126 // function in one variable, hence there exist a, b ∈ ℝ s.t. c(x) = axᵢ + b
127 // and a ≠ 0. The gradient of c is then aeᵢ (where eᵢ denotes the i-th basis
128 // element), and c(0) = b. If c(x) ≥ 0, then since either a < 0 or a > 0, we
129 // have either x ≤ -b/a or x ≥ -b/a, respectively. ∎
130 //
131 // The nonzero value of the j-th constraint's gradient.
132 const auto constraint_coefficient = row_nonzero_value[constraint_index];
133
134 // The Jacobian's sparsity pattern is structural, so the single stored value
135 // can be zero if the constraint's terms cancel (e.g., x − x + 1 ≥ 0). The
136 // constraint is then constant and not a bound.
137 if (constraint_coefficient == Scalar(0)) {
138 continue;
139 }
140
141 const auto decision_variable_index = row_nonzero_col[constraint_index];
142 const auto decision_variable_value =
143 decision_variables[decision_variable_index].value();
144 Scalar constraint_constant;
145 // We need to evaluate this constraint *exactly* at zero.
146 if (decision_variable_value != Scalar(0)) {
147 decision_variables[decision_variable_index].set_value(Scalar(0));
148 constraint_constant = inequality_constraints[constraint_index].value();
149 decision_variables[decision_variable_index].set_value(
150 decision_variable_value);
151 } else {
152 constraint_constant = inequality_constraints[constraint_index].value();
153 }
154
155 using std::isfinite;
156
157 // The constraint's constant or coefficient can be nonfinite if the user
158 // wrote a constraint like x ≥ −∞ or ∞x ≥ 1. Note that this constraint will
159 // cause the solver to report a nonfinite initial guess later.
160 if (!isfinite(constraint_constant) || !isfinite(constraint_coefficient)) {
161 continue;
162 }
163
164 // Update bounds; we assume constraints of the form c(x) ≥ 0.
165 auto& [lower_bound, upper_bound] =
166 decision_var_indices_to_bounds[decision_variable_index];
167 auto& [lower_index, upper_index] =
168 decision_var_indices_to_constraint_indices[decision_variable_index];
169 const auto detected_bound = -constraint_constant / constraint_coefficient;
170 if (constraint_coefficient < Scalar(0) && detected_bound < upper_bound) {
171 upper_bound = detected_bound;
172 upper_index = constraint_index;
173 } else if (constraint_coefficient > Scalar(0) &&
174 detected_bound > lower_bound) {
175 lower_bound = detected_bound;
176 lower_index = constraint_index;
177 }
178
179 // Update conflicting bounds
180 if (lower_bound > upper_bound) {
181 conflicting_bound_indices.emplace_back(lower_index, upper_index);
182 }
183
184 // Set the bound constraint mask appropriately.
185 bound_constraint_mask[constraint_index] = true;
186 }
187 return {bound_constraint_mask, decision_var_indices_to_bounds,
188 conflicting_bound_indices};
189}
190
203template <typename Derived>
204 requires(static_cast<bool>(Eigen::DenseBase<Derived>::IsVectorAtCompileTime))
205void project_onto_bounds(
206 Eigen::DenseBase<Derived>& x,
207 std::span<const std::pair<typename Eigen::DenseBase<Derived>::Scalar,
208 typename Eigen::DenseBase<Derived>::Scalar>>
209 decision_variable_indices_to_bounds,
210 const typename Eigen::DenseBase<Derived>::Scalar κ_1 =
211 typename Eigen::DenseBase<Derived>::Scalar(1e-2),
212 const typename Eigen::DenseBase<Derived>::Scalar κ_2 =
213 typename Eigen::DenseBase<Derived>::Scalar(1e-2)) {
214 using Scalar = typename Eigen::DenseBase<Derived>::Scalar;
215
216 using std::abs;
217 using std::isfinite;
218
219 slp_assert(κ_1 > Scalar(0) && κ_2 > Scalar(0) && κ_2 < Scalar(0.5));
220
221 Eigen::Index decision_variable_idx = 0;
222 for (const auto& [lower, upper] : decision_variable_indices_to_bounds) {
223 Scalar& x_i = x[decision_variable_idx++];
224
225 // We assume that bound infeasibility is handled elsewhere.
226 slp_assert(lower <= upper);
227
228 // See section 3.6 of [2]
229 if (isfinite(lower) && isfinite(upper)) {
230 auto p_L = std::min(κ_1 * std::max(Scalar(1), abs(lower)),
231 κ_2 * (upper - lower));
232 auto p_U = std::min(κ_1 * std::max(Scalar(1), abs(upper)),
233 κ_2 * (upper - lower));
234 x_i = std::min(std::max(lower + p_L, x_i), upper - p_U);
235 } else if (isfinite(lower)) {
236 x_i = std::max(x_i, lower + κ_1 * std::max(Scalar(1), abs(lower)));
237 } else if (isfinite(upper)) {
238 x_i = std::min(x_i, upper - κ_1 * std::max(Scalar(1), abs(upper)));
239 }
240 }
241}
242} // 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