Sleipnir C++ API
Loading...
Searching...
No Matches
ipm.hpp
1// Copyright (c) Sleipnir contributors
2
3#pragma once
4
5#include <algorithm>
6#include <chrono>
7#include <cmath>
8#include <functional>
9#include <span>
10
11#include <Eigen/Core>
12#include <Eigen/SparseCore>
13#include <gch/small_vector.hpp>
14
15#include "sleipnir/optimization/solver/exit_status.hpp"
16#include "sleipnir/optimization/solver/ipm_matrix_callbacks.hpp"
17#include "sleipnir/optimization/solver/iteration_info.hpp"
18#include "sleipnir/optimization/solver/options.hpp"
19#include "sleipnir/optimization/solver/util/all_finite.hpp"
20#include "sleipnir/optimization/solver/util/append_as_triplets.hpp"
21#include "sleipnir/optimization/solver/util/feasibility_restoration.hpp"
22#include "sleipnir/optimization/solver/util/filter.hpp"
23#include "sleipnir/optimization/solver/util/fraction_to_the_boundary_rule.hpp"
24#include "sleipnir/optimization/solver/util/kkt_error.hpp"
25#include "sleipnir/optimization/solver/util/kkt_solver.hpp"
26#include "sleipnir/util/assert.hpp"
27#include "sleipnir/util/print_diagnostics.hpp"
28#include "sleipnir/util/profiler.hpp"
29#include "sleipnir/util/scope_exit.hpp"
30#include "sleipnir/util/symbol_exports.hpp"
31
32// See docs/algorithms.md#Works_cited for citation definitions.
33//
34// See docs/algorithms.md#Interior-point_method for a derivation of the
35// interior-point method formulation being used.
36
37namespace slp {
38
61template <typename Scalar>
62ExitStatus ipm(const IPMMatrixCallbacks<Scalar>& matrix_callbacks,
63 std::span<std::function<bool(const IterationInfo<Scalar>& info)>>
64 iteration_callbacks,
65 const Options& options,
66#ifdef SLEIPNIR_ENABLE_BOUND_PROJECTION
67 const Eigen::ArrayX<bool>& bound_constraint_mask,
68#endif
69 Eigen::Vector<Scalar, Eigen::Dynamic>& x) {
70 using DenseVector = Eigen::Vector<Scalar, Eigen::Dynamic>;
71
72 DenseVector s =
73 DenseVector::Ones(matrix_callbacks.num_inequality_constraints);
74 DenseVector y = DenseVector::Zero(matrix_callbacks.num_equality_constraints);
75 DenseVector z =
76 DenseVector::Ones(matrix_callbacks.num_inequality_constraints);
77 Scalar μ = Scalar(0.1) * matrix_callbacks.scaling.f;
78 int iterations = 0;
79
80 return ipm(matrix_callbacks, iteration_callbacks, options, false,
81#ifdef SLEIPNIR_ENABLE_BOUND_PROJECTION
82 bound_constraint_mask,
83#endif
84 x, s, y, z, μ, iterations);
85}
86
120template <typename Scalar>
121ExitStatus ipm(const IPMMatrixCallbacks<Scalar>& matrix_callbacks,
122 std::span<std::function<bool(const IterationInfo<Scalar>& info)>>
123 iteration_callbacks,
124 const Options& options, bool in_feasibility_restoration,
125#ifdef SLEIPNIR_ENABLE_BOUND_PROJECTION
126 const Eigen::ArrayX<bool>& bound_constraint_mask,
127#endif
128 Eigen::Vector<Scalar, Eigen::Dynamic>& x,
129 Eigen::Vector<Scalar, Eigen::Dynamic>& s,
130 Eigen::Vector<Scalar, Eigen::Dynamic>& y,
131 Eigen::Vector<Scalar, Eigen::Dynamic>& z, Scalar& μ,
132 int& iterations) {
133 using DenseVector = Eigen::Vector<Scalar, Eigen::Dynamic>;
134 using SparseMatrix = Eigen::SparseMatrix<Scalar>;
135 using SparseVector = Eigen::SparseVector<Scalar>;
136
138 struct Step {
140 DenseVector p_x;
142 DenseVector p_s;
144 DenseVector p_y;
146 DenseVector p_z;
147 };
148
149 using std::isfinite;
150
151 const auto solve_start_time = std::chrono::steady_clock::now();
152
153 gch::small_vector<SolveProfiler> solve_profilers;
154 solve_profilers.emplace_back("solver");
155 solve_profilers.emplace_back("↳ setup");
156 solve_profilers.emplace_back("↳ iteration");
157 solve_profilers.emplace_back(" ↳ callbacks");
158 solve_profilers.emplace_back(" ↳ KKT matrix build");
159 solve_profilers.emplace_back(" ↳ KKT matrix decomp");
160 solve_profilers.emplace_back(" ↳ KKT system solve");
161 solve_profilers.emplace_back(" ↳ line search");
162 solve_profilers.emplace_back(" ↳ SOC");
163 solve_profilers.emplace_back(" ↳ feas. restoration");
164 solve_profilers.emplace_back(" ↳ f(x)");
165 solve_profilers.emplace_back(" ↳ ∇f(x)");
166 solve_profilers.emplace_back(" ↳ ∇²ₓₓL");
167 solve_profilers.emplace_back(" ↳ ∇²ₓₓL_c");
168 solve_profilers.emplace_back(" ↳ cₑ(x)");
169 solve_profilers.emplace_back(" ↳ ∂cₑ/∂x");
170 solve_profilers.emplace_back(" ↳ cᵢ(x)");
171 solve_profilers.emplace_back(" ↳ ∂cᵢ/∂x");
172
173 auto& solver_prof = solve_profilers[0];
174 auto& setup_prof = solve_profilers[1];
175 auto& inner_iter_prof = solve_profilers[2];
176 auto& iter_callbacks_prof = solve_profilers[3];
177 auto& kkt_matrix_build_prof = solve_profilers[4];
178 auto& kkt_matrix_decomp_prof = solve_profilers[5];
179 auto& kkt_system_solve_prof = solve_profilers[6];
180 auto& line_search_prof = solve_profilers[7];
181 auto& soc_prof = solve_profilers[8];
182 auto& feasibility_restoration_prof = solve_profilers[9];
183
184 // Set up profiled matrix callbacks
185#ifndef SLEIPNIR_DISABLE_DIAGNOSTICS
186 auto& f_prof = solve_profilers[10];
187 auto& g_prof = solve_profilers[11];
188 auto& H_prof = solve_profilers[12];
189 auto& H_c_prof = solve_profilers[13];
190 auto& c_e_prof = solve_profilers[14];
191 auto& A_e_prof = solve_profilers[15];
192 auto& c_i_prof = solve_profilers[16];
193 auto& A_i_prof = solve_profilers[17];
194
195 IPMMatrixCallbacks<Scalar> matrices{
196 matrix_callbacks.num_decision_variables,
197 matrix_callbacks.num_equality_constraints,
198 matrix_callbacks.num_inequality_constraints,
199 [&](const DenseVector& x) -> Scalar {
200 ScopedProfiler prof{f_prof};
201 return matrix_callbacks.f(x);
202 },
203 [&](const DenseVector& x) -> SparseVector {
204 ScopedProfiler prof{g_prof};
205 return matrix_callbacks.g(x);
206 },
207 [&](const DenseVector& x, const DenseVector& y,
208 const DenseVector& z) -> SparseMatrix {
209 ScopedProfiler prof{H_prof};
210 return matrix_callbacks.H(x, y, z);
211 },
212 [&](const DenseVector& x, const DenseVector& y,
213 const DenseVector& z) -> SparseMatrix {
214 ScopedProfiler prof{H_c_prof};
215 return matrix_callbacks.H_c(x, y, z);
216 },
217 [&](const DenseVector& x) -> DenseVector {
218 ScopedProfiler prof{c_e_prof};
219 return matrix_callbacks.c_e(x);
220 },
221 [&](const DenseVector& x) -> SparseMatrix {
222 ScopedProfiler prof{A_e_prof};
223 return matrix_callbacks.A_e(x);
224 },
225 [&](const DenseVector& x) -> DenseVector {
226 ScopedProfiler prof{c_i_prof};
227 return matrix_callbacks.c_i(x);
228 },
229 [&](const DenseVector& x) -> SparseMatrix {
230 ScopedProfiler prof{A_i_prof};
231 return matrix_callbacks.A_i(x);
232 },
233 matrix_callbacks.scaling};
234#else
235 const auto& matrices = matrix_callbacks;
236#endif
237
238 solver_prof.start();
239 setup_prof.start();
240
241 Scalar f = matrices.f(x);
242 SparseVector g = matrices.g(x);
243 SparseMatrix H = matrices.H(x, y, z);
244 DenseVector c_e = matrices.c_e(x);
245 SparseMatrix A_e = matrices.A_e(x);
246 DenseVector c_i = matrices.c_i(x);
247 SparseMatrix A_i = matrices.A_i(x);
248
249 // Ensure matrix callback dimensions are consistent
250 slp_assert(g.rows() == matrices.num_decision_variables);
251 slp_assert(H.rows() == matrices.num_decision_variables);
252 slp_assert(H.cols() == matrices.num_decision_variables);
253 slp_assert(c_e.rows() == matrices.num_equality_constraints);
254 slp_assert(A_e.rows() == matrices.num_equality_constraints);
255 slp_assert(A_e.cols() == matrices.num_decision_variables);
256 slp_assert(c_i.rows() == matrices.num_inequality_constraints);
257 slp_assert(A_i.rows() == matrices.num_inequality_constraints);
258 slp_assert(A_i.cols() == matrices.num_decision_variables);
259
260 DenseVector trial_x;
261 DenseVector trial_s;
262 DenseVector trial_y;
263 DenseVector trial_z;
264
265 Scalar trial_f;
266 DenseVector trial_c_e;
267 DenseVector trial_c_i;
268
269 // Check for overconstrained problem
270 if (matrices.num_equality_constraints > matrices.num_decision_variables) {
271 if (options.diagnostics) {
272 print_too_few_dofs_error(c_e);
273 }
274
275 return ExitStatus::TOO_FEW_DOFS;
276 }
277
278 // Check whether initial guess has finite cost, constraints, and derivatives
279 if (!isfinite(f) || !all_finite(g) || !all_finite(H) || !c_e.allFinite() ||
280 !all_finite(A_e) || !c_i.allFinite() || !all_finite(A_i)) {
281 return ExitStatus::NONFINITE_INITIAL_GUESS;
282 }
283
284#ifdef SLEIPNIR_ENABLE_BOUND_PROJECTION
285 // We set sʲ = cᵢʲ(x) for each bound inequality constraint index j
286 s = bound_constraint_mask.select(c_i, s);
287#endif
288
289 // Barrier parameter minimum
290 const Scalar μ_min =
291 matrices.scaling.f * Scalar(options.tolerance) / Scalar(10);
292
293 // Fraction-to-the-boundary rule scale factor minimum
294 constexpr Scalar τ_min(0.99);
295
296 // Fraction-to-the-boundary rule scale factor τ
297 Scalar τ = τ_min;
298
299 Filter<Scalar> filter{c_e.template lpNorm<1>() +
300 (c_i - s).template lpNorm<1>()};
301
302 // This should be run when the error is below a desired threshold for the
303 // current barrier parameter
304 auto update_barrier_parameter_and_reset_filter = [&] {
305 // Barrier parameter linear decrease power in "κ_μ μ". Range of (0, 1).
306 constexpr Scalar κ_μ(0.2);
307
308 // Barrier parameter superlinear decrease power in "μ^(θ_μ)". Range of (1,
309 // 2).
310 constexpr Scalar θ_μ(1.5);
311
312 // Update the barrier parameter.
313 //
314 // μⱼ₊₁ = max(εₜₒₗ/10, min(κ_μ μⱼ, μⱼ^θ_μ))
315 //
316 // See equation (7) of [2].
317 using std::pow;
318 μ = std::max(μ_min, std::min(κ_μ * μ, pow(μ, θ_μ)));
319
320 // Update the fraction-to-the-boundary rule scaling factor.
321 //
322 // τⱼ = max(τₘᵢₙ, 1 − μⱼ)
323 //
324 // See equation (8) of [2].
325 τ = std::max(τ_min, Scalar(1) - μ);
326
327 // Reset the filter when the barrier parameter is updated
328 filter.reset();
329 };
330
331 // Kept outside the loop so its storage can be reused
332 gch::small_vector<Eigen::Triplet<Scalar>> triplets;
333
334 const int lhs_rows =
335 matrices.num_decision_variables + matrices.num_equality_constraints;
336 KKTSolver<Scalar> solver{
337 // Use sparse solver if lower triangle fills < 25% of system
338 H.nonZeros() +
339 (A_i.transpose() * A_i)
340 .template triangularView<Eigen::Lower>()
341 .eval()
342 .nonZeros() +
343 A_e.nonZeros() <
344 0.25 * lhs_rows * lhs_rows,
345 matrices.num_decision_variables, matrices.num_equality_constraints,
346 // Constraint regularization is forced to zero in feasibility restoration
347 // because the equality constraint Jacobian cannot be rank-deficient
348 in_feasibility_restoration ? Scalar(0) : Scalar(1e-10)};
349
350 // Variables for determining when a step is acceptable
351 constexpr Scalar α_reduction_factor(0.5);
352 constexpr Scalar α_min(1e-7);
353
354 int full_step_rejected_counter = 0;
355
356 StallDetector<Scalar> stall_detector;
357
358 // Error
359 Scalar E_0 = unscaled_kkt_error<Scalar, KKTErrorType::INF_NORM_SCALED>(
360 matrices.scaling, g, A_e, c_e, A_i, c_i, s, y, z, Scalar(0));
361
362 setup_prof.stop();
363
364 // Prints final solver diagnostics when the solver exits
365 scope_exit exit{[&] {
366 if (options.diagnostics) {
367 solver_prof.stop();
368
369 if (in_feasibility_restoration) {
370 return;
371 }
372
373 if (iterations > 0) {
374 print_bottom_iteration_diagnostics();
375 }
376 print_solver_diagnostics(solve_profilers);
377 }
378 }};
379
380 // Print initial iterate diagnostics
381 if (options.diagnostics) {
382 print_initial_iterate_diagnostics(
383 E_0, f, c_e.template lpNorm<1>() + (c_i - s).template lpNorm<1>(),
384 s.dot(z), μ);
385 }
386
387 while (E_0 > Scalar(options.tolerance)) {
388 ScopedProfiler inner_iter_profiler{inner_iter_prof};
389
390 // Check for diverging iterates
391 if (x.template lpNorm<Eigen::Infinity>() > Scalar(1e10) || !x.allFinite() ||
392 s.template lpNorm<Eigen::Infinity>() > Scalar(1e10) || !s.allFinite()) {
393 return ExitStatus::DIVERGING_ITERATES;
394 }
395
396 ScopedProfiler iter_callbacks_profiler{iter_callbacks_prof};
397
398 // Call iteration callbacks
399 for (const auto& callback : iteration_callbacks) {
400 if (callback({iterations, x, s, y, z, g, H, A_e, A_i})) {
401 return ExitStatus::CALLBACK_REQUESTED_STOP;
402 }
403 }
404
405 iter_callbacks_profiler.stop();
406 ScopedProfiler kkt_matrix_build_profiler{kkt_matrix_build_prof};
407
408 // S = diag(s)
409 // Z = diag(z)
410 // Σ = S⁻¹Z
411 const SparseMatrix Σ{s.cwiseInverse().asDiagonal() * z.asDiagonal()};
412
413 // lhs = [H + AᵢᵀΣAᵢ Aₑᵀ]
414 // [ Aₑ 0 ]
415 //
416 // Don't assign upper triangle because solver only uses lower triangle.
417 const SparseMatrix top_left =
418 H + (A_i.transpose() * Σ * A_i).template triangularView<Eigen::Lower>();
419 triplets.clear();
420 triplets.reserve(top_left.nonZeros() + A_e.nonZeros());
421 append_as_triplets(triplets, 0, 0, {top_left, A_e});
422 SparseMatrix lhs(
423 matrices.num_decision_variables + matrices.num_equality_constraints,
424 matrices.num_decision_variables + matrices.num_equality_constraints);
425 lhs.setFromSortedTriplets(triplets.begin(), triplets.end());
426
427 // rhs = −[∇f − Aₑᵀy − Aᵢᵀ(−Σcᵢ + μS⁻¹e + z)]
428 // [ cₑ ]
429 DenseVector rhs{x.rows() + y.rows()};
430 rhs.segment(0, x.rows()) =
431 -g + A_e.transpose() * y +
432 A_i.transpose() * (-Σ * c_i + μ * s.cwiseInverse() + z);
433 rhs.segment(x.rows(), y.rows()) = -c_e;
434
435 kkt_matrix_build_profiler.stop();
436 ScopedProfiler kkt_matrix_decomp_profiler{kkt_matrix_decomp_prof};
437
438 Step step;
439 Scalar α_max(1);
440 Scalar α(1);
441 Scalar α_z(1);
442 bool call_feasibility_restoration = false;
443
444 // Solve the Newton-KKT system
445 //
446 // [H + AᵢᵀΣAᵢ Aₑᵀ][ pˣ] = −[∇f − Aₑᵀy − Aᵢᵀ(−Σcᵢ + μS⁻¹e + z)]
447 // [ Aₑ 0 ][−pʸ] [ cₑ ]
448 if (solver.compute(lhs).info() != Eigen::Success) [[unlikely]] {
449 return ExitStatus::FACTORIZATION_FAILED;
450 }
451
452 kkt_matrix_decomp_profiler.stop();
453 ScopedProfiler kkt_system_solve_profiler{kkt_system_solve_prof};
454
455 auto compute_step = [&](Step& step, const DenseVector& c_i_minus_s) {
456 // p = [ pˣ]
457 // [−pʸ]
458 DenseVector p = solver.solve(rhs);
459 step.p_x = p.segment(0, x.rows());
460 step.p_y = -p.segment(x.rows(), y.rows());
461
462 // pˢ = cᵢ − s + Aᵢpˣ
463 // pᶻ = μS⁻¹e − z − Σpˢ
464 step.p_s = c_i_minus_s + A_i * step.p_x;
465 step.p_z = μ * s.cwiseInverse() - z - Σ * step.p_s;
466 };
467 compute_step(step, c_i - s);
468
469 kkt_system_solve_profiler.stop();
470 ScopedProfiler line_search_profiler{line_search_prof};
471
472 // αᵐᵃˣ = max(α ∈ (0, 1] : sₖ + αpₖˢ ≥ (1−τⱼ)sₖ)
473 α_max = fraction_to_the_boundary_rule<Scalar>(s, step.p_s, τ);
474 α = α_max;
475
476 // If maximum step size is below minimum, invoke feasibility restoration
477 if (α < α_min) {
478 call_feasibility_restoration = true;
479 }
480
481 // αₖᶻ = max(α ∈ (0, 1] : zₖ + αpₖᶻ ≥ (1−τⱼ)zₖ)
482 α_z = fraction_to_the_boundary_rule<Scalar>(z, step.p_z, τ);
483
484 const FilterEntry<Scalar> current_entry{f, s, c_e, c_i, μ};
485
486 // Compute the directional derivative of the log-barrier function along the
487 // search direction.
488 //
489 // ϕ_μ(x, s) = f(x) − μ∑ᵢ ln(sᵢ)
490 //
491 // D_ϕ = ∇ϕ_μ(x, s)ᵀ[pˣ pˢ]
492 // = ∇f(x)ᵀpˣ − μ∑ᵢ pᵢˢ/sᵢ
493 const Scalar D_ϕ =
494 g.transpose() * step.p_x - μ * s.cwiseInverse().dot(step.p_s);
495
496 // Loop until a step is accepted
497 while (1) {
498 trial_x = x + α * step.p_x;
499 trial_c_i = matrices.c_i(trial_x);
500 if (options.feasible_ipm && c_i.cwiseGreater(Scalar(0)).all()) {
501 // If the inequality constraints are all feasible, prevent them from
502 // becoming infeasible again.
503 //
504 // See equation (19.30) of [1].
505 trial_s = trial_c_i;
506 } else {
507 trial_s = s + α * step.p_s;
508 }
509 trial_y = y + α * step.p_y;
510 trial_z = z + α_z * step.p_z;
511
512 trial_f = matrices.f(trial_x);
513 trial_c_e = matrices.c_e(trial_x);
514
515 // If f(xₖ + αpₖˣ), cₑ(xₖ + αpₖˣ), or cᵢ(xₖ + αpₖˣ) aren't finite, reduce
516 // step size immediately
517 if (!isfinite(trial_f) || !trial_c_e.allFinite() ||
518 !trial_c_i.allFinite()) {
519 // Reduce step size
520 α *= α_reduction_factor;
521
522 if (α < α_min) {
523 call_feasibility_restoration = true;
524 break;
525 }
526 continue;
527 }
528
529 // Check whether filter accepts trial iterate
530 FilterEntry trial_entry{trial_f, trial_s, trial_c_e, trial_c_i, μ};
531 if (filter.try_add(current_entry, trial_entry, D_ϕ, α)) {
532 // Accept step
533 break;
534 }
535
536 Scalar prev_constraint_violation =
537 c_e.template lpNorm<1>() + (c_i - s).template lpNorm<1>();
538 Scalar next_constraint_violation =
539 trial_c_e.template lpNorm<1>() +
540 (trial_c_i - trial_s).template lpNorm<1>();
541
542 // Second-order corrections
543 //
544 // If first trial point was rejected and constraint violation stayed the
545 // same or went up, apply second-order corrections
546 if (α == α_max &&
547 next_constraint_violation >= prev_constraint_violation) {
548 // Apply second-order corrections. See section 2.4 of [2].
549 auto soc_step = step;
550
551 Scalar α_soc = α;
552 Scalar α_z_soc = α_z;
553 DenseVector c_e_soc = c_e;
554 DenseVector c_i_minus_s_soc = c_i - s;
555
556 Scalar soc_constraint_violation = next_constraint_violation;
557
558 bool step_acceptable = false;
559 for (int soc_iteration = 0; soc_iteration < 5 && !step_acceptable;
560 ++soc_iteration) {
561 ScopedProfiler soc_profiler{soc_prof};
562
563 scope_exit soc_exit{[&] {
564 soc_profiler.stop();
565
566 if (options.diagnostics && step_acceptable) {
567 print_iteration_diagnostics(
568 iterations, IterationType::SECOND_ORDER_CORRECTION,
569 soc_profiler.current_duration(),
570 unscaled_kkt_error<Scalar, KKTErrorType::INF_NORM_SCALED>(
571 matrices.scaling, g, A_e, trial_c_e, A_i, trial_c_i,
572 trial_s, trial_y, trial_z, Scalar(0)),
573 trial_f,
574 trial_c_e.template lpNorm<1>() +
575 (trial_c_i - trial_s).template lpNorm<1>(),
576 trial_s.dot(trial_z), μ, solver.hessian_regularization(),
577 solver.constraint_jacobian_regularization(),
578 std::max(soc_step.p_x.template lpNorm<Eigen::Infinity>(),
579 soc_step.p_s.template lpNorm<Eigen::Infinity>()),
580 std::max(soc_step.p_y.template lpNorm<Eigen::Infinity>(),
581 soc_step.p_z.template lpNorm<Eigen::Infinity>()),
582 α_soc, Scalar(1), α_reduction_factor, α_z_soc);
583 }
584 }};
585
586 // Rebuild Newton-KKT rhs with updated constraint values.
587 //
588 // rhs = −[∇f − Aₑᵀy − Aᵢᵀ(μS⁻¹e − Σ(cᵢ − s)ˢᵒᶜ)]
589 // [ cₑˢᵒᶜ ]
590 //
591 // where
592 //
593 // cₑˢᵒᶜ = αˢᵒᶜcₑ(xₖ) + cₑ(xₖ + αˢᵒᶜpˣˢᵒᶜ)
594 // (cᵢ − s)ˢᵒᶜ =
595 // αˢᵒᶜ(cᵢ(xₖ) − sₖ) + cᵢ(xₖ + αˢᵒᶜpˣˢᵒᶜ) − (sₖ + αˢᵒᶜpˢˢᵒᶜ)
596 c_e_soc = α_soc * c_e_soc + trial_c_e;
597 c_i_minus_s_soc = α_soc * c_i_minus_s_soc + trial_c_i - trial_s;
598 rhs.segment(0, x.rows()) =
599 -g + A_e.transpose() * y +
600 A_i.transpose() * (μ * s.cwiseInverse() - Σ * c_i_minus_s_soc);
601 rhs.segment(x.rows(), y.rows()) = -c_e_soc;
602
603 // Solve the Newton-KKT system
604 compute_step(soc_step, c_i_minus_s_soc);
605
606 // αˢᵒᶜ = max(α ∈ (0, 1] : sₖ + αpₖˢ ≥ (1−τⱼ)sₖ)
607 // αₖᶻˢᵒᶜ = max(α ∈ (0, 1] : zₖ + αpₖᶻ ≥ (1−τⱼ)zₖ)
608 α_soc = fraction_to_the_boundary_rule<Scalar>(s, soc_step.p_s, τ);
609 α_z_soc = fraction_to_the_boundary_rule<Scalar>(z, soc_step.p_z, τ);
610
611 trial_x = x + α_soc * soc_step.p_x;
612 trial_s = s + α_soc * soc_step.p_s;
613 trial_y = y + α_soc * soc_step.p_y;
614 trial_z = z + α_z_soc * soc_step.p_z;
615
616 trial_f = matrices.f(trial_x);
617 trial_c_e = matrices.c_e(trial_x);
618 trial_c_i = matrices.c_i(trial_x);
619
620 // Check whether filter accepts trial iterate
621 FilterEntry trial_entry{trial_f, trial_s, trial_c_e, trial_c_i, μ};
622 if (filter.try_add(current_entry, trial_entry, D_ϕ, α)) {
623 step = soc_step;
624 α = α_soc;
625 α_z = α_z_soc;
626 step_acceptable = true;
627 break;
628 }
629
630 // Constraint violation scale factor for second-order corrections
631 constexpr Scalar κ_soc(0.99);
632
633 // If constraint violation hasn't been sufficiently reduced, stop
634 // making second-order corrections
635 next_constraint_violation =
636 trial_c_e.template lpNorm<1>() +
637 (trial_c_i - trial_s).template lpNorm<1>();
638 if (next_constraint_violation > κ_soc * soc_constraint_violation) {
639 break;
640 }
641
642 soc_constraint_violation = next_constraint_violation;
643 }
644
645 if (step_acceptable) {
646 // Accept step
647 break;
648 }
649 }
650
651 // If we got here and α is the full step, the full step was rejected.
652 // Increment the full-step rejected counter to keep track of how many full
653 // steps have been rejected in a row.
654 if (α == α_max) {
655 ++full_step_rejected_counter;
656 }
657
658 // If the full step was rejected enough times in a row, reset the filter
659 // because it may be impeding progress.
660 //
661 // See section 3.2 case I of [2].
662 if (full_step_rejected_counter >= 4 &&
663 filter.max_constraint_violation >
664 current_entry.constraint_violation / Scalar(10) &&
665 filter.last_rejection_due_to_filter()) {
666 filter.max_constraint_violation *= Scalar(0.1);
667 filter.reset();
668 continue;
669 }
670
671 // Reduce step size
672 α *= α_reduction_factor;
673
674 // If step size hit a minimum, check if the KKT error was reduced. If it
675 // wasn't, invoke feasibility restoration.
676 if (α < α_min) {
677 Scalar current_kkt_error = kkt_error<Scalar, KKTErrorType::ONE_NORM>(
678 g, A_e, c_e, A_i, c_i, s, y, z, μ);
679
680 trial_x = x + α_max * step.p_x;
681 trial_s = s + α_max * step.p_s;
682 trial_y = y + α_max * step.p_y;
683 trial_z = z + α_z * step.p_z;
684
685 trial_f = matrices.f(trial_x);
686 trial_c_e = matrices.c_e(trial_x);
687 trial_c_i = matrices.c_i(trial_x);
688
689 Scalar next_kkt_error = kkt_error<Scalar, KKTErrorType::ONE_NORM>(
690 matrices.g(trial_x), matrices.A_e(trial_x), trial_c_e,
691 matrices.A_i(trial_x), trial_c_i, trial_s, trial_y, trial_z, μ);
692
693 // If the step using αᵐᵃˣ reduced the KKT error, accept it anyway
694 if (next_kkt_error <= Scalar(0.999) * current_kkt_error) {
695 // Accept step
696 α = α_max;
697 break;
698 }
699
700 call_feasibility_restoration = true;
701 break;
702 }
703 }
704
705 line_search_profiler.stop();
706
707 // If solver stalled, invoke feasibility restoration
708 if (!call_feasibility_restoration && !in_feasibility_restoration) {
709 FilterEntry trial_entry{trial_f, trial_s, trial_c_e, trial_c_i, μ};
710 if (stall_detector.update(current_entry, trial_entry, α,
711 filter.min_constraint_violation)) {
712 call_feasibility_restoration = true;
713 }
714 }
715
716 if (call_feasibility_restoration) {
717 ScopedProfiler feasibility_restoration_profiler{
718 feasibility_restoration_prof};
719
720 // If already in feasibility restoration mode, running it again won't help
721 if (in_feasibility_restoration) {
722 return ExitStatus::FEASIBILITY_RESTORATION_FAILED;
723 }
724
725 FilterEntry initial_entry{matrices.f(x), s, c_e, c_i, μ};
726
727 // Feasibility restoration phase
728 gch::small_vector<std::function<bool(const IterationInfo<Scalar>& info)>>
729 callbacks;
730 for (auto& callback : iteration_callbacks) {
731 callbacks.emplace_back(callback);
732 }
733 callbacks.emplace_back([&](const IterationInfo<Scalar>& info) {
734 DenseVector trial_x =
735 info.x.segment(0, matrices.num_decision_variables);
736 DenseVector trial_s =
737 info.s.segment(0, matrices.num_inequality_constraints);
738
739 DenseVector trial_c_e = matrices.c_e(trial_x);
740 DenseVector trial_c_i = matrices.c_i(trial_x);
741
742 // If the current iterate sufficiently reduces constraint violation and
743 // is accepted by the normal filter, stop feasibility restoration
744 FilterEntry trial_entry{matrices.f(trial_x), trial_s, trial_c_e,
745 trial_c_i, μ};
746 const Scalar D_ϕ_restoration = g.transpose() * (trial_x - x) -
747 μ * s.cwiseInverse().dot(trial_s - s);
748 return trial_entry.constraint_violation <
749 Scalar(0.9) * initial_entry.constraint_violation &&
750 filter.try_add(initial_entry, trial_entry, D_ϕ_restoration,
751 Scalar(1));
752 });
753 auto status =
754 feasibility_restoration<Scalar>(matrices, callbacks, options,
755#ifdef SLEIPNIR_ENABLE_BOUND_PROJECTION
756 bound_constraint_mask,
757#endif
758 x, s, y, z, μ, iterations);
759
760 if (status != ExitStatus::SUCCESS) {
761 // Report failure
762 return status;
763 }
764
765 f = matrices.f(x);
766 c_e = matrices.c_e(x);
767 c_i = matrices.c_i(x);
768 } else {
769 // If full step was accepted, reset full-step rejected counter
770 if (α == α_max) {
771 full_step_rejected_counter = 0;
772 }
773
774 // Update iterates
775 x = trial_x;
776 s = trial_s;
777 y = trial_y;
778 z = trial_z;
779
780 f = trial_f;
781 c_e = trial_c_e;
782 c_i = trial_c_i;
783 }
784
785 // A requirement for the convergence proof is that the primal-dual barrier
786 // term Hessian Σₖ₊₁ does not deviate arbitrarily much from the primal
787 // barrier term Hessian μSₖ₊₁⁻².
788 //
789 // Σₖ₊₁ = μSₖ₊₁⁻²
790 // Sₖ₊₁⁻¹Zₖ₊₁ = μSₖ₊₁⁻²
791 // Zₖ₊₁ = μSₖ₊₁⁻¹
792 //
793 // We ensure this by resetting
794 //
795 // zₖ₊₁ = clamp(zₖ₊₁, 1/κ_Σ μ/sₖ₊₁, κ_Σ μ/sₖ₊₁)
796 //
797 // for some fixed κ_Σ ≥ 1 after each step. See equation (16) of [2].
798 for (int row = 0; row < z.rows(); ++row) {
799 constexpr Scalar κ_Σ(1e10);
800 z[row] =
801 std::clamp(z[row], Scalar(1) / κ_Σ * μ / s[row], κ_Σ * μ / s[row]);
802 }
803
804 // Update autodiff for Jacobians and Hessian
805 A_e = matrices.A_e(x);
806 A_i = matrices.A_i(x);
807 g = matrices.g(x);
808 H = matrices.H(x, y, z);
809
810 // Update the error
811 E_0 = unscaled_kkt_error<Scalar, KKTErrorType::INF_NORM_SCALED>(
812 matrices.scaling, g, A_e, c_e, A_i, c_i, s, y, z, Scalar(0));
813
814 // Update the barrier parameter if necessary
815 if (E_0 > Scalar(options.tolerance)) {
816 // Barrier parameter scale factor for tolerance checks
817 constexpr Scalar κ_ε(10);
818
819 // While the error is below the desired threshold for this barrier
820 // parameter value, decrease the barrier parameter further
821 Scalar E_μ = kkt_error<Scalar, KKTErrorType::INF_NORM_SCALED>(
822 g, A_e, c_e, A_i, c_i, s, y, z, μ);
823 while (μ > μ_min && E_μ <= κ_ε * μ) {
824 update_barrier_parameter_and_reset_filter();
825 E_μ = kkt_error<Scalar, KKTErrorType::INF_NORM_SCALED>(g, A_e, c_e, A_i,
826 c_i, s, y, z, μ);
827 }
828 }
829
830 inner_iter_profiler.stop();
831
832 if (options.diagnostics) {
833 print_iteration_diagnostics(
834 iterations,
835 in_feasibility_restoration ? IterationType::FEASIBILITY_RESTORATION
836 : IterationType::NORMAL,
837 inner_iter_profiler.current_duration(), E_0, f,
838 c_e.template lpNorm<1>() + (c_i - s).template lpNorm<1>(), s.dot(z),
839 μ, solver.hessian_regularization(),
840 solver.constraint_jacobian_regularization(),
841 std::max(step.p_x.template lpNorm<Eigen::Infinity>(),
842 step.p_s.template lpNorm<Eigen::Infinity>()),
843 std::max(step.p_y.template lpNorm<Eigen::Infinity>(),
844 step.p_z.template lpNorm<Eigen::Infinity>()),
845 α, α_max, α_reduction_factor, α_z);
846 }
847
848 ++iterations;
849
850 // Check for max iterations
851 if (iterations >= options.max_iterations) {
852 return ExitStatus::MAX_ITERATIONS_EXCEEDED;
853 }
854
855 // Check for max wall clock time
856 if (std::chrono::steady_clock::now() - solve_start_time > options.timeout) {
857 return ExitStatus::TIMEOUT;
858 }
859 }
860
861 if (!isfinite(E_0)) {
862 return ExitStatus::DIVERGING_ITERATES;
863 } else {
864 return ExitStatus::SUCCESS;
865 }
866}
867
868extern template SLEIPNIR_DLLEXPORT ExitStatus
869ipm(const IPMMatrixCallbacks<double>& matrix_callbacks,
870 std::span<std::function<bool(const IterationInfo<double>& info)>>
871 iteration_callbacks,
872 const Options& options,
873#ifdef SLEIPNIR_ENABLE_BOUND_PROJECTION
874 const Eigen::ArrayX<bool>& bound_constraint_mask,
875#endif
876 Eigen::Vector<double, Eigen::Dynamic>& x);
877
878} // namespace slp