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 <limits>
10#include <optional>
11#include <span>
12
13#include <Eigen/Core>
14#include <Eigen/SparseCore>
15#include <gch/small_vector.hpp>
16
17#include "sleipnir/optimization/solver/exit_status.hpp"
18#include "sleipnir/optimization/solver/ipm_matrix_callbacks.hpp"
19#include "sleipnir/optimization/solver/iteration_info.hpp"
20#include "sleipnir/optimization/solver/options.hpp"
21#include "sleipnir/optimization/solver/util/all_finite.hpp"
22#include "sleipnir/optimization/solver/util/append_as_triplets.hpp"
23#include "sleipnir/optimization/solver/util/constraint_violation.hpp"
24#include "sleipnir/optimization/solver/util/feasibility_restoration.hpp"
25#include "sleipnir/optimization/solver/util/feasibility_restoration_cycle_detector.hpp"
26#include "sleipnir/optimization/solver/util/filter.hpp"
27#include "sleipnir/optimization/solver/util/fraction_to_the_boundary_rule.hpp"
28#include "sleipnir/optimization/solver/util/kkt_error.hpp"
29#include "sleipnir/optimization/solver/util/kkt_solver.hpp"
30#include "sleipnir/optimization/solver/util/time_trace.hpp"
31#include "sleipnir/util/assert.hpp"
32#include "sleipnir/util/print_diagnostics.hpp"
33#include "sleipnir/util/scope_exit.hpp"
34#include "sleipnir/util/symbol_exports.hpp"
35#include "sleipnir/util/time_tracer.hpp"
36
37// See docs/algorithms.md#Works_cited for citation definitions.
38//
39// See docs/algorithms.md#Interior-point_method for a derivation of the
40// interior-point method formulation being used.
41
42namespace slp {
43
66template <typename Scalar>
67ExitStatus ipm(const IPMMatrixCallbacks<Scalar>& matrix_callbacks,
68 std::span<std::function<bool(const IterationInfo<Scalar>& info)>>
69 iteration_callbacks,
70 const Options<Scalar>& options,
71#ifdef SLEIPNIR_ENABLE_BOUND_PROJECTION
72 const Eigen::ArrayX<bool>& bound_constraint_mask,
73#endif
74 Eigen::VectorX<Scalar>& x) {
75 using DenseVector = Eigen::VectorX<Scalar>;
76
77 // If cᵢ(x) is sufficiently feasible, start the constraint with zero
78 // violation. Otherwise, make Σ = S⁻¹Z ≈ I.
79 DenseVector c_i = matrix_callbacks.c_i(x);
80 DenseVector s = (c_i.array() >= Scalar(1e-2)).select(c_i, Scalar(1));
81
82 DenseVector y = DenseVector::Zero(matrix_callbacks.num_equality_constraints);
83 DenseVector z =
84 DenseVector::Ones(matrix_callbacks.num_inequality_constraints);
85 Scalar μ = Scalar(0.1) * matrix_callbacks.scaling.f;
86 int iterations = 0;
87
88 return ipm(matrix_callbacks, iteration_callbacks, options, false,
89#ifdef SLEIPNIR_ENABLE_BOUND_PROJECTION
90 bound_constraint_mask,
91#endif
92 x, s, y, z, μ, iterations);
93}
94
128template <typename Scalar>
129ExitStatus ipm(const IPMMatrixCallbacks<Scalar>& matrix_callbacks,
130 std::span<std::function<bool(const IterationInfo<Scalar>& info)>>
131 iteration_callbacks,
132 const Options<Scalar>& options, bool in_feasibility_restoration,
133#ifdef SLEIPNIR_ENABLE_BOUND_PROJECTION
134 const Eigen::ArrayX<bool>& bound_constraint_mask,
135#endif
136 Eigen::VectorX<Scalar>& x, Eigen::VectorX<Scalar>& s,
137 Eigen::VectorX<Scalar>& y, Eigen::VectorX<Scalar>& z, Scalar& μ,
138 int& iterations) {
139 using DenseVector = Eigen::VectorX<Scalar>;
140 using SparseMatrix = Eigen::SparseMatrix<Scalar>;
141 using SparseVector = Eigen::SparseVector<Scalar>;
142
144 struct Step {
146 DenseVector p_x;
148 DenseVector p_s;
150 DenseVector p_y;
152 DenseVector p_z;
153
157 Scalar primal_inf_norm() const {
158 return std::max(p_x.template lpNorm<Eigen::Infinity>(),
159 p_s.template lpNorm<Eigen::Infinity>());
160 }
161
165 Scalar dual_inf_norm() const {
166 return std::max(p_y.template lpNorm<Eigen::Infinity>(),
167 p_z.template lpNorm<Eigen::Infinity>());
168 }
169
177 Scalar tiny_step_size(const DenseVector& x, const DenseVector& s) const {
178 using std::max;
179
180 // A primal step α[pˣ pˢ] is negligible if
181 //
182 // max(‖αpˣ/(1 + |x|)‖_∞, ‖αpˢ/(1 + |s|)‖_∞) < 10ε
183 //
184 // where the absolute values and division are elementwise. Since α > 0,
185 // it factors out of the norms and max.
186 //
187 // α max(‖pˣ/(1 + |x|)‖_∞, ‖pˢ/(1 + |s|)‖_∞) < 10ε
188 //
189 // Dividing both sides by the max gives
190 //
191 // α < 10ε / max(‖pˣ/(1 + |x|)‖_∞, ‖pˢ/(1 + |s|)‖_∞)
192 return Scalar(10) * std::numeric_limits<Scalar>::epsilon() /
193 max((p_x.array().abs() / (Scalar(1) + x.array().abs()))
194 .matrix()
195 .template lpNorm<Eigen::Infinity>(),
196 (p_s.array().abs() / (Scalar(1) + s.array().abs()))
197 .matrix()
198 .template lpNorm<Eigen::Infinity>());
199 }
200 };
201
202 using std::isfinite;
203
204 const auto solve_start_time = std::chrono::steady_clock::now();
205
206 using enum SolveTimeTracerID;
207
208 // Prints final solver diagnostics when the solver exits
209 //
210 // Feasibility restoration still traces so its iterations can be printed, but
211 // the outer solver prints the final solver diagnostics.
212 SolveDiagnostics<ipm_time_trace> diagnostics{options.diagnostics,
213 !in_feasibility_restoration};
214
215 // Set up traced matrix callbacks if diagnostics are enabled. Feasibility
216 // restoration's callbacks call the outer solver's, which are already traced.
217#ifndef SLEIPNIR_DISABLE_DIAGNOSTICS
218 std::optional<IPMMatrixCallbacks<Scalar>> traced_matrices;
219 if (options.diagnostics && !in_feasibility_restoration) {
220 traced_matrices.emplace(
221 matrix_callbacks.num_decision_variables,
222 matrix_callbacks.num_equality_constraints,
223 matrix_callbacks.num_inequality_constraints,
224 traced(diagnostics[COST], matrix_callbacks.f),
225 traced(diagnostics[GRADIENT], matrix_callbacks.g),
226 traced(diagnostics[HESSIAN], matrix_callbacks.H),
227 traced(diagnostics[HESSIAN_CONSTRAINTS], matrix_callbacks.H_c),
228 traced(diagnostics[EQUALITY_CONSTRAINTS], matrix_callbacks.c_e),
229 traced(diagnostics[EQUALITY_JACOBIAN], matrix_callbacks.A_e),
230 traced(diagnostics[INEQUALITY_CONSTRAINTS], matrix_callbacks.c_i),
231 traced(diagnostics[INEQUALITY_JACOBIAN], matrix_callbacks.A_i),
232 matrix_callbacks.scaling);
233 }
234 const auto& matrices = traced_matrices ? *traced_matrices : matrix_callbacks;
235#else
236 const auto& matrices = matrix_callbacks;
237#endif
238
239 ScopedTimeTracer setup_tracer{diagnostics[SETUP]};
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 = matrices.scaling.f * options.tolerance / Scalar(10);
291
292 // Fraction-to-the-boundary rule scale factor minimum
293 constexpr Scalar τ_min(0.99);
294
295 // Fraction-to-the-boundary rule scale factor τ
296 Scalar τ = τ_min;
297
298 Filter<Scalar> filter{constraint_violation(c_e, c_i, s)};
299
300 // The smallest KKT error of a step accepted by the KKT error reduction
301 // fallback for the current barrier parameter. Each fallback step must improve
302 // on it, so finitely many fallback steps are taken before the barrier
303 // subproblem converges.
304 Scalar fallback_kkt_error = std::numeric_limits<Scalar>::infinity();
305
306 // This should be run when the error is below a desired threshold for the
307 // current barrier parameter
308 auto update_barrier_parameter_and_reset_filter = [&] {
309 // Barrier parameter linear decrease power in "κ_μ μ". Range of (0, 1).
310 constexpr Scalar κ_μ(0.2);
311
312 // Barrier parameter superlinear decrease power in "μ^(θ_μ)". Range of (1,
313 // 2).
314 constexpr Scalar θ_μ(1.5);
315
316 // Update the barrier parameter.
317 //
318 // μⱼ₊₁ = max(εₜₒₗ/10, min(κ_μ μⱼ, μⱼ^θ_μ))
319 //
320 // See equation (7) of [2].
321 using std::pow;
322 μ = std::max(μ_min, std::min(κ_μ * μ, pow(μ, θ_μ)));
323
324 // Update the fraction-to-the-boundary rule scaling factor.
325 //
326 // τⱼ = max(τₘᵢₙ, 1 − μⱼ)
327 //
328 // See equation (8) of [2].
329 τ = std::max(τ_min, Scalar(1) - μ);
330
331 // Reset the filter when the barrier parameter is updated
332 filter.reset();
333 fallback_kkt_error = std::numeric_limits<Scalar>::infinity();
334 };
335
336 // Kept outside the loop so its storage can be reused
337 gch::small_vector<Eigen::Triplet<Scalar>> triplets;
338
339 const int lhs_rows =
340 matrices.num_decision_variables + matrices.num_equality_constraints;
341 KKTSolver<Scalar> solver{
342 // Use sparse solver if lower triangle fills < 25% of system
343 H.nonZeros() +
344 (A_i.transpose() * A_i)
345 .template triangularView<Eigen::Lower>()
346 .eval()
347 .nonZeros() +
348 A_e.nonZeros() <
349 0.25 * lhs_rows * lhs_rows,
350 matrices.num_decision_variables, matrices.num_equality_constraints,
351 // Constraint regularization is forced to zero in feasibility restoration
352 // because the equality constraint Jacobian cannot be rank-deficient
353 in_feasibility_restoration ? Scalar(0) : Scalar(1e-10)};
354
355 // Step size reduction factor
356 constexpr Scalar α_reduction_factor(0.5);
357
358 int full_step_rejected_counter = 0;
359
360 StallDetector<Scalar> stall_detector;
361
362 FeasibilityRestorationCycleDetector<Scalar>
363 feasibility_restoration_cycle_detector;
364
365 // Error
366 Scalar E_0 = unscaled_kkt_error<Scalar, KKTErrorType::INF_NORM_SCALED>(
367 matrices.scaling, g, A_e, c_e, A_i, c_i, s, y, z, Scalar(0),
368 options.tolerance);
369
370 setup_tracer.stop();
371
372 // Prints initial iterate diagnostics now and closes the iterate diagnostics
373 // table when the solver exits.
374 //
375 // Feasibility restoration's iterations are printed in the outer solver's
376 // table, so it doesn't open its own.
377 IterateDiagnosticsTable table{
378 options.diagnostics && !in_feasibility_restoration,
379 E_0,
380 f,
381 constraint_violation(c_e, c_i, s),
382 s.dot(z),
383 μ};
384
385 while (E_0 > options.tolerance) {
386 ScopedTimeTracer inner_iter_tracer{diagnostics[ITERATION]};
387
388 // Check for diverging iterates
389 if (x.template lpNorm<Eigen::Infinity>() > Scalar(1e10) || !x.allFinite() ||
390 s.template lpNorm<Eigen::Infinity>() > Scalar(1e10) || !s.allFinite()) {
391 return ExitStatus::DIVERGING_ITERATES;
392 }
393
394 PhaseTimeTracer phase_tracer{diagnostics[CALLBACKS]};
395
396 // Call iteration callbacks
397 for (const auto& callback : iteration_callbacks) {
398 if (callback({iterations, x, s, y, z, g, H, A_e, A_i})) {
399 return ExitStatus::CALLBACK_REQUESTED_STOP;
400 }
401 }
402
403 phase_tracer.next(diagnostics[KKT_MATRIX_BUILD]);
404
405 // S = diag(s)
406 // Z = diag(z)
407 // Σ = S⁻¹Z
408 const SparseMatrix Σ{s.cwiseInverse().asDiagonal() * z.asDiagonal()};
409
410 // lhs = [H + AᵢᵀΣAᵢ Aₑᵀ]
411 // [ Aₑ 0 ]
412 //
413 // Don't assign upper triangle because solver only uses lower triangle.
414 const SparseMatrix top_left =
415 H + (A_i.transpose() * Σ * A_i).template triangularView<Eigen::Lower>();
416 triplets.clear();
417 triplets.reserve(top_left.nonZeros() + A_e.nonZeros());
418 append_as_triplets(triplets, 0, 0, {top_left, A_e});
419 SparseMatrix lhs(
420 matrices.num_decision_variables + matrices.num_equality_constraints,
421 matrices.num_decision_variables + matrices.num_equality_constraints);
422 lhs.setFromSortedTriplets(triplets.begin(), triplets.end());
423
424 // rhs = −[∇f − Aₑᵀy − Aᵢᵀ(−Σcᵢ + μS⁻¹e + z)]
425 // [ cₑ ]
426 DenseVector rhs{x.rows() + y.rows()};
427 rhs.segment(0, x.rows()) =
428 -g + A_e.transpose() * y +
429 A_i.transpose() * (-Σ * c_i + μ * s.cwiseInverse() + z);
430 rhs.segment(x.rows(), y.rows()) = -c_e;
431
432 phase_tracer.next(diagnostics[KKT_MATRIX_DECOMP]);
433
434 Step step;
435 Scalar α_max(1);
436 Scalar α(1);
437 Scalar α_z(1);
438 bool call_feasibility_restoration = false;
439
440 // Solve the Newton-KKT system
441 //
442 // [H + AᵢᵀΣAᵢ Aₑᵀ][ pˣ] = −[∇f − Aₑᵀy − Aᵢᵀ(−Σcᵢ + μS⁻¹e + z)]
443 // [ Aₑ 0 ][−pʸ] [ cₑ ]
444 if (solver.compute(lhs).info() != Eigen::Success) [[unlikely]] {
445 return ExitStatus::FACTORIZATION_FAILED;
446 }
447
448 phase_tracer.next(diagnostics[KKT_SYSTEM_SOLVE]);
449
450 auto compute_step = [&](Step& step, const DenseVector& c_i_minus_s) {
451 // p = [ pˣ]
452 // [−pʸ]
453 DenseVector p = solver.solve(rhs);
454 step.p_x = p.segment(0, x.rows());
455 step.p_y = -p.segment(x.rows(), y.rows());
456
457 // pˢ = cᵢ − s + Aᵢpˣ
458 // pᶻ = μS⁻¹e − z − Σpˢ
459 step.p_s = c_i_minus_s + A_i * step.p_x;
460 step.p_z = μ * s.cwiseInverse() - z - Σ * step.p_s;
461 };
462 compute_step(step, c_i - s);
463
464 phase_tracer.next(diagnostics[LINE_SEARCH]);
465
466 // αᵐᵃˣ = max(α ∈ (0, 1] : sₖ + αpₖˢ ≥ (1−τⱼ)sₖ)
467 α_max = fraction_to_the_boundary_rule<Scalar>(s, step.p_s, τ);
468 α = α_max;
469
470 // αₖᶻ = max(α ∈ (0, 1] : zₖ + αpₖᶻ ≥ (1−τⱼ)zₖ)
471 α_z = fraction_to_the_boundary_rule<Scalar>(z, step.p_z, τ);
472
473 const FilterEntry<Scalar> current_entry{f, s, c_e, c_i, μ};
474
475 // Compute the directional derivative of the log-barrier function along the
476 // search direction.
477 //
478 // ϕ_μ(x, s) = f(x) − μ∑ᵢ ln(sᵢ)
479 //
480 // D_ϕ = ∇ϕ_μ(x, s)ᵀ[pˣ pˢ]
481 // = ∇f(x)ᵀpˣ − μ∑ᵢ pᵢˢ/sᵢ
482 const Scalar D_ϕ =
483 g.transpose() * step.p_x - μ * s.cwiseInverse().dot(step.p_s);
484
485 const Scalar α_min = filter.min_step_size(current_entry, D_ϕ);
486
487 // If maximum step size is below minimum, invoke feasibility restoration
488 if (α < α_min) {
489 call_feasibility_restoration = true;
490 }
491
492 // α_min can be zero (e.g., when the constraint violation is zero), so also
493 // end the line search when the step becomes negligible relative to the
494 // iterate. This bounds the number of step size reductions.
495 const Scalar α_tiny = step.tiny_step_size(x, s);
496
497 // Loop until a step is accepted
498 while (!call_feasibility_restoration) {
499 trial_x = x + α * step.p_x;
500 trial_c_i = matrices.c_i(trial_x);
501 if (options.feasible_ipm && c_i.cwiseGreater(Scalar(0)).all()) {
502 // If the inequality constraints are all feasible, prevent them from
503 // becoming infeasible again.
504 //
505 // See equation (19.30) of [1].
506 trial_s = trial_c_i;
507 } else {
508 trial_s = s + α * step.p_s;
509 }
510 trial_y = y + α * step.p_y;
511 trial_z = z + α_z * step.p_z;
512
513 trial_f = matrices.f(trial_x);
514 trial_c_e = matrices.c_e(trial_x);
515
516 // If f(xₖ + αpₖˣ), cₑ(xₖ + αpₖˣ), or cᵢ(xₖ + αpₖˣ) aren't finite, reduce
517 // step size immediately
518 if (!isfinite(trial_f) || !trial_c_e.allFinite() ||
519 !trial_c_i.allFinite()) {
520 // Reduce step size
521 α *= α_reduction_factor;
522
523 if (α < α_min || α < α_tiny) {
524 call_feasibility_restoration = true;
525 break;
526 }
527 continue;
528 }
529
530 // Check whether filter accepts trial iterate
531 FilterEntry trial_entry{trial_f, trial_s, trial_c_e, trial_c_i, μ};
532 if (filter.try_add(current_entry, trial_entry, D_ϕ, α)) {
533 // Accept step
534 break;
535 }
536
537 Scalar prev_constraint_violation = constraint_violation(c_e, c_i, s);
538 Scalar next_constraint_violation =
539 constraint_violation(trial_c_e, trial_c_i, trial_s);
540
541 // Second-order corrections
542 //
543 // If first trial point was rejected and constraint violation stayed the
544 // same or went up, apply second-order corrections
545 if (α == α_max &&
546 next_constraint_violation >= prev_constraint_violation) {
547 // Apply second-order corrections. See section 2.4 of [2].
548 auto soc_step = step;
549
550 Scalar α_soc = α;
551 Scalar α_z_soc = α_z;
552 DenseVector c_e_soc = c_e;
553 DenseVector c_i_minus_s_soc = c_i - s;
554
555 Scalar soc_constraint_violation = next_constraint_violation;
556
557 bool step_acceptable = false;
558 for (int soc_iteration = 0; soc_iteration < 5 && !step_acceptable;
559 ++soc_iteration) {
560 ScopedTimeTracer soc_tracer{diagnostics[SOC]};
561
562 scope_exit soc_exit{[&] {
563 soc_tracer.stop();
564
565 if (options.diagnostics && step_acceptable) {
566 print_iterate_diagnostics(
567 iterations, IterationType::SECOND_ORDER_CORRECTION,
568 soc_tracer.current_duration(),
569 unscaled_kkt_error<Scalar, KKTErrorType::INF_NORM_SCALED>(
570 matrices.scaling, g, A_e, trial_c_e, A_i, trial_c_i,
571 trial_s, trial_y, trial_z, Scalar(0), options.tolerance),
572 trial_f, constraint_violation(trial_c_e, trial_c_i, trial_s),
573 trial_s.dot(trial_z), μ, solver.hessian_regularization(),
574 solver.constraint_jacobian_regularization(),
575 soc_step.primal_inf_norm(), soc_step.dual_inf_norm(), α_soc,
576 Scalar(1), α_reduction_factor, α_z_soc);
577 }
578 }};
579
580 // Rebuild Newton-KKT rhs with updated constraint values.
581 //
582 // rhs = −[∇f − Aₑᵀy − Aᵢᵀ(μS⁻¹e − Σ(cᵢ − s)ˢᵒᶜ)]
583 // [ cₑˢᵒᶜ ]
584 //
585 // where
586 //
587 // cₑˢᵒᶜ = αˢᵒᶜcₑ(xₖ) + cₑ(xₖ + αˢᵒᶜpˣˢᵒᶜ)
588 // (cᵢ − s)ˢᵒᶜ =
589 // αˢᵒᶜ(cᵢ(xₖ) − sₖ) + cᵢ(xₖ + αˢᵒᶜpˣˢᵒᶜ) − (sₖ + αˢᵒᶜpˢˢᵒᶜ)
590 c_e_soc = α_soc * c_e_soc + trial_c_e;
591 c_i_minus_s_soc = α_soc * c_i_minus_s_soc + trial_c_i - trial_s;
592 rhs.segment(0, x.rows()) =
593 -g + A_e.transpose() * y +
594 A_i.transpose() * (μ * s.cwiseInverse() - Σ * c_i_minus_s_soc);
595 rhs.segment(x.rows(), y.rows()) = -c_e_soc;
596
597 // Solve the Newton-KKT system
598 compute_step(soc_step, c_i_minus_s_soc);
599
600 // αˢᵒᶜ = max(α ∈ (0, 1] : sₖ + αpₖˢ ≥ (1−τⱼ)sₖ)
601 // αₖᶻˢᵒᶜ = max(α ∈ (0, 1] : zₖ + αpₖᶻ ≥ (1−τⱼ)zₖ)
602 α_soc = fraction_to_the_boundary_rule<Scalar>(s, soc_step.p_s, τ);
603 α_z_soc = fraction_to_the_boundary_rule<Scalar>(z, soc_step.p_z, τ);
604
605 trial_x = x + α_soc * soc_step.p_x;
606 trial_s = s + α_soc * soc_step.p_s;
607 trial_y = y + α_soc * soc_step.p_y;
608 trial_z = z + α_z_soc * soc_step.p_z;
609
610 trial_f = matrices.f(trial_x);
611 trial_c_e = matrices.c_e(trial_x);
612 trial_c_i = matrices.c_i(trial_x);
613
614 // Check whether filter accepts trial iterate
615 FilterEntry trial_entry{trial_f, trial_s, trial_c_e, trial_c_i, μ};
616 if (filter.try_add(current_entry, trial_entry, D_ϕ, α)) {
617 step = soc_step;
618 α = α_soc;
619 α_z = α_z_soc;
620 step_acceptable = true;
621 break;
622 }
623
624 // Constraint violation scale factor for second-order corrections
625 constexpr Scalar κ_soc(0.99);
626
627 // If constraint violation hasn't been sufficiently reduced, stop
628 // making second-order corrections
629 next_constraint_violation =
630 constraint_violation(trial_c_e, trial_c_i, trial_s);
631 if (next_constraint_violation > κ_soc * soc_constraint_violation) {
632 break;
633 }
634
635 soc_constraint_violation = next_constraint_violation;
636 }
637
638 if (step_acceptable) {
639 // Accept step
640 break;
641 }
642 }
643
644 // If we got here and α is the full step, the full step was rejected.
645 // Increment the full-step rejected counter to keep track of how many full
646 // steps have been rejected in a row.
647 if (α == α_max) {
648 ++full_step_rejected_counter;
649 }
650
651 // If the full step was rejected enough times in a row, reset the filter
652 // if it may be impeding progress
653 if (full_step_rejected_counter >= 4 && filter.try_reset(current_entry)) {
654 continue;
655 }
656
657 // Reduce step size
658 α *= α_reduction_factor;
659
660 // If step size hit a minimum or the step became negligible, check if the
661 // KKT error was reduced. If it wasn't, invoke feasibility restoration.
662 if (α < α_min || α < α_tiny) {
663 Scalar current_kkt_error = kkt_error<Scalar, KKTErrorType::ONE_NORM>(
664 g, A_e, c_e, A_i, c_i, s, y, z, μ, options.tolerance);
665
666 trial_x = x + α_max * step.p_x;
667 trial_s = s + α_max * step.p_s;
668 trial_y = y + α_max * step.p_y;
669 trial_z = z + α_z * step.p_z;
670
671 trial_f = matrices.f(trial_x);
672 trial_c_e = matrices.c_e(trial_x);
673 trial_c_i = matrices.c_i(trial_x);
674
675 Scalar next_kkt_error = kkt_error<Scalar, KKTErrorType::ONE_NORM>(
676 matrices.g(trial_x), matrices.A_e(trial_x), trial_c_e,
677 matrices.A_i(trial_x), trial_c_i, trial_s, trial_y, trial_z, μ,
678 options.tolerance);
679
680 // If the step using αᵐᵃˣ reduced the KKT error below the current
681 // iterate's and every previous fallback step's, accept it anyway
682 if (next_kkt_error <= Scalar(0.999) * std::min(current_kkt_error,
683 fallback_kkt_error) &&
684 constraint_violation(trial_c_e, trial_c_i, trial_s) <=
685 filter.max_constraint_violation) {
686 // Accept step
687 fallback_kkt_error = next_kkt_error;
688 α = α_max;
689 break;
690 }
691
692 call_feasibility_restoration = true;
693 break;
694 }
695 }
696
697 phase_tracer.stop();
698
699 // If solver stalled, invoke feasibility restoration
700 if (!call_feasibility_restoration && !in_feasibility_restoration) {
701 FilterEntry trial_entry{trial_f, trial_s, trial_c_e, trial_c_i, μ};
702 if (stall_detector.update(current_entry, trial_entry, α,
703 filter.min_constraint_violation)) {
704 call_feasibility_restoration = true;
705 }
706 }
707
708 if (call_feasibility_restoration) {
709 ScopedTimeTracer feasibility_restoration_tracer{
710 diagnostics[FEASIBILITY_RESTORATION]};
711
712 // If already in feasibility restoration mode, running it again won't help
713 if (in_feasibility_restoration) {
714 return ExitStatus::FEASIBILITY_RESTORATION_FAILED;
715 }
716
717 FilterEntry initial_entry{matrices.f(x), s, c_e, c_i, μ};
718
719 // Augment the filter with the current iterate so the normal phase can't
720 // return to it
721 filter.add(initial_entry);
722
723 feasibility_restoration_cycle_detector.record_entry(
724 initial_entry.constraint_violation);
725 const bool run_feasibility_restoration_to_convergence =
726 feasibility_restoration_cycle_detector.is_cycling();
727
728 // Feasibility restoration phase
729 gch::small_vector<std::function<bool(const IterationInfo<Scalar>& info)>>
730 callbacks;
731 for (auto& callback : iteration_callbacks) {
732 callbacks.emplace_back(callback);
733 }
734 callbacks.emplace_back([&](const IterationInfo<Scalar>& info) {
735 if (run_feasibility_restoration_to_convergence) {
736 return false;
737 }
738
739 DenseVector trial_x =
740 info.x.segment(0, matrices.num_decision_variables);
741
742 DenseVector trial_c_e = matrices.c_e(trial_x);
743 DenseVector trial_c_i = matrices.c_i(trial_x);
744
745 // Feasibility restoration re-centers the slacks when it returns, so
746 // check the iterate the normal phase will resume from
747 DenseVector trial_s =
748 (trial_c_i.array() >= Scalar(1e-2))
749 .select(trial_c_i,
750 info.s.segment(0, matrices.num_inequality_constraints));
751
752 // If the current iterate sufficiently reduces constraint violation and
753 // is accepted by the normal filter, stop feasibility restoration
754 FilterEntry trial_entry{matrices.f(trial_x), trial_s, trial_c_e,
755 trial_c_i, μ};
756 return trial_entry.constraint_violation <
757 Scalar(0.9) * initial_entry.constraint_violation &&
758 filter.is_acceptable(trial_entry);
759 });
760 auto status =
761 feasibility_restoration<Scalar>(matrices, callbacks, options,
762#ifdef SLEIPNIR_ENABLE_BOUND_PROJECTION
763 bound_constraint_mask,
764#endif
765 x, s, y, z, μ, iterations);
766
767 if (status != ExitStatus::SUCCESS) {
768 // Report failure
769 return status;
770 }
771
772 f = matrices.f(x);
773 c_e = matrices.c_e(x);
774 c_i = matrices.c_i(x);
775
776 // Resuming from an iterate the filter rejects would break the filter's
777 // convergence guarantee, and continuing feasibility restoration from its
778 // converged point would return that point again, so report failure.
779 //
780 // The returned iterate can be rejected because feasibility restoration
781 // stops at its own convergence tolerance instead of driving constraint
782 // violation to zero, so the filter can still hold entries with lower cost
783 // and constraint violation.
784 if (!filter.is_acceptable(FilterEntry{f, s, c_e, c_i, μ})) {
785 return ExitStatus::FEASIBILITY_RESTORATION_FAILED;
786 }
787 } else {
788 // If full step was accepted, reset full-step rejected counter
789 if (α == α_max) {
790 full_step_rejected_counter = 0;
791 }
792
793 // Update iterates
794 x = trial_x;
795 s = trial_s;
796 y = trial_y;
797 z = trial_z;
798
799 f = trial_f;
800 c_e = trial_c_e;
801 c_i = trial_c_i;
802 }
803
804 // A requirement for the convergence proof is that the primal-dual barrier
805 // term Hessian Σₖ₊₁ does not deviate arbitrarily much from the primal
806 // barrier term Hessian μSₖ₊₁⁻².
807 //
808 // Σₖ₊₁ = μSₖ₊₁⁻²
809 // Sₖ₊₁⁻¹Zₖ₊₁ = μSₖ₊₁⁻²
810 // Zₖ₊₁ = μSₖ₊₁⁻¹
811 //
812 // We ensure this by resetting
813 //
814 // zₖ₊₁ = clamp(zₖ₊₁, 1/κ_Σ μ/sₖ₊₁, κ_Σ μ/sₖ₊₁)
815 //
816 // for some fixed κ_Σ ≥ 1 after each step. See equation (16) of [2].
817 for (int row = 0; row < z.rows(); ++row) {
818 constexpr Scalar κ_Σ(1e10);
819 z[row] =
820 std::clamp(z[row], Scalar(1) / κ_Σ * μ / s[row], κ_Σ * μ / s[row]);
821 }
822
823 // Update autodiff for Jacobians and Hessian
824 A_e = matrices.A_e(x);
825 A_i = matrices.A_i(x);
826 g = matrices.g(x);
827 H = matrices.H(x, y, z);
828
829 // Update the error
830 E_0 = unscaled_kkt_error<Scalar, KKTErrorType::INF_NORM_SCALED>(
831 matrices.scaling, g, A_e, c_e, A_i, c_i, s, y, z, Scalar(0),
832 options.tolerance);
833
834 // Update the barrier parameter if necessary
835 if (E_0 > options.tolerance) {
836 // Barrier parameter scale factor for tolerance checks
837 constexpr Scalar κ_ε(10);
838
839 // While the error is below the desired threshold for this barrier
840 // parameter value, decrease the barrier parameter further
841 Scalar E_μ = kkt_error<Scalar, KKTErrorType::INF_NORM_SCALED>(
842 g, A_e, c_e, A_i, c_i, s, y, z, μ, options.tolerance);
843 while (μ > μ_min && E_μ <= κ_ε * μ) {
844 update_barrier_parameter_and_reset_filter();
845 E_μ = kkt_error<Scalar, KKTErrorType::INF_NORM_SCALED>(
846 g, A_e, c_e, A_i, c_i, s, y, z, μ, options.tolerance);
847 }
848 }
849
850 inner_iter_tracer.stop();
851
852 if (options.diagnostics) {
853 print_iterate_diagnostics(
854 iterations,
855 in_feasibility_restoration ? IterationType::FEASIBILITY_RESTORATION
856 : IterationType::NORMAL,
857 inner_iter_tracer.current_duration(), E_0, f,
858 constraint_violation(c_e, c_i, s), s.dot(z), μ,
859 solver.hessian_regularization(),
860 solver.constraint_jacobian_regularization(), step.primal_inf_norm(),
861 step.dual_inf_norm(), α, α_max, α_reduction_factor, α_z);
862 }
863
864 ++iterations;
865
866 // Check for max iterations
867 if (iterations >= options.max_iterations) {
868 return ExitStatus::MAX_ITERATIONS_EXCEEDED;
869 }
870
871 // Check for max wall clock time
872 if (std::chrono::steady_clock::now() - solve_start_time > options.timeout) {
873 return ExitStatus::TIMEOUT;
874 }
875 }
876
877 if (!isfinite(E_0)) {
878 return ExitStatus::DIVERGING_ITERATES;
879 } else {
880 return ExitStatus::SUCCESS;
881 }
882}
883
884extern template SLEIPNIR_DLLEXPORT ExitStatus
885ipm(const IPMMatrixCallbacks<double>& matrix_callbacks,
886 std::span<std::function<bool(const IterationInfo<double>& info)>>
887 iteration_callbacks,
888 const Options<double>& options,
889#ifdef SLEIPNIR_ENABLE_BOUND_PROJECTION
890 const Eigen::ArrayX<bool>& bound_constraint_mask,
891#endif
892 Eigen::VectorX<double>& x);
893
894} // namespace slp