12#include <Eigen/SparseCore>
13#include <gch/small_vector.hpp>
15#include "sleipnir/optimization/solver/exit_status.hpp"
16#include "sleipnir/optimization/solver/ipm_matrix_callbacks.hpp"
17#include "sleipnir/optimization/solver/iteration_info.hpp"
18#include "sleipnir/optimization/solver/options.hpp"
19#include "sleipnir/optimization/solver/util/all_finite.hpp"
20#include "sleipnir/optimization/solver/util/append_as_triplets.hpp"
21#include "sleipnir/optimization/solver/util/feasibility_restoration.hpp"
22#include "sleipnir/optimization/solver/util/filter.hpp"
23#include "sleipnir/optimization/solver/util/fraction_to_the_boundary_rule.hpp"
24#include "sleipnir/optimization/solver/util/kkt_error.hpp"
25#include "sleipnir/optimization/solver/util/kkt_solver.hpp"
26#include "sleipnir/util/assert.hpp"
27#include "sleipnir/util/print_diagnostics.hpp"
28#include "sleipnir/util/profiler.hpp"
29#include "sleipnir/util/scope_exit.hpp"
30#include "sleipnir/util/symbol_exports.hpp"
61template <
typename Scalar>
62ExitStatus ipm(
const IPMMatrixCallbacks<Scalar>& matrix_callbacks,
63 std::span<std::function<
bool(
const IterationInfo<Scalar>& info)>>
65 const Options& options,
66#ifdef SLEIPNIR_ENABLE_BOUND_PROJECTION
67 const Eigen::ArrayX<bool>& bound_constraint_mask,
69 Eigen::Vector<Scalar, Eigen::Dynamic>& x) {
70 using DenseVector = Eigen::Vector<Scalar, Eigen::Dynamic>;
73 DenseVector::Ones(matrix_callbacks.num_inequality_constraints);
74 DenseVector y = DenseVector::Zero(matrix_callbacks.num_equality_constraints);
76 DenseVector::Ones(matrix_callbacks.num_inequality_constraints);
77 Scalar μ = Scalar(0.1) * matrix_callbacks.scaling.f;
80 return ipm(matrix_callbacks, iteration_callbacks, options,
false,
81#ifdef SLEIPNIR_ENABLE_BOUND_PROJECTION
82 bound_constraint_mask,
84 x, s, y, z, μ, iterations);
120template <
typename Scalar>
121ExitStatus ipm(
const IPMMatrixCallbacks<Scalar>& matrix_callbacks,
122 std::span<std::function<
bool(
const IterationInfo<Scalar>& info)>>
124 const Options& options,
bool in_feasibility_restoration,
125#ifdef SLEIPNIR_ENABLE_BOUND_PROJECTION
126 const Eigen::ArrayX<bool>& bound_constraint_mask,
128 Eigen::Vector<Scalar, Eigen::Dynamic>& x,
129 Eigen::Vector<Scalar, Eigen::Dynamic>& s,
130 Eigen::Vector<Scalar, Eigen::Dynamic>& y,
131 Eigen::Vector<Scalar, Eigen::Dynamic>& z, Scalar& μ,
133 using DenseVector = Eigen::Vector<Scalar, Eigen::Dynamic>;
134 using SparseMatrix = Eigen::SparseMatrix<Scalar>;
135 using SparseVector = Eigen::SparseVector<Scalar>;
151 const auto solve_start_time = std::chrono::steady_clock::now();
153 gch::small_vector<SolveProfiler> solve_profilers;
154 solve_profilers.emplace_back(
"solver");
155 solve_profilers.emplace_back(
"↳ setup");
156 solve_profilers.emplace_back(
"↳ iteration");
157 solve_profilers.emplace_back(
" ↳ callbacks");
158 solve_profilers.emplace_back(
" ↳ KKT matrix build");
159 solve_profilers.emplace_back(
" ↳ KKT matrix decomp");
160 solve_profilers.emplace_back(
" ↳ KKT system solve");
161 solve_profilers.emplace_back(
" ↳ line search");
162 solve_profilers.emplace_back(
" ↳ SOC");
163 solve_profilers.emplace_back(
" ↳ feas. restoration");
164 solve_profilers.emplace_back(
" ↳ f(x)");
165 solve_profilers.emplace_back(
" ↳ ∇f(x)");
166 solve_profilers.emplace_back(
" ↳ ∇²ₓₓL");
167 solve_profilers.emplace_back(
" ↳ ∇²ₓₓL_c");
168 solve_profilers.emplace_back(
" ↳ cₑ(x)");
169 solve_profilers.emplace_back(
" ↳ ∂cₑ/∂x");
170 solve_profilers.emplace_back(
" ↳ cᵢ(x)");
171 solve_profilers.emplace_back(
" ↳ ∂cᵢ/∂x");
173 auto& solver_prof = solve_profilers[0];
174 auto& setup_prof = solve_profilers[1];
175 auto& inner_iter_prof = solve_profilers[2];
176 auto& iter_callbacks_prof = solve_profilers[3];
177 auto& kkt_matrix_build_prof = solve_profilers[4];
178 auto& kkt_matrix_decomp_prof = solve_profilers[5];
179 auto& kkt_system_solve_prof = solve_profilers[6];
180 auto& line_search_prof = solve_profilers[7];
181 auto& soc_prof = solve_profilers[8];
182 auto& feasibility_restoration_prof = solve_profilers[9];
185#ifndef SLEIPNIR_DISABLE_DIAGNOSTICS
186 auto& f_prof = solve_profilers[10];
187 auto& g_prof = solve_profilers[11];
188 auto& H_prof = solve_profilers[12];
189 auto& H_c_prof = solve_profilers[13];
190 auto& c_e_prof = solve_profilers[14];
191 auto& A_e_prof = solve_profilers[15];
192 auto& c_i_prof = solve_profilers[16];
193 auto& A_i_prof = solve_profilers[17];
195 IPMMatrixCallbacks<Scalar> matrices{
196 matrix_callbacks.num_decision_variables,
197 matrix_callbacks.num_equality_constraints,
198 matrix_callbacks.num_inequality_constraints,
199 [&](
const DenseVector& x) -> Scalar {
200 ScopedProfiler prof{f_prof};
201 return matrix_callbacks.f(x);
203 [&](
const DenseVector& x) -> SparseVector {
204 ScopedProfiler prof{g_prof};
205 return matrix_callbacks.g(x);
207 [&](
const DenseVector& x,
const DenseVector& y,
208 const DenseVector& z) -> SparseMatrix {
209 ScopedProfiler prof{H_prof};
210 return matrix_callbacks.H(x, y, z);
212 [&](
const DenseVector& x,
const DenseVector& y,
213 const DenseVector& z) -> SparseMatrix {
214 ScopedProfiler prof{H_c_prof};
215 return matrix_callbacks.H_c(x, y, z);
217 [&](
const DenseVector& x) -> DenseVector {
218 ScopedProfiler prof{c_e_prof};
219 return matrix_callbacks.c_e(x);
221 [&](
const DenseVector& x) -> SparseMatrix {
222 ScopedProfiler prof{A_e_prof};
223 return matrix_callbacks.A_e(x);
225 [&](
const DenseVector& x) -> DenseVector {
226 ScopedProfiler prof{c_i_prof};
227 return matrix_callbacks.c_i(x);
229 [&](
const DenseVector& x) -> SparseMatrix {
230 ScopedProfiler prof{A_i_prof};
231 return matrix_callbacks.A_i(x);
233 matrix_callbacks.scaling};
235 const auto& matrices = matrix_callbacks;
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);
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);
266 DenseVector trial_c_e;
267 DenseVector trial_c_i;
270 if (matrices.num_equality_constraints > matrices.num_decision_variables) {
271 if (options.diagnostics) {
272 print_too_few_dofs_error(c_e);
275 return ExitStatus::TOO_FEW_DOFS;
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;
284#ifdef SLEIPNIR_ENABLE_BOUND_PROJECTION
286 s = bound_constraint_mask.select(c_i, s);
291 matrices.scaling.f * Scalar(options.tolerance) / Scalar(10);
294 constexpr Scalar τ_min(0.99);
299 Filter<Scalar> filter{c_e.template lpNorm<1>() +
300 (c_i - s).
template lpNorm<1>()};
304 auto update_barrier_parameter_and_reset_filter = [&] {
306 constexpr Scalar κ_μ(0.2);
310 constexpr Scalar θ_μ(1.5);
318 μ = std::max(μ_min, std::min(κ_μ * μ, pow(μ, θ_μ)));
325 τ = std::max(τ_min, Scalar(1) - μ);
332 gch::small_vector<Eigen::Triplet<Scalar>> triplets;
335 matrices.num_decision_variables + matrices.num_equality_constraints;
336 KKTSolver<Scalar> solver{
339 (A_i.transpose() * A_i)
340 .
template triangularView<Eigen::Lower>()
344 0.25 * lhs_rows * lhs_rows,
345 matrices.num_decision_variables, matrices.num_equality_constraints,
348 in_feasibility_restoration ? Scalar(0) : Scalar(1e-10)};
351 constexpr Scalar α_reduction_factor(0.5);
352 constexpr Scalar α_min(1e-7);
354 int full_step_rejected_counter = 0;
357 Scalar E_0 = unscaled_kkt_error<Scalar, KKTErrorType::INF_NORM_SCALED>(
358 matrices.scaling, g, A_e, c_e, A_i, c_i, s, y, z, Scalar(0));
363 scope_exit exit{[&] {
364 if (options.diagnostics) {
367 if (in_feasibility_restoration) {
371 if (iterations > 0) {
372 print_bottom_iteration_diagnostics();
374 print_solver_diagnostics(solve_profilers);
378 while (E_0 > Scalar(options.tolerance)) {
379 ScopedProfiler inner_iter_profiler{inner_iter_prof};
382 if (x.template lpNorm<Eigen::Infinity>() > Scalar(1e10) || !x.allFinite() ||
383 s.template lpNorm<Eigen::Infinity>() > Scalar(1e10) || !s.allFinite()) {
384 return ExitStatus::DIVERGING_ITERATES;
387 ScopedProfiler iter_callbacks_profiler{iter_callbacks_prof};
390 for (
const auto& callback : iteration_callbacks) {
391 if (callback({iterations, x, s, y, z, g, H, A_e, A_i})) {
392 return ExitStatus::CALLBACK_REQUESTED_STOP;
396 iter_callbacks_profiler.stop();
397 ScopedProfiler kkt_matrix_build_profiler{kkt_matrix_build_prof};
402 const SparseMatrix Σ{s.cwiseInverse().asDiagonal() * z.asDiagonal()};
408 const SparseMatrix top_left =
409 H + (A_i.transpose() * Σ * A_i).
template triangularView<Eigen::Lower>();
411 triplets.reserve(top_left.nonZeros() + A_e.nonZeros());
412 append_as_triplets(triplets, 0, 0, {top_left, A_e});
414 matrices.num_decision_variables + matrices.num_equality_constraints,
415 matrices.num_decision_variables + matrices.num_equality_constraints);
416 lhs.setFromSortedTriplets(triplets.begin(), triplets.end());
420 DenseVector rhs{x.rows() + y.rows()};
421 rhs.segment(0, x.rows()) =
422 -g + A_e.transpose() * y +
423 A_i.transpose() * (-Σ * c_i + μ * s.cwiseInverse() + z);
424 rhs.segment(x.rows(), y.rows()) = -c_e;
426 kkt_matrix_build_profiler.stop();
427 ScopedProfiler kkt_matrix_decomp_profiler{kkt_matrix_decomp_prof};
433 bool call_feasibility_restoration =
false;
439 if (solver.compute(lhs).info() != Eigen::Success) [[unlikely]] {
440 return ExitStatus::FACTORIZATION_FAILED;
443 kkt_matrix_decomp_profiler.stop();
444 ScopedProfiler kkt_system_solve_profiler{kkt_system_solve_prof};
446 auto compute_step = [&](Step& step,
const DenseVector& c_i_minus_s) {
449 DenseVector p = solver.solve(rhs);
450 step.p_x = p.segment(0, x.rows());
451 step.p_y = -p.segment(x.rows(), y.rows());
455 step.p_s = c_i_minus_s + A_i * step.p_x;
456 step.p_z = μ * s.cwiseInverse() - z - Σ * step.p_s;
458 compute_step(step, c_i - s);
460 kkt_system_solve_profiler.stop();
461 ScopedProfiler line_search_profiler{line_search_prof};
464 α_max = fraction_to_the_boundary_rule<Scalar>(s, step.p_s, τ);
469 call_feasibility_restoration =
true;
473 α_z = fraction_to_the_boundary_rule<Scalar>(z, step.p_z, τ);
475 const FilterEntry<Scalar> current_entry{f, s, c_e, c_i, μ};
485 g.transpose() * step.p_x - μ * s.cwiseInverse().dot(step.p_s);
489 trial_x = x + α * step.p_x;
490 trial_c_i = matrices.c_i(trial_x);
491 if (options.feasible_ipm && c_i.cwiseGreater(Scalar(0)).all()) {
498 trial_s = s + α * step.p_s;
500 trial_y = y + α_z * step.p_y;
501 trial_z = z + α_z * step.p_z;
503 trial_f = matrices.f(trial_x);
504 trial_c_e = matrices.c_e(trial_x);
508 if (!isfinite(trial_f) || !trial_c_e.allFinite() ||
509 !trial_c_i.allFinite()) {
511 α *= α_reduction_factor;
514 call_feasibility_restoration =
true;
521 FilterEntry trial_entry{trial_f, trial_s, trial_c_e, trial_c_i, μ};
522 if (filter.try_add(current_entry, trial_entry, D_ϕ, α)) {
527 Scalar prev_constraint_violation =
528 c_e.template lpNorm<1>() + (c_i - s).
template lpNorm<1>();
529 Scalar next_constraint_violation =
530 trial_c_e.template lpNorm<1>() +
531 (trial_c_i - trial_s).
template lpNorm<1>();
538 next_constraint_violation >= prev_constraint_violation) {
540 auto soc_step = step;
543 Scalar α_z_soc = α_z;
544 DenseVector c_e_soc = c_e;
545 DenseVector c_i_minus_s_soc = c_i - s;
547 Scalar soc_constraint_violation = next_constraint_violation;
549 bool step_acceptable =
false;
550 for (
int soc_iteration = 0; soc_iteration < 5 && !step_acceptable;
552 ScopedProfiler soc_profiler{soc_prof};
554 scope_exit soc_exit{[&] {
557 if (options.diagnostics && step_acceptable) {
558 print_iteration_diagnostics(
559 iterations, IterationType::SECOND_ORDER_CORRECTION,
560 soc_profiler.current_duration(),
561 unscaled_kkt_error<Scalar, KKTErrorType::INF_NORM_SCALED>(
562 matrices.scaling, g, A_e, trial_c_e, A_i, trial_c_i,
563 trial_s, trial_y, trial_z, Scalar(0)),
565 trial_c_e.template lpNorm<1>() +
566 (trial_c_i - trial_s).template lpNorm<1>(),
567 trial_s.dot(trial_z), μ, solver.hessian_regularization(),
568 solver.constraint_jacobian_regularization(),
569 std::max(soc_step.p_x.template lpNorm<Eigen::Infinity>(),
570 soc_step.p_s.template lpNorm<Eigen::Infinity>()),
571 std::max(soc_step.p_y.template lpNorm<Eigen::Infinity>(),
572 soc_step.p_z.template lpNorm<Eigen::Infinity>()),
573 α_soc, Scalar(1), α_reduction_factor, α_z_soc);
587 c_e_soc = α_soc * c_e_soc + trial_c_e;
588 c_i_minus_s_soc = α_soc * c_i_minus_s_soc + trial_c_i - trial_s;
589 rhs.segment(0, x.rows()) =
590 -g + A_e.transpose() * y +
591 A_i.transpose() * (μ * s.cwiseInverse() - Σ * c_i_minus_s_soc);
592 rhs.segment(x.rows(), y.rows()) = -c_e_soc;
595 compute_step(soc_step, c_i_minus_s_soc);
599 α_soc = fraction_to_the_boundary_rule<Scalar>(s, soc_step.p_s, τ);
600 α_z_soc = fraction_to_the_boundary_rule<Scalar>(z, soc_step.p_z, τ);
602 trial_x = x + α_soc * soc_step.p_x;
603 trial_s = s + α_soc * soc_step.p_s;
604 trial_y = y + α_z_soc * soc_step.p_y;
605 trial_z = z + α_z_soc * soc_step.p_z;
607 trial_f = matrices.f(trial_x);
608 trial_c_e = matrices.c_e(trial_x);
609 trial_c_i = matrices.c_i(trial_x);
612 FilterEntry trial_entry{trial_f, trial_s, trial_c_e, trial_c_i, μ};
613 if (filter.try_add(current_entry, trial_entry, D_ϕ, α)) {
617 step_acceptable =
true;
622 constexpr Scalar κ_soc(0.99);
626 next_constraint_violation =
627 trial_c_e.template lpNorm<1>() +
628 (trial_c_i - trial_s).
template lpNorm<1>();
629 if (next_constraint_violation > κ_soc * soc_constraint_violation) {
633 soc_constraint_violation = next_constraint_violation;
636 if (step_acceptable) {
646 ++full_step_rejected_counter;
653 if (full_step_rejected_counter >= 4 &&
654 filter.max_constraint_violation >
655 current_entry.constraint_violation / Scalar(10) &&
656 filter.last_rejection_due_to_filter()) {
657 filter.max_constraint_violation *= Scalar(0.1);
663 α *= α_reduction_factor;
668 Scalar current_kkt_error = kkt_error<Scalar, KKTErrorType::ONE_NORM>(
669 g, A_e, c_e, A_i, c_i, s, y, z, μ);
671 trial_x = x + α_max * step.p_x;
672 trial_s = s + α_max * step.p_s;
673 trial_y = y + α_z * step.p_y;
674 trial_z = z + α_z * step.p_z;
676 trial_f = matrices.f(trial_x);
677 trial_c_e = matrices.c_e(trial_x);
678 trial_c_i = matrices.c_i(trial_x);
680 Scalar next_kkt_error = kkt_error<Scalar, KKTErrorType::ONE_NORM>(
681 matrices.g(trial_x), matrices.A_e(trial_x), trial_c_e,
682 matrices.A_i(trial_x), trial_c_i, trial_s, trial_y, trial_z, μ);
685 if (next_kkt_error <= Scalar(0.999) * current_kkt_error) {
690 call_feasibility_restoration =
true;
695 line_search_profiler.stop();
697 if (call_feasibility_restoration) {
698 ScopedProfiler feasibility_restoration_profiler{
699 feasibility_restoration_prof};
702 if (in_feasibility_restoration) {
703 return ExitStatus::FEASIBILITY_RESTORATION_FAILED;
706 FilterEntry initial_entry{matrices.f(x), s, c_e, c_i, μ};
709 gch::small_vector<std::function<bool(
const IterationInfo<Scalar>& info)>>
711 for (
auto& callback : iteration_callbacks) {
712 callbacks.emplace_back(callback);
714 callbacks.emplace_back([&](
const IterationInfo<Scalar>& info) {
715 DenseVector trial_x =
716 info.x.segment(0, matrices.num_decision_variables);
717 DenseVector trial_s =
718 info.s.segment(0, matrices.num_inequality_constraints);
720 DenseVector trial_c_e = matrices.c_e(trial_x);
721 DenseVector trial_c_i = matrices.c_i(trial_x);
725 FilterEntry trial_entry{matrices.f(trial_x), trial_s, trial_c_e,
727 const Scalar D_ϕ_restoration = g.transpose() * (trial_x - x) -
728 μ * s.cwiseInverse().dot(trial_s - s);
729 return trial_entry.constraint_violation <
730 Scalar(0.9) * initial_entry.constraint_violation &&
731 filter.try_add(initial_entry, trial_entry, D_ϕ_restoration, α);
734 feasibility_restoration<Scalar>(matrices, callbacks, options,
735#ifdef SLEIPNIR_ENABLE_BOUND_PROJECTION
736 bound_constraint_mask,
738 x, s, y, z, μ, iterations);
740 if (status != ExitStatus::SUCCESS) {
746 c_e = matrices.c_e(x);
747 c_i = matrices.c_i(x);
751 full_step_rejected_counter = 0;
773 for (
int row = 0; row < z.rows(); ++row) {
774 constexpr Scalar κ_Σ(1e10);
776 std::clamp(z[row], Scalar(1) / κ_Σ * μ / s[row], κ_Σ * μ / s[row]);
785 A_e = matrices.A_e(x);
786 A_i = matrices.A_i(x);
788 H = matrices.H(x, y, z);
791 E_0 = unscaled_kkt_error<Scalar, KKTErrorType::INF_NORM_SCALED>(
792 matrices.scaling, g, A_e, c_e, A_i, c_i, s, y, z, Scalar(0));
795 if (E_0 > Scalar(options.tolerance)) {
797 constexpr Scalar κ_ε(10);
801 Scalar E_μ = kkt_error<Scalar, KKTErrorType::INF_NORM_SCALED>(
802 g, A_e, c_e, A_i, c_i, s, y, z, μ);
803 while (μ > μ_min && E_μ <= κ_ε * μ) {
804 update_barrier_parameter_and_reset_filter();
805 E_μ = kkt_error<Scalar, KKTErrorType::INF_NORM_SCALED>(g, A_e, c_e, A_i,
810 inner_iter_profiler.stop();
812 if (options.diagnostics) {
813 print_iteration_diagnostics(
815 in_feasibility_restoration ? IterationType::FEASIBILITY_RESTORATION
816 : IterationType::NORMAL,
817 inner_iter_profiler.current_duration(), E_0, f,
818 c_e.template lpNorm<1>() + (c_i - s).template lpNorm<1>(), s.dot(z),
819 μ, solver.hessian_regularization(),
820 solver.constraint_jacobian_regularization(),
821 std::max(step.p_x.template lpNorm<Eigen::Infinity>(),
822 step.p_s.template lpNorm<Eigen::Infinity>()),
823 std::max(step.p_y.template lpNorm<Eigen::Infinity>(),
824 step.p_z.template lpNorm<Eigen::Infinity>()),
825 α, α_max, α_reduction_factor, α_z);
831 if (iterations >= options.max_iterations) {
832 return ExitStatus::MAX_ITERATIONS_EXCEEDED;
836 if (std::chrono::steady_clock::now() - solve_start_time > options.timeout) {
837 return ExitStatus::TIMEOUT;
841 if (!isfinite(E_0)) {
842 return ExitStatus::DIVERGING_ITERATES;
844 return ExitStatus::SUCCESS;
848extern template SLEIPNIR_DLLEXPORT ExitStatus
849ipm(
const IPMMatrixCallbacks<double>& matrix_callbacks,
850 std::span<std::function<
bool(
const IterationInfo<double>& info)>>
852 const Options& options,
853#ifdef SLEIPNIR_ENABLE_BOUND_PROJECTION
854 const Eigen::ArrayX<bool>& bound_constraint_mask,
856 Eigen::Vector<double, Eigen::Dynamic>& x);