Sleipnir C++ API
Loading...
Searching...
No Matches
feasibility_restoration.hpp
1// Copyright (c) Sleipnir contributors
2
3#pragma once
4
5#include <algorithm>
6#include <cmath>
7#include <functional>
8#include <span>
9#include <tuple>
10#include <utility>
11
12#include <Eigen/Core>
13#include <Eigen/SparseCore>
14#include <gch/small_vector.hpp>
15
16#include "sleipnir/optimization/solver/exit_status.hpp"
17#include "sleipnir/optimization/solver/ipm_matrix_callbacks.hpp"
18#include "sleipnir/optimization/solver/iteration_info.hpp"
19#include "sleipnir/optimization/solver/options.hpp"
20#include "sleipnir/optimization/solver/sqp_matrix_callbacks.hpp"
21#include "sleipnir/optimization/solver/util/append_as_triplets.hpp"
22#include "sleipnir/optimization/solver/util/problem_scaling.hpp"
23#include "sleipnir/util/print_diagnostics.hpp"
24
25namespace slp {
26
27template <typename Scalar>
28ExitStatus ipm(const IPMMatrixCallbacks<Scalar>& matrix_callbacks,
29 std::span<std::function<bool(const IterationInfo<Scalar>& info)>>
30 iteration_callbacks,
31 const Options<Scalar>& options, bool in_feasibility_restoration,
32#ifdef SLEIPNIR_ENABLE_BOUND_PROJECTION
33 const Eigen::ArrayX<bool>& bound_constraint_mask,
34#endif
35 Eigen::VectorX<Scalar>& x, Eigen::VectorX<Scalar>& s,
36 Eigen::VectorX<Scalar>& y, Eigen::VectorX<Scalar>& z, Scalar& μ,
37 int& iterations);
38
47template <typename Scalar>
48std::tuple<Eigen::VectorX<Scalar>, Eigen::VectorX<Scalar>> compute_p_n(
49 const Eigen::VectorX<Scalar>& c, Scalar ρ, Scalar μ) {
50 // From equation (33) of [2]:
51 // ______________________
52 // μ − ρ c(x) /(μ − ρ c(x))² μ c(x)
53 // n = −−−−−−−−−− + / (−−−−−−−−−−) + −−−−−− (1)
54 // 2ρ √ ( 2ρ ) 2ρ
55 //
56 // The quadratic formula:
57 // ________
58 // -b + √b² - 4ac
59 // x = −−−−−−−−−−−−−− (2)
60 // 2a
61 //
62 // Rearrange (1) to fit the quadratic formula better:
63 // _________________________
64 // μ - ρ c(x) + √(μ - ρ c(x))² + 2ρ μ c(x)
65 // n = −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
66 // 2ρ
67 //
68 // Solve for coefficients:
69 //
70 // a = ρ (3)
71 // b = ρ c(x) - μ (4)
72 //
73 // -4ac = 2ρ μ c(x)
74 // -4(ρ)c = 2ρ μ c(x)
75 // -4c = 2μ c(x)
76 // c = -μ c(x)/2 (5)
77 //
78 // p = c(x) + n (6)
79
80 using DenseVector = Eigen::VectorX<Scalar>;
81
82 using std::sqrt;
83
84 DenseVector p{c.rows()};
85 DenseVector n{c.rows()};
86 for (int row = 0; row < p.rows(); ++row) {
87 Scalar _a = ρ;
88 Scalar _b = ρ * c[row] - μ;
89 Scalar _c = -μ * c[row] / Scalar(2);
90
91 n[row] = (-_b + sqrt(_b * _b - Scalar(4) * _a * _c)) / (Scalar(2) * _a);
92 p[row] = c[row] + n[row];
93 }
94
95 return {std::move(p), std::move(n)};
96}
97
113template <typename Scalar>
114ExitStatus feasibility_restoration(
115 const SQPMatrixCallbacks<Scalar>& matrix_callbacks,
116 std::span<std::function<bool(const IterationInfo<Scalar>& info)>>
117 iteration_callbacks,
118 const Options<Scalar>& options, Eigen::VectorX<Scalar>& x,
119 Eigen::VectorX<Scalar>& y, int& iterations) {
120 // Feasibility restoration
121 //
122 // min ρ Σ (pₑ + nₑ) + ζ/2 (x - xᵣ)ᵀDᵣ(x - xᵣ)
123 // x
124 // pₑ,nₑ
125 //
126 // s.t. cₑ(x) - pₑ + nₑ = 0
127 // pₑ ≥ 0
128 // nₑ ≥ 0
129 //
130 // where ρ = 1000, ζ = √μ where μ is the barrier parameter, xᵣ is original
131 // iterate before feasibility restoration, and Dᵣ is a scaling matrix defined
132 // by
133 //
134 // Dᵣ = diag(min(1, 1/xᵣ[i]²) for i in x.rows())
135
136 using DenseVector = Eigen::VectorX<Scalar>;
137 using DiagonalMatrix = Eigen::DiagonalMatrix<Scalar, Eigen::Dynamic>;
138 using SparseMatrix = Eigen::SparseMatrix<Scalar>;
139 using SparseVector = Eigen::SparseVector<Scalar>;
140
141 using std::sqrt;
142
143 const auto& matrices = matrix_callbacks;
144 const auto& num_vars = matrices.num_decision_variables;
145 const auto& num_eq = matrices.num_equality_constraints;
146
147 constexpr Scalar ρ(1e3);
148 const Scalar μ = options.tolerance / Scalar(10);
149
150 const DenseVector c_e = matrices.c_e(x);
151
152 Scalar fr_μ = std::max(μ, c_e.template lpNorm<Eigen::Infinity>());
153 const Scalar ζ = sqrt(fr_μ);
154
155 const auto& x_r = x;
156 const auto [p_e_0, n_e_0] = compute_p_n(c_e, ρ, fr_μ);
157
158 // Dᵣ = diag(min(1, 1/xᵣ[i]²) for i in x.rows())
159 const DiagonalMatrix D_r =
160 x.cwiseSquare().cwiseInverse().cwiseMin(Scalar(1)).asDiagonal();
161
162 DenseVector fr_x{num_vars + 2 * num_eq};
163 fr_x << x, p_e_0, n_e_0;
164
165 // Start the constraints pₑ, nₑ ≥ 0 with zero violation
166 DenseVector fr_s{2 * num_eq};
167 fr_s << p_e_0, n_e_0;
168
169 DenseVector fr_y = DenseVector::Zero(num_eq);
170
171 // Start the duals with perfect complementarity with respect to the slacks
172 DenseVector fr_z{2 * num_eq};
173 fr_z << fr_μ * p_e_0.cwiseInverse(), fr_μ * n_e_0.cwiseInverse();
174
175 // Inherit the parent problem's scaling for the constraints, and use no
176 // scaling for the cost function since it has changed. The new rows introduced
177 // are not scaled.
178 const ProblemScaling<Scalar> fr_scaling{Scalar(1), matrices.scaling.c_e,
179 DenseVector::Ones(2 * num_eq)};
180
181 IPMMatrixCallbacks<Scalar> fr_matrix_callbacks{
182 static_cast<int>(fr_x.rows()),
183 static_cast<int>(fr_y.rows()),
184 static_cast<int>(fr_z.rows()),
185 [&](const DenseVector& x_p) -> Scalar {
186 auto x = x_p.segment(0, num_vars);
187
188 // Cost function
189 //
190 // ρ Σ (pₑ + nₑ) + ζ/2 (x - xᵣ)ᵀDᵣ(x - xᵣ)
191
192 auto diff = x - x_r;
193 return ρ * x_p.segment(num_vars, 2 * num_eq).array().sum() +
194 ζ / Scalar(2) * diff.transpose() * D_r * diff;
195 },
196 [&](const DenseVector& x_p) -> SparseVector {
197 auto x = x_p.segment(0, num_vars);
198
199 // Cost function gradient
200 //
201 // [ζDᵣ(x − xᵣ)]
202 // [ ρ ]
203 // [ ρ ]
204 DenseVector g{x_p.rows()};
205 g.segment(0, num_vars) = ζ * D_r * (x - x_r);
206 g.segment(num_vars, 2 * num_eq).setConstant(ρ);
207 return g.sparseView();
208 },
209 [&](const DenseVector& x_p, const DenseVector& y_p,
210 [[maybe_unused]] const DenseVector& z_p) -> SparseMatrix {
211 auto x = x_p.segment(0, num_vars);
212 const auto& y = y_p;
213
214 // Cost function Hessian
215 //
216 // [ζDᵣ 0 0]
217 // [ 0 0 0]
218 // [ 0 0 0]
219 gch::small_vector<Eigen::Triplet<Scalar>> triplets;
220 triplets.reserve(x_p.rows());
221 append_as_triplets(triplets, 0, 0, {SparseMatrix{ζ * D_r}});
222 SparseMatrix d2f_dx2{x_p.rows(), x_p.rows()};
223 d2f_dx2.setFromSortedTriplets(triplets.begin(), triplets.end());
224
225 // Constraint part of original problem's Lagrangian Hessian
226 //
227 // −∇ₓₓ²yᵀcₑ(x)
228 auto H_c = matrices.H_c(x, y);
229 H_c.conservativeResize(x_p.rows(), x_p.rows());
230
231 // Lagrangian Hessian
232 //
233 // [ζDᵣ 0 0]
234 // [ 0 0 0] − ∇ₓₓ²yᵀcₑ(x)
235 // [ 0 0 0]
236 return d2f_dx2 + H_c;
237 },
238 [&](const DenseVector& x_p, [[maybe_unused]] const DenseVector& y_p,
239 [[maybe_unused]] const DenseVector& z_p) -> SparseMatrix {
240 return SparseMatrix{x_p.rows(), x_p.rows()};
241 },
242 [&](const DenseVector& x_p) -> DenseVector {
243 auto x = x_p.segment(0, num_vars);
244 auto p_e = x_p.segment(num_vars, num_eq);
245 auto n_e = x_p.segment(num_vars + num_eq, num_eq);
246
247 // Equality constraints
248 //
249 // cₑ(x) - pₑ + nₑ = 0
250 return matrices.c_e(x) - p_e + n_e;
251 },
252 [&](const DenseVector& x_p) -> SparseMatrix {
253 auto x = x_p.segment(0, num_vars);
254
255 // Equality constraint Jacobian
256 //
257 // [Aₑ −I I]
258
259 SparseMatrix A_e = matrices.A_e(x);
260
261 gch::small_vector<Eigen::Triplet<Scalar>> triplets;
262 triplets.reserve(A_e.nonZeros() + 2 * num_eq);
263
264 append_as_triplets(triplets, 0, 0, {A_e});
265 append_diagonal_as_triplets(
266 triplets, 0, num_vars,
267 DenseVector::Constant(num_eq, Scalar(-1)).eval());
268 append_diagonal_as_triplets(
269 triplets, 0, num_vars + num_eq,
270 DenseVector::Constant(num_eq, Scalar(1)).eval());
271
272 SparseMatrix A_e_p{A_e.rows(), x_p.rows()};
273 A_e_p.setFromSortedTriplets(triplets.begin(), triplets.end());
274 return A_e_p;
275 },
276 [&](const DenseVector& x_p) -> DenseVector {
277 // Inequality constraints
278 //
279 // pₑ ≥ 0
280 // nₑ ≥ 0
281 return x_p.segment(num_vars, 2 * num_eq);
282 },
283 [&](const DenseVector& x_p) -> SparseMatrix {
284 // Inequality constraint Jacobian
285 //
286 // [0 I 0]
287 // [0 0 I]
288
289 gch::small_vector<Eigen::Triplet<Scalar>> triplets;
290 triplets.reserve(2 * num_eq);
291
292 append_diagonal_as_triplets(
293 triplets, 0, num_vars,
294 DenseVector::Constant(2 * num_eq, Scalar(1)).eval());
295
296 SparseMatrix A_i_p{2 * num_eq, x_p.rows()};
297 A_i_p.setFromSortedTriplets(triplets.begin(), triplets.end());
298 return A_i_p;
299 },
300 fr_scaling};
301
302 auto status =
303 ipm<Scalar>(fr_matrix_callbacks, iteration_callbacks, options, true,
304#ifdef SLEIPNIR_ENABLE_BOUND_PROJECTION
305 Eigen::ArrayX<bool>::Constant(2 * num_eq, true),
306#endif
307 fr_x, fr_s, fr_y, fr_z, fr_μ, iterations);
308
309 x = fr_x.segment(0, x.rows());
310
311 // If feasibility restoration converged, check the constraint violation in
312 // the original problem's scaling
313 DenseVector c_e_unscaled;
314 bool c_e_violated = false;
315 if (status == ExitStatus::SUCCESS) {
316 c_e_unscaled = matrices.c_e(x);
317 if (matrices.scaling.c_e.size() > 0) {
318 c_e_unscaled =
319 matrices.scaling.c_e.cwiseInverse().cwiseProduct(c_e_unscaled);
320 }
321
322 c_e_violated =
323 c_e_unscaled.template lpNorm<Eigen::Infinity>() > options.tolerance;
324 }
325
326 // Return to the normal phase if feasibility restoration stopped early, or if
327 // it converged to a feasible point (e.g., because it was run to convergence)
328 if (status == ExitStatus::CALLBACK_REQUESTED_STOP ||
329 (status == ExitStatus::SUCCESS && !c_e_violated)) {
330 // Reset the equality constraint duals since feasibility restoration didn't
331 // account for the cost function
332 y.setZero();
333
334 return ExitStatus::SUCCESS;
335 } else if (status == ExitStatus::SUCCESS) {
336 // Feasibility restoration converged to a minimizer of the constraint
337 // violation, and the constraint violation is still above the tolerance, so
338 // that minimizer is a certificate of local infeasibility. Declaring local
339 // infeasibility anywhere else risks false positives (e.g., a
340 // point-in-time test can reject iterates the solver would otherwise
341 // escape). See section 3.3, p. 14 of [2].
342 if (options.diagnostics) {
343 print_c_e_local_infeasibility_error(c_e_unscaled, options.tolerance);
344 }
345
346 return ExitStatus::LOCALLY_INFEASIBLE;
347 } else {
348 return ExitStatus::FEASIBILITY_RESTORATION_FAILED;
349 }
350}
351
371template <typename Scalar>
372ExitStatus feasibility_restoration(
373 const IPMMatrixCallbacks<Scalar>& matrix_callbacks,
374 std::span<std::function<bool(const IterationInfo<Scalar>& info)>>
375 iteration_callbacks,
376 const Options<Scalar>& options,
377#ifdef SLEIPNIR_ENABLE_BOUND_PROJECTION
378 const Eigen::ArrayX<bool>& bound_constraint_mask,
379#endif
380 Eigen::VectorX<Scalar>& x, Eigen::VectorX<Scalar>& s,
381 Eigen::VectorX<Scalar>& y, Eigen::VectorX<Scalar>& z, Scalar μ,
382 int& iterations) {
383 // Feasibility restoration
384 //
385 // min ρ Σ (pₑ + nₑ + pᵢ + nᵢ) + ζ/2 (x - xᵣ)ᵀDᵣ(x - xᵣ)
386 // x
387 // pₑ,nₑ
388 // pᵢ,nᵢ
389 //
390 // s.t. cₑ(x) - pₑ + nₑ = 0
391 // cᵢ(x) - pᵢ + nᵢ ≥ 0
392 // pₑ ≥ 0
393 // nₑ ≥ 0
394 // pᵢ ≥ 0
395 // nᵢ ≥ 0
396 //
397 // where ρ = 1000, ζ = √μ where μ is the barrier parameter, xᵣ is original
398 // iterate before feasibility restoration, and Dᵣ is a scaling matrix defined
399 // by
400 //
401 // Dᵣ = diag(min(1, 1/xᵣ[i]²) for i in x.rows())
402
403 using DenseVector = Eigen::VectorX<Scalar>;
404 using DiagonalMatrix = Eigen::DiagonalMatrix<Scalar, Eigen::Dynamic>;
405 using SparseMatrix = Eigen::SparseMatrix<Scalar>;
406 using SparseVector = Eigen::SparseVector<Scalar>;
407
408 using std::sqrt;
409
410 const auto& matrices = matrix_callbacks;
411 const auto& num_vars = matrices.num_decision_variables;
412 const auto& num_eq = matrices.num_equality_constraints;
413 const auto& num_ineq = matrices.num_inequality_constraints;
414
415 constexpr Scalar ρ(1e3);
416
417 const DenseVector c_e = matrices.c_e(x);
418 const DenseVector c_i = matrices.c_i(x);
419
420 Scalar fr_μ = std::max({μ, c_e.template lpNorm<Eigen::Infinity>(),
421 (c_i - s).template lpNorm<Eigen::Infinity>()});
422 const Scalar ζ = sqrt(fr_μ);
423
424 const auto& x_r = x;
425 const auto [p_e_0, n_e_0] = compute_p_n(c_e, ρ, fr_μ);
426 const auto [p_i_0, n_i_0] = compute_p_n((c_i - s).eval(), ρ, fr_μ);
427
428 // Dᵣ = diag(min(1, 1/xᵣ[i]²) for i in x.rows())
429 const DiagonalMatrix D_r =
430 x.cwiseSquare().cwiseInverse().cwiseMin(Scalar(1)).asDiagonal();
431
432 DenseVector fr_x{num_vars + 2 * num_eq + 2 * num_ineq};
433 fr_x << x, p_e_0, n_e_0, p_i_0, n_i_0;
434
435 // Use the original problem's slacks for cᵢ(x) − pᵢ + nᵢ ≥ 0 since
436 // cᵢ(x) − pᵢ + nᵢ = s by construction.
437 //
438 // Start the constraints pₑ, nₑ, pᵢ, nᵢ ≥ 0 with zero violation.
439 DenseVector fr_s{s.rows() + 2 * num_eq + 2 * num_ineq};
440 fr_s.segment(0, s.rows()) = s;
441 fr_s.segment(s.rows(), 2 * num_eq + 2 * num_ineq) << p_e_0, n_e_0, p_i_0,
442 n_i_0;
443
444 DenseVector fr_y = DenseVector::Zero(c_e.rows());
445
446 // Start the duals with perfect complementarity with respect to the slacks
447 DenseVector fr_z{c_i.rows() + 2 * num_eq + 2 * num_ineq};
448 fr_z << fr_μ * s.cwiseInverse(), fr_μ * p_e_0.cwiseInverse(),
449 fr_μ * n_e_0.cwiseInverse(), fr_μ * p_i_0.cwiseInverse(),
450 fr_μ * n_i_0.cwiseInverse();
451
452 // Inherit the parent problem's scaling for the constraints, and use no
453 // scaling for the cost function since it has changed. The new rows introduced
454 // are not scaled.
455 DenseVector fr_d_c_i{c_i.rows() + 2 * num_eq + 2 * num_ineq};
456 fr_d_c_i << matrices.scaling.c_i,
457 DenseVector::Ones(2 * num_eq + 2 * num_ineq);
458 const ProblemScaling<Scalar> fr_scaling{Scalar(1), matrices.scaling.c_e,
459 fr_d_c_i};
460
461 IPMMatrixCallbacks<Scalar> fr_matrix_callbacks{
462 static_cast<int>(fr_x.rows()),
463 static_cast<int>(fr_y.rows()),
464 static_cast<int>(fr_z.rows()),
465 [&](const DenseVector& x_p) -> Scalar {
466 auto x = x_p.segment(0, num_vars);
467
468 // Cost function
469 //
470 // ρ Σ (pₑ + nₑ + pᵢ + nᵢ) + ζ/2 (x - xᵣ)ᵀDᵣ(x - xᵣ)
471 auto diff = x - x_r;
472 return ρ * x_p.segment(num_vars, 2 * num_eq + 2 * num_ineq)
473 .array()
474 .sum() +
475 ζ / Scalar(2) * diff.transpose() * D_r * diff;
476 },
477 [&](const DenseVector& x_p) -> SparseVector {
478 auto x = x_p.segment(0, num_vars);
479
480 // Cost function gradient
481 //
482 // [ζDᵣ(x − xᵣ)]
483 // [ ρ ]
484 // [ ρ ]
485 // [ ρ ]
486 // [ ρ ]
487 DenseVector g{x_p.rows()};
488 g.segment(0, num_vars) = ζ * D_r * (x - x_r);
489 g.segment(num_vars, 2 * num_eq + 2 * num_ineq).setConstant(ρ);
490 return g.sparseView();
491 },
492 [&](const DenseVector& x_p, const DenseVector& y_p,
493 const DenseVector& z_p) -> SparseMatrix {
494 auto x = x_p.segment(0, num_vars);
495 const auto& y = y_p;
496 auto z = z_p.segment(0, num_ineq);
497
498 // Cost function Hessian
499 //
500 // [ζDᵣ 0 0 0 0]
501 // [ 0 0 0 0 0]
502 // [ 0 0 0 0 0]
503 // [ 0 0 0 0 0]
504 // [ 0 0 0 0 0]
505 gch::small_vector<Eigen::Triplet<Scalar>> triplets;
506 triplets.reserve(x_p.rows());
507 append_as_triplets(triplets, 0, 0, {SparseMatrix{ζ * D_r}});
508 SparseMatrix d2f_dx2{x_p.rows(), x_p.rows()};
509 d2f_dx2.setFromSortedTriplets(triplets.begin(), triplets.end());
510
511 // Constraint part of original problem's Lagrangian Hessian
512 //
513 // −∇ₓₓ²yᵀcₑ(x) − ∇ₓₓ²zᵀcᵢ(x)
514 auto H_c = matrices.H_c(x, y, z);
515 H_c.conservativeResize(x_p.rows(), x_p.rows());
516
517 // Lagrangian Hessian
518 //
519 // [ζDᵣ 0 0 0 0]
520 // [ 0 0 0 0 0]
521 // [ 0 0 0 0 0] − ∇ₓₓ²yᵀcₑ(x) − ∇ₓₓ²zᵀcᵢ(x)
522 // [ 0 0 0 0 0]
523 // [ 0 0 0 0 0]
524 return d2f_dx2 + H_c;
525 },
526 [&](const DenseVector& x_p, [[maybe_unused]] const DenseVector& y_p,
527 [[maybe_unused]] const DenseVector& z_p) -> SparseMatrix {
528 return SparseMatrix{x_p.rows(), x_p.rows()};
529 },
530 [&](const DenseVector& x_p) -> DenseVector {
531 auto x = x_p.segment(0, num_vars);
532 auto p_e = x_p.segment(num_vars, num_eq);
533 auto n_e = x_p.segment(num_vars + num_eq, num_eq);
534
535 // Equality constraints
536 //
537 // cₑ(x) - pₑ + nₑ = 0
538 return matrices.c_e(x) - p_e + n_e;
539 },
540 [&](const DenseVector& x_p) -> SparseMatrix {
541 auto x = x_p.segment(0, num_vars);
542
543 // Equality constraint Jacobian
544 //
545 // [Aₑ −I I 0 0]
546
547 SparseMatrix A_e = matrices.A_e(x);
548
549 gch::small_vector<Eigen::Triplet<Scalar>> triplets;
550 triplets.reserve(A_e.nonZeros() + 2 * num_eq);
551
552 append_as_triplets(triplets, 0, 0, {A_e});
553 append_diagonal_as_triplets(
554 triplets, 0, num_vars,
555 DenseVector::Constant(num_eq, Scalar(-1)).eval());
556 append_diagonal_as_triplets(
557 triplets, 0, num_vars + num_eq,
558 DenseVector::Constant(num_eq, Scalar(1)).eval());
559
560 SparseMatrix A_e_p{A_e.rows(), x_p.rows()};
561 A_e_p.setFromSortedTriplets(triplets.begin(), triplets.end());
562 return A_e_p;
563 },
564 [&](const DenseVector& x_p) -> DenseVector {
565 auto x = x_p.segment(0, num_vars);
566 auto p_i = x_p.segment(num_vars + 2 * num_eq, num_ineq);
567 auto n_i = x_p.segment(num_vars + 2 * num_eq + num_ineq, num_ineq);
568
569 // Inequality constraints
570 //
571 // cᵢ(x) - pᵢ + nᵢ ≥ 0
572 // pₑ ≥ 0
573 // nₑ ≥ 0
574 // pᵢ ≥ 0
575 // nᵢ ≥ 0
576 DenseVector c_i_p{c_i.rows() + 2 * num_eq + 2 * num_ineq};
577 c_i_p.segment(0, num_ineq) = matrices.c_i(x) - p_i + n_i;
578 c_i_p.segment(p_i.rows(), 2 * num_eq + 2 * num_ineq) =
579 x_p.segment(num_vars, 2 * num_eq + 2 * num_ineq);
580 return c_i_p;
581 },
582 [&](const DenseVector& x_p) -> SparseMatrix {
583 auto x = x_p.segment(0, num_vars);
584
585 // Inequality constraint Jacobian
586 //
587 // [Aᵢ 0 0 −I I]
588 // [0 I 0 0 0]
589 // [0 0 I 0 0]
590 // [0 0 0 I 0]
591 // [0 0 0 0 I]
592
593 SparseMatrix A_i = matrices.A_i(x);
594
595 gch::small_vector<Eigen::Triplet<Scalar>> triplets;
596 triplets.reserve(A_i.nonZeros() + 2 * num_eq + 4 * num_ineq);
597
598 // Column 0
599 append_as_triplets(triplets, 0, 0, {A_i});
600
601 // Columns 1 and 2
602 append_diagonal_as_triplets(
603 triplets, num_ineq, num_vars,
604 DenseVector::Constant(2 * num_eq, Scalar(1)).eval());
605
606 SparseMatrix I_ineq{
607 DenseVector::Constant(num_ineq, Scalar(1)).asDiagonal()};
608
609 // Column 3
610 SparseMatrix Z_col3{2 * num_eq, num_ineq};
611 append_as_triplets(triplets, 0, num_vars + 2 * num_eq,
612 {(-I_ineq).eval(), Z_col3, I_ineq});
613
614 // Column 4
615 SparseMatrix Z_col4{2 * num_eq + num_ineq, num_ineq};
616 append_as_triplets(triplets, 0, num_vars + 2 * num_eq + num_ineq,
617 {I_ineq, Z_col4, I_ineq});
618
619 SparseMatrix A_i_p{2 * num_eq + 3 * num_ineq, x_p.rows()};
620 A_i_p.setFromSortedTriplets(triplets.begin(), triplets.end());
621 return A_i_p;
622 },
623 fr_scaling};
624
625#ifdef SLEIPNIR_ENABLE_BOUND_PROJECTION
626 Eigen::ArrayX<bool> fr_bound_constraint_mask{2 * num_eq + 3 * num_ineq};
627 fr_bound_constraint_mask.segment(0, num_ineq) = bound_constraint_mask;
628 fr_bound_constraint_mask.segment(num_ineq, 2 * num_eq + 2 * num_ineq) = true;
629#endif
630
631 auto status =
632 ipm<Scalar>(fr_matrix_callbacks, iteration_callbacks, options, true,
633#ifdef SLEIPNIR_ENABLE_BOUND_PROJECTION
634 fr_bound_constraint_mask,
635#endif
636 fr_x, fr_s, fr_y, fr_z, fr_μ, iterations);
637
638 x = fr_x.segment(0, x.rows());
639
640 // If feasibility restoration converged, check the constraint violation in the
641 // original problem's scaling
642 DenseVector c_e_unscaled;
643 DenseVector c_i_unscaled;
644 bool c_e_violated = false;
645 bool c_i_violated = false;
646 if (status == ExitStatus::SUCCESS) {
647 c_e_unscaled = matrices.c_e(x);
648 if (matrices.scaling.c_e.size() > 0) {
649 c_e_unscaled =
650 matrices.scaling.c_e.cwiseInverse().cwiseProduct(c_e_unscaled);
651 }
652
653 c_i_unscaled = matrices.c_i(x);
654 if (matrices.scaling.c_i.size() > 0) {
655 c_i_unscaled =
656 matrices.scaling.c_i.cwiseInverse().cwiseProduct(c_i_unscaled);
657 }
658
659 // Inequality constraints cᵢ(x) ≥ 0 are only violated where they're
660 // negative
661 c_e_violated =
662 c_e_unscaled.template lpNorm<Eigen::Infinity>() > options.tolerance;
663 c_i_violated =
664 (-c_i_unscaled).cwiseMax(Scalar(0)).template lpNorm<Eigen::Infinity>() >
665 options.tolerance;
666 }
667
668 // Return to the normal phase if feasibility restoration stopped early, or if
669 // it converged to a feasible point (e.g., because it was run to convergence)
670 if (status == ExitStatus::CALLBACK_REQUESTED_STOP ||
671 (status == ExitStatus::SUCCESS && !c_e_violated && !c_i_violated)) {
672 const DenseVector s_old = s;
673
674 // Re-center the slacks on the original constraints cᵢ(x) if they're
675 // sufficiently feasible, which zeroes their constraint violation.
676 // Otherwise, keep feasibility restoration's slacks.
677 //
678 // Note that those slacks can make Σ = S⁻¹Z and μS⁻¹e very large because
679 // fr_μ drives them toward 0 independently of μ, so they can end up much
680 // smaller than μ. We can't fix that by raising the slacks because the added
681 // constraint violation would make the filter reject more returned iterates.
682 //
683 // The normal phase's early-exit callback computes the slacks the same way,
684 // so the returned iterate is the one the filter accepted.
685 const DenseVector c_i = matrices.c_i(x);
686 s = (c_i.array() >= Scalar(1e-2)).select(c_i, fr_s.segment(0, s.rows()));
687
688 // Reset the equality constraint duals since feasibility restoration didn't
689 // account for the cost function
690 y.setZero();
691
692 // Update the inequality constraint duals with a Newton step for
693 // complementarity, taking the change in slacks during the restoration
694 // phase as the primal step.
695 //
696 // pᶻ = μS⁻¹e − z − Σpˢ
697 //
698 // If the result isn't strictly positive or is too large, reset all of them
699 // to 1.
700 const DenseVector Σ = z.cwiseQuotient(s_old);
701 const DenseVector p_s = s - s_old;
702 z += μ * s_old.cwiseInverse() - z - Σ.cwiseProduct(p_s);
703 if ((z.array() <= Scalar(0)).any() || (z.array() > Scalar(1e3)).any()) {
704 z.setOnes();
705 }
706
707 return ExitStatus::SUCCESS;
708 } else if (status == ExitStatus::SUCCESS) {
709 // Feasibility restoration converged to a minimizer of the constraint
710 // violation, and the constraint violation is still above the tolerance, so
711 // that minimizer is a certificate of local infeasibility. Declaring local
712 // infeasibility anywhere else risks false positives (e.g., a
713 // point-in-time test can reject iterates the solver would otherwise
714 // escape). See section 3.3, p. 14 of [2].
715
716 s = fr_s.segment(0, s.rows());
717
718 if (options.diagnostics) {
719 if (c_e_violated) {
720 print_c_e_local_infeasibility_error(c_e_unscaled, options.tolerance);
721 }
722 if (c_i_violated) {
723 print_c_i_local_infeasibility_error(c_i_unscaled, options.tolerance);
724 }
725 }
726
727 return ExitStatus::LOCALLY_INFEASIBLE;
728 } else {
729 s = fr_s.segment(0, s.rows());
730 return ExitStatus::FEASIBILITY_RESTORATION_FAILED;
731 }
732}
733
734} // namespace slp
735
736#include "sleipnir/optimization/solver/ipm.hpp"