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 <limits>
10#include <span>
11
12#include <Eigen/Core>
13#include <Eigen/SparseCore>
14#include <gch/small_vector.hpp>
15
16#include "sleipnir/optimization/solver/exit_status.hpp"
17#include "sleipnir/optimization/solver/interior_point_matrix_callbacks.hpp"
18#include "sleipnir/optimization/solver/iteration_info.hpp"
19#include "sleipnir/optimization/solver/options.hpp"
20#include "sleipnir/optimization/solver/util/error_estimate.hpp"
21#include "sleipnir/optimization/solver/util/filter.hpp"
22#include "sleipnir/optimization/solver/util/fraction_to_the_boundary_rule.hpp"
23#include "sleipnir/optimization/solver/util/is_locally_infeasible.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/scope_exit.hpp"
29#include "sleipnir/util/scoped_profiler.hpp"
30#include "sleipnir/util/solve_profiler.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
64template <typename Scalar>
65ExitStatus interior_point(
66 const InteriorPointMatrixCallbacks<Scalar>& matrix_callbacks,
67 std::span<std::function<bool(const IterationInfo<Scalar>& info)>>
68 iteration_callbacks,
69 const Options& options,
70#ifdef SLEIPNIR_ENABLE_BOUND_PROJECTION
71 const Eigen::ArrayX<bool>& bound_constraint_mask,
72#endif
73 Eigen::Vector<Scalar, Eigen::Dynamic>& x) {
77 struct Step {
79 Eigen::Vector<Scalar, Eigen::Dynamic> p_x;
81 Eigen::Vector<Scalar, Eigen::Dynamic> p_y;
83 Eigen::Vector<Scalar, Eigen::Dynamic> p_s;
85 Eigen::Vector<Scalar, Eigen::Dynamic> p_z;
86 };
87
88 using std::isfinite;
89
90 const auto solve_start_time = std::chrono::steady_clock::now();
91
92 gch::small_vector<SolveProfiler> solve_profilers;
93 solve_profilers.emplace_back("solver");
94 solve_profilers.emplace_back(" ↳ setup");
95 solve_profilers.emplace_back(" ↳ iteration");
96 solve_profilers.emplace_back(" ↳ feasibility ✓");
97 solve_profilers.emplace_back(" ↳ iter callbacks");
98 solve_profilers.emplace_back(" ↳ KKT matrix build");
99 solve_profilers.emplace_back(" ↳ KKT matrix decomp");
100 solve_profilers.emplace_back(" ↳ KKT system solve");
101 solve_profilers.emplace_back(" ↳ line search");
102 solve_profilers.emplace_back(" ↳ SOC");
103 solve_profilers.emplace_back(" ↳ next iter prep");
104 solve_profilers.emplace_back(" ↳ f(x)");
105 solve_profilers.emplace_back(" ↳ ∇f(x)");
106 solve_profilers.emplace_back(" ↳ ∇²ₓₓL");
107 solve_profilers.emplace_back(" ↳ cₑ(x)");
108 solve_profilers.emplace_back(" ↳ ∂cₑ/∂x");
109 solve_profilers.emplace_back(" ↳ cᵢ(x)");
110 solve_profilers.emplace_back(" ↳ ∂cᵢ/∂x");
111
112 auto& solver_prof = solve_profilers[0];
113 auto& setup_prof = solve_profilers[1];
114 auto& inner_iter_prof = solve_profilers[2];
115 auto& feasibility_check_prof = solve_profilers[3];
116 auto& iter_callbacks_prof = solve_profilers[4];
117 auto& kkt_matrix_build_prof = solve_profilers[5];
118 auto& kkt_matrix_decomp_prof = solve_profilers[6];
119 auto& kkt_system_solve_prof = solve_profilers[7];
120 auto& line_search_prof = solve_profilers[8];
121 auto& soc_prof = solve_profilers[9];
122 auto& next_iter_prep_prof = solve_profilers[10];
123
124 // Set up profiled matrix callbacks
125#ifndef SLEIPNIR_DISABLE_DIAGNOSTICS
126 auto& f_prof = solve_profilers[11];
127 auto& g_prof = solve_profilers[12];
128 auto& H_prof = solve_profilers[13];
129 auto& c_e_prof = solve_profilers[14];
130 auto& A_e_prof = solve_profilers[15];
131 auto& c_i_prof = solve_profilers[16];
132 auto& A_i_prof = solve_profilers[17];
133
134 InteriorPointMatrixCallbacks<Scalar> matrices{
135 [&](const Eigen::Vector<Scalar, Eigen::Dynamic>& x) -> Scalar {
136 ScopedProfiler prof{f_prof};
137 return matrix_callbacks.f(x);
138 },
139 [&](const Eigen::Vector<Scalar, Eigen::Dynamic>& x)
140 -> Eigen::SparseVector<Scalar> {
141 ScopedProfiler prof{g_prof};
142 return matrix_callbacks.g(x);
143 },
144 [&](const Eigen::Vector<Scalar, Eigen::Dynamic>& x,
145 const Eigen::Vector<Scalar, Eigen::Dynamic>& y,
146 const Eigen::Vector<Scalar, Eigen::Dynamic>& z)
147 -> Eigen::SparseMatrix<Scalar> {
148 ScopedProfiler prof{H_prof};
149 return matrix_callbacks.H(x, y, z);
150 },
151 [&](const Eigen::Vector<Scalar, Eigen::Dynamic>& x)
152 -> Eigen::Vector<Scalar, Eigen::Dynamic> {
153 ScopedProfiler prof{c_e_prof};
154 return matrix_callbacks.c_e(x);
155 },
156 [&](const Eigen::Vector<Scalar, Eigen::Dynamic>& x)
157 -> Eigen::SparseMatrix<Scalar> {
158 ScopedProfiler prof{A_e_prof};
159 return matrix_callbacks.A_e(x);
160 },
161 [&](const Eigen::Vector<Scalar, Eigen::Dynamic>& x)
162 -> Eigen::Vector<Scalar, Eigen::Dynamic> {
163 ScopedProfiler prof{c_i_prof};
164 return matrix_callbacks.c_i(x);
165 },
166 [&](const Eigen::Vector<Scalar, Eigen::Dynamic>& x)
167 -> Eigen::SparseMatrix<Scalar> {
168 ScopedProfiler prof{A_i_prof};
169 return matrix_callbacks.A_i(x);
170 }};
171#else
172 const auto& matrices = matrix_callbacks;
173#endif
174
175 solver_prof.start();
176 setup_prof.start();
177
178 Scalar f = matrices.f(x);
179 Eigen::Vector<Scalar, Eigen::Dynamic> c_e = matrices.c_e(x);
180 Eigen::Vector<Scalar, Eigen::Dynamic> c_i = matrices.c_i(x);
181
182 int num_decision_variables = x.rows();
183 int num_equality_constraints = c_e.rows();
184 int num_inequality_constraints = c_i.rows();
185
186 // Check for overconstrained problem
187 if (num_equality_constraints > num_decision_variables) {
188 if (options.diagnostics) {
189 print_too_few_dofs_error(c_e);
190 }
191
192 return ExitStatus::TOO_FEW_DOFS;
193 }
194
195 Eigen::SparseVector<Scalar> g = matrices.g(x);
196 Eigen::SparseMatrix<Scalar> A_e = matrices.A_e(x);
197 Eigen::SparseMatrix<Scalar> A_i = matrices.A_i(x);
198
199 Eigen::Vector<Scalar, Eigen::Dynamic> s =
200 Eigen::Vector<Scalar, Eigen::Dynamic>::Ones(num_inequality_constraints);
201#ifdef SLEIPNIR_ENABLE_BOUND_PROJECTION
202 // We set sʲ = cᵢʲ(x) for each bound inequality constraint index j
203 s = bound_constraint_mask.select(c_i, s);
204#endif
205 Eigen::Vector<Scalar, Eigen::Dynamic> y =
206 Eigen::Vector<Scalar, Eigen::Dynamic>::Zero(num_equality_constraints);
207 Eigen::Vector<Scalar, Eigen::Dynamic> z =
208 Eigen::Vector<Scalar, Eigen::Dynamic>::Ones(num_inequality_constraints);
209
210 Eigen::SparseMatrix<Scalar> H = matrices.H(x, y, z);
211
212 // Ensure matrix callback dimensions are consistent
213 slp_assert(g.rows() == num_decision_variables);
214 slp_assert(A_e.rows() == num_equality_constraints);
215 slp_assert(A_e.cols() == num_decision_variables);
216 slp_assert(A_i.rows() == num_inequality_constraints);
217 slp_assert(A_i.cols() == num_decision_variables);
218 slp_assert(H.rows() == num_decision_variables);
219 slp_assert(H.cols() == num_decision_variables);
220
221 // Check whether initial guess has finite f(xₖ), cₑ(xₖ), and cᵢ(xₖ)
222 if (!isfinite(f) || !c_e.allFinite() || !c_i.allFinite()) {
223 return ExitStatus::NONFINITE_INITIAL_COST_OR_CONSTRAINTS;
224 }
225
226 int iterations = 0;
227
228 // Barrier parameter minimum
229 const Scalar μ_min = Scalar(options.tolerance) / Scalar(10);
230
231 // Barrier parameter μ
232 Scalar μ(0.1);
233
234 // Fraction-to-the-boundary rule scale factor minimum
235 constexpr Scalar τ_min(0.99);
236
237 // Fraction-to-the-boundary rule scale factor τ
238 Scalar τ = τ_min;
239
240 Filter<Scalar> filter;
241
242 // This should be run when the error estimate is below a desired threshold for
243 // the current barrier parameter
244 auto update_barrier_parameter_and_reset_filter = [&] {
245 // Barrier parameter linear decrease power in "κ_μ μ". Range of (0, 1).
246 constexpr Scalar κ_μ(0.2);
247
248 // Barrier parameter superlinear decrease power in "μ^(θ_μ)". Range of (1,
249 // 2).
250 constexpr Scalar θ_μ(1.5);
251
252 // Update the barrier parameter.
253 //
254 // μⱼ₊₁ = max(εₜₒₗ/10, min(κ_μ μⱼ, μⱼ^θ_μ))
255 //
256 // See equation (7) of [2].
257 using std::pow;
258 μ = std::max(μ_min, std::min(κ_μ * μ, pow(μ, θ_μ)));
259
260 // Update the fraction-to-the-boundary rule scaling factor.
261 //
262 // τⱼ = max(τₘᵢₙ, 1 − μⱼ)
263 //
264 // See equation (8) of [2].
265 τ = std::max(τ_min, Scalar(1) - μ);
266
267 // Reset the filter when the barrier parameter is updated
268 filter.reset();
269 };
270
271 // Kept outside the loop so its storage can be reused
272 gch::small_vector<Eigen::Triplet<Scalar>> triplets;
273
274 RegularizedLDLT<Scalar> solver{num_decision_variables,
275 num_equality_constraints};
276
277 // Variables for determining when a step is acceptable
278 constexpr Scalar α_reduction_factor(0.5);
279 constexpr Scalar α_min(1e-7);
280
281 int full_step_rejected_counter = 0;
282
283 // Error estimate
284 Scalar E_0 = std::numeric_limits<Scalar>::infinity();
285
286 setup_prof.stop();
287
288 // Prints final solver diagnostics when the solver exits
289 scope_exit exit{[&] {
290 if (options.diagnostics) {
291 solver_prof.stop();
292 if (iterations > 0) {
293 print_bottom_iteration_diagnostics();
294 }
295 print_solver_diagnostics(solve_profilers);
296 }
297 }};
298
299 while (E_0 > Scalar(options.tolerance)) {
300 ScopedProfiler inner_iter_profiler{inner_iter_prof};
301 ScopedProfiler feasibility_check_profiler{feasibility_check_prof};
302
303 // Check for local equality constraint infeasibility
304 if (is_equality_locally_infeasible(A_e, c_e)) {
305 if (options.diagnostics) {
306 print_c_e_local_infeasibility_error(c_e);
307 }
308
309 return ExitStatus::LOCALLY_INFEASIBLE;
310 }
311
312 // Check for local inequality constraint infeasibility
313 if (is_inequality_locally_infeasible(A_i, c_i)) {
314 if (options.diagnostics) {
315 print_c_i_local_infeasibility_error(c_i);
316 }
317
318 return ExitStatus::LOCALLY_INFEASIBLE;
319 }
320
321 // Check for diverging iterates
322 if (x.template lpNorm<Eigen::Infinity>() > Scalar(1e10) || !x.allFinite() ||
323 s.template lpNorm<Eigen::Infinity>() > Scalar(1e10) || !s.allFinite()) {
324 return ExitStatus::DIVERGING_ITERATES;
325 }
326
327 feasibility_check_profiler.stop();
328 ScopedProfiler iter_callbacks_profiler{iter_callbacks_prof};
329
330 // Call iteration callbacks
331 for (const auto& callback : iteration_callbacks) {
332 if (callback({iterations, x, g, H, A_e, A_i})) {
333 return ExitStatus::CALLBACK_REQUESTED_STOP;
334 }
335 }
336
337 iter_callbacks_profiler.stop();
338 ScopedProfiler kkt_matrix_build_profiler{kkt_matrix_build_prof};
339
340 // S = diag(s)
341 // Z = diag(z)
342 // Σ = S⁻¹Z
343 const Eigen::SparseMatrix<Scalar> Σ{s.cwiseInverse().asDiagonal() *
344 z.asDiagonal()};
345
346 // lhs = [H + AᵢᵀΣAᵢ Aₑᵀ]
347 // [ Aₑ 0 ]
348 //
349 // Don't assign upper triangle because solver only uses lower triangle.
350 const Eigen::SparseMatrix<Scalar> top_left =
351 H + (A_i.transpose() * Σ * A_i).template triangularView<Eigen::Lower>();
352 triplets.clear();
353 triplets.reserve(top_left.nonZeros() + A_e.nonZeros());
354 for (int col = 0; col < H.cols(); ++col) {
355 // Append column of H + AᵢᵀΣAᵢ lower triangle in top-left quadrant
356 for (typename Eigen::SparseMatrix<Scalar>::InnerIterator it{top_left,
357 col};
358 it; ++it) {
359 triplets.emplace_back(it.row(), it.col(), it.value());
360 }
361 // Append column of Aₑ in bottom-left quadrant
362 for (typename Eigen::SparseMatrix<Scalar>::InnerIterator it{A_e, col}; it;
363 ++it) {
364 triplets.emplace_back(H.rows() + it.row(), it.col(), it.value());
365 }
366 }
367 Eigen::SparseMatrix<Scalar> lhs(
368 num_decision_variables + num_equality_constraints,
369 num_decision_variables + num_equality_constraints);
370 lhs.setFromSortedTriplets(triplets.begin(), triplets.end());
371
372 // rhs = −[∇f − Aₑᵀy − Aᵢᵀ(−Σcᵢ + μS⁻¹e + z)]
373 // [ cₑ ]
374 Eigen::Vector<Scalar, Eigen::Dynamic> rhs{x.rows() + y.rows()};
375 rhs.segment(0, x.rows()) =
376 -g + A_e.transpose() * y +
377 A_i.transpose() * (-Σ * c_i + μ * s.cwiseInverse() + z);
378 rhs.segment(x.rows(), y.rows()) = -c_e;
379
380 kkt_matrix_build_profiler.stop();
381 ScopedProfiler kkt_matrix_decomp_profiler{kkt_matrix_decomp_prof};
382
383 Step step;
384 Scalar α_max(1);
385 Scalar α(1);
386 Scalar α_z(1);
387
388 // Solve the Newton-KKT system
389 //
390 // [H + AᵢᵀΣAᵢ Aₑᵀ][ pˣ] = −[∇f − Aₑᵀy − Aᵢᵀ(−Σcᵢ + μS⁻¹e + z)]
391 // [ Aₑ 0 ][−pʸ] [ cₑ ]
392 if (solver.compute(lhs).info() != Eigen::Success) [[unlikely]] {
393 return ExitStatus::FACTORIZATION_FAILED;
394 }
395
396 kkt_matrix_decomp_profiler.stop();
397 ScopedProfiler kkt_system_solve_profiler{kkt_system_solve_prof};
398
399 auto compute_step = [&](Step& step) {
400 // p = [ pˣ]
401 // [−pʸ]
402 Eigen::Vector<Scalar, Eigen::Dynamic> p = solver.solve(rhs);
403 step.p_x = p.segment(0, x.rows());
404 step.p_y = -p.segment(x.rows(), y.rows());
405
406 // pˢ = cᵢ − s + Aᵢpˣ
407 // pᶻ = −Σcᵢ + μS⁻¹e − ΣAᵢpˣ
408 step.p_s = c_i - s + A_i * step.p_x;
409 step.p_z = -Σ * c_i + μ * s.cwiseInverse() - Σ * A_i * step.p_x;
410 };
411 compute_step(step);
412
413 kkt_system_solve_profiler.stop();
414 ScopedProfiler line_search_profiler{line_search_prof};
415
416 // αᵐᵃˣ = max(α ∈ (0, 1] : sₖ + αpₖˢ ≥ (1−τⱼ)sₖ)
417 α_max = fraction_to_the_boundary_rule<Scalar>(s, step.p_s, τ);
418 α = α_max;
419
420 // If maximum step size is below minimum, report line search failure
421 if (α < α_min) {
422 return ExitStatus::LINE_SEARCH_FAILED;
423 }
424
425 // αₖᶻ = max(α ∈ (0, 1] : zₖ + αpₖᶻ ≥ (1−τⱼ)zₖ)
426 α_z = fraction_to_the_boundary_rule<Scalar>(z, step.p_z, τ);
427
428 // Loop until a step is accepted
429 while (1) {
430 Eigen::Vector<Scalar, Eigen::Dynamic> trial_x = x + α * step.p_x;
431 Eigen::Vector<Scalar, Eigen::Dynamic> trial_y = y + α_z * step.p_y;
432 Eigen::Vector<Scalar, Eigen::Dynamic> trial_z = z + α_z * step.p_z;
433
434 Scalar trial_f = matrices.f(trial_x);
435 Eigen::Vector<Scalar, Eigen::Dynamic> trial_c_e = matrices.c_e(trial_x);
436 Eigen::Vector<Scalar, Eigen::Dynamic> trial_c_i = matrices.c_i(trial_x);
437
438 // If f(xₖ + αpₖˣ), cₑ(xₖ + αpₖˣ), or cᵢ(xₖ + αpₖˣ) aren't finite, reduce
439 // step size immediately
440 if (!isfinite(trial_f) || !trial_c_e.allFinite() ||
441 !trial_c_i.allFinite()) {
442 // Reduce step size
443 α *= α_reduction_factor;
444
445 if (α < α_min) {
446 return ExitStatus::LINE_SEARCH_FAILED;
447 }
448 continue;
449 }
450
451 Eigen::Vector<Scalar, Eigen::Dynamic> trial_s;
452 if (options.feasible_ipm && c_i.cwiseGreater(Scalar(0)).all()) {
453 // If the inequality constraints are all feasible, prevent them from
454 // becoming infeasible again.
455 //
456 // See equation (19.30) in [1].
457 trial_s = trial_c_i;
458 } else {
459 trial_s = s + α * step.p_s;
460 }
461
462 // Check whether filter accepts trial iterate
463 if (filter.try_add(FilterEntry{trial_f, trial_s, trial_c_e, trial_c_i, μ},
464 α)) {
465 // Accept step
466 break;
467 }
468
469 Scalar prev_constraint_violation =
470 c_e.template lpNorm<1>() + (c_i - s).template lpNorm<1>();
471 Scalar next_constraint_violation =
472 trial_c_e.template lpNorm<1>() +
473 (trial_c_i - trial_s).template lpNorm<1>();
474
475 // Second-order corrections
476 //
477 // If first trial point was rejected and constraint violation stayed the
478 // same or went up, apply second-order corrections
479 if (α == α_max &&
480 next_constraint_violation >= prev_constraint_violation) {
481 // Apply second-order corrections. See section 2.4 of [2].
482 auto soc_step = step;
483
484 Scalar α_soc = α;
485 Scalar α_z_soc = α_z;
486 Eigen::Vector<Scalar, Eigen::Dynamic> c_e_soc = c_e;
487
488 bool step_acceptable = false;
489 for (int soc_iteration = 0; soc_iteration < 5 && !step_acceptable;
490 ++soc_iteration) {
491 ScopedProfiler soc_profiler{soc_prof};
492
493 scope_exit soc_exit{[&] {
494 soc_profiler.stop();
495
496 if (options.diagnostics) {
497 print_iteration_diagnostics(
498 iterations,
499 step_acceptable ? IterationType::ACCEPTED_SOC
500 : IterationType::REJECTED_SOC,
501 soc_profiler.current_duration(),
502 error_estimate<Scalar>(g, A_e, trial_c_e, trial_y), trial_f,
503 trial_c_e.template lpNorm<1>() +
504 (trial_c_i - trial_s).template lpNorm<1>(),
505 trial_s.dot(trial_z), μ, solver.hessian_regularization(),
506 α_soc, Scalar(1), α_reduction_factor, α_z_soc);
507 }
508 }};
509
510 // Rebuild Newton-KKT rhs with updated constraint values.
511 //
512 // rhs = −[∇f − Aₑᵀy − Aᵢᵀ(−Σcᵢ + μS⁻¹e + z)]
513 // [ cₑˢᵒᶜ ]
514 //
515 // where cₑˢᵒᶜ = αc(xₖ) + c(xₖ + αpₖˣ)
516 c_e_soc = α_soc * c_e_soc + trial_c_e;
517 rhs.bottomRows(y.rows()) = -c_e_soc;
518
519 // Solve the Newton-KKT system
520 compute_step(soc_step);
521
522 // αˢᵒᶜ = max(α ∈ (0, 1] : sₖ + αpₖˢ ≥ (1−τⱼ)sₖ)
523 α_soc = fraction_to_the_boundary_rule<Scalar>(s, soc_step.p_s, τ);
524 trial_x = x + α_soc * soc_step.p_x;
525 trial_s = s + α_soc * soc_step.p_s;
526
527 // αₖᶻ = max(α ∈ (0, 1] : zₖ + αpₖᶻ ≥ (1−τⱼ)zₖ)
528 α_z_soc = fraction_to_the_boundary_rule<Scalar>(z, soc_step.p_z, τ);
529 trial_y = y + α_z_soc * soc_step.p_y;
530 trial_z = z + α_z_soc * soc_step.p_z;
531
532 trial_f = matrices.f(trial_x);
533 trial_c_e = matrices.c_e(trial_x);
534 trial_c_i = matrices.c_i(trial_x);
535
536 // Constraint violation scale factor for second-order corrections
537 constexpr Scalar κ_soc(0.99);
538
539 // If constraint violation hasn't been sufficiently reduced, stop
540 // making second-order corrections
541 next_constraint_violation =
542 trial_c_e.template lpNorm<1>() +
543 (trial_c_i - trial_s).template lpNorm<1>();
544 if (next_constraint_violation > κ_soc * prev_constraint_violation) {
545 break;
546 }
547
548 // Check whether filter accepts trial iterate
549 if (filter.try_add(
550 FilterEntry{trial_f, trial_s, trial_c_e, trial_c_i, μ}, α)) {
551 step = soc_step;
552 α = α_soc;
553 α_z = α_z_soc;
554 step_acceptable = true;
555 }
556 }
557
558 if (step_acceptable) {
559 // Accept step
560 break;
561 }
562 }
563
564 // If we got here and α is the full step, the full step was rejected.
565 // Increment the full-step rejected counter to keep track of how many full
566 // steps have been rejected in a row.
567 if (α == α_max) {
568 ++full_step_rejected_counter;
569 }
570
571 // If the full step was rejected enough times in a row, reset the filter
572 // because it may be impeding progress.
573 //
574 // See section 3.2 case I of [2].
575 if (full_step_rejected_counter >= 4 &&
576 filter.max_constraint_violation >
577 filter.back().constraint_violation / Scalar(10)) {
578 filter.max_constraint_violation *= Scalar(0.1);
579 filter.reset();
580 continue;
581 }
582
583 // Reduce step size
584 α *= α_reduction_factor;
585
586 // If step size hit a minimum, check if the KKT error was reduced. If it
587 // wasn't, report line search failure.
588 if (α < α_min) {
589 Scalar current_kkt_error =
590 kkt_error<Scalar>(g, A_e, c_e, A_i, c_i, s, y, z, μ);
591
592 trial_x = x + α_max * step.p_x;
593 trial_s = s + α_max * step.p_s;
594
595 trial_y = y + α_z * step.p_y;
596 trial_z = z + α_z * step.p_z;
597
598 trial_c_e = matrices.c_e(trial_x);
599 trial_c_i = matrices.c_i(trial_x);
600
601 Scalar next_kkt_error = kkt_error<Scalar>(
602 matrices.g(trial_x), matrices.A_e(trial_x), matrices.c_e(trial_x),
603 matrices.A_i(trial_x), trial_c_i, trial_s, trial_y, trial_z, μ);
604
605 // If the step using αᵐᵃˣ reduced the KKT error, accept it anyway
606 if (next_kkt_error <= Scalar(0.999) * current_kkt_error) {
607 α = α_max;
608
609 // Accept step
610 break;
611 }
612
613 return ExitStatus::LINE_SEARCH_FAILED;
614 }
615 }
616
617 line_search_profiler.stop();
618
619 // If full step was accepted, reset full-step rejected counter
620 if (α == α_max) {
621 full_step_rejected_counter = 0;
622 }
623
624 // xₖ₊₁ = xₖ + αₖpₖˣ
625 // sₖ₊₁ = sₖ + αₖpₖˢ
626 // yₖ₊₁ = yₖ + αₖᶻpₖʸ
627 // zₖ₊₁ = zₖ + αₖᶻpₖᶻ
628 x += α * step.p_x;
629 s += α * step.p_s;
630 y += α_z * step.p_y;
631 z += α_z * step.p_z;
632
633 // A requirement for the convergence proof is that the primal-dual barrier
634 // term Hessian Σₖ₊₁ does not deviate arbitrarily much from the primal
635 // barrier term Hessian μSₖ₊₁⁻².
636 //
637 // Σₖ₊₁ = μSₖ₊₁⁻²
638 // Sₖ₊₁⁻¹Zₖ₊₁ = μSₖ₊₁⁻²
639 // Zₖ₊₁ = μSₖ₊₁⁻¹
640 //
641 // We ensure this by resetting
642 //
643 // zₖ₊₁ = clamp(zₖ₊₁, 1/κ_Σ μ/sₖ₊₁, κ_Σ μ/sₖ₊₁)
644 //
645 // for some fixed κ_Σ ≥ 1 after each step. See equation (16) of [2].
646 for (int row = 0; row < z.rows(); ++row) {
647 constexpr Scalar κ_Σ(1e10);
648 z[row] =
649 std::clamp(z[row], Scalar(1) / κ_Σ * μ / s[row], κ_Σ * μ / s[row]);
650 }
651
652 // Update autodiff for Jacobians and Hessian
653 f = matrices.f(x);
654 A_e = matrices.A_e(x);
655 A_i = matrices.A_i(x);
656 g = matrices.g(x);
657 H = matrices.H(x, y, z);
658
659 ScopedProfiler next_iter_prep_profiler{next_iter_prep_prof};
660
661 c_e = matrices.c_e(x);
662 c_i = matrices.c_i(x);
663
664 // Update the error estimate
665 E_0 = error_estimate<Scalar>(g, A_e, c_e, A_i, c_i, s, y, z, Scalar(0));
666
667 // Update the barrier parameter if necessary
668 if (E_0 > Scalar(options.tolerance)) {
669 // Barrier parameter scale factor for tolerance checks
670 constexpr Scalar κ_ε(10);
671
672 // While the error estimate is below the desired threshold for this
673 // barrier parameter value, decrease the barrier parameter further
674 Scalar E_μ = error_estimate<Scalar>(g, A_e, c_e, A_i, c_i, s, y, z, μ);
675 while (μ > μ_min && E_μ <= κ_ε * μ) {
676 update_barrier_parameter_and_reset_filter();
677 E_μ = error_estimate<Scalar>(g, A_e, c_e, A_i, c_i, s, y, z, μ);
678 }
679 }
680
681 next_iter_prep_profiler.stop();
682 inner_iter_profiler.stop();
683
684 if (options.diagnostics) {
685 print_iteration_diagnostics(
686 iterations, IterationType::NORMAL,
687 inner_iter_profiler.current_duration(), E_0, f,
688 c_e.template lpNorm<1>() + (c_i - s).template lpNorm<1>(), s.dot(z),
689 μ, solver.hessian_regularization(), α, α_max, α_reduction_factor,
690 α_z);
691 }
692
693 ++iterations;
694
695 // Check for max iterations
696 if (iterations >= options.max_iterations) {
697 return ExitStatus::MAX_ITERATIONS_EXCEEDED;
698 }
699
700 // Check for max wall clock time
701 if (std::chrono::steady_clock::now() - solve_start_time > options.timeout) {
702 return ExitStatus::TIMEOUT;
703 }
704 }
705
706 return ExitStatus::SUCCESS;
707}
708
709extern template SLEIPNIR_DLLEXPORT ExitStatus
710interior_point(const InteriorPointMatrixCallbacks<double>& matrix_callbacks,
711 std::span<std::function<bool(const IterationInfo<double>& info)>>
712 iteration_callbacks,
713 const Options& options,
714#ifdef SLEIPNIR_ENABLE_BOUND_PROJECTION
715 const Eigen::ArrayX<bool>& bound_constraint_mask,
716#endif
717 Eigen::Vector<double, Eigen::Dynamic>& x);
718
719} // namespace slp