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