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