14#include <Eigen/SparseCore>
15#include <gch/small_vector.hpp>
17#include "sleipnir/optimization/solver/exit_status.hpp"
18#include "sleipnir/optimization/solver/ipm_matrix_callbacks.hpp"
19#include "sleipnir/optimization/solver/iteration_info.hpp"
20#include "sleipnir/optimization/solver/options.hpp"
21#include "sleipnir/optimization/solver/util/all_finite.hpp"
22#include "sleipnir/optimization/solver/util/append_as_triplets.hpp"
23#include "sleipnir/optimization/solver/util/constraint_violation.hpp"
24#include "sleipnir/optimization/solver/util/feasibility_restoration.hpp"
25#include "sleipnir/optimization/solver/util/feasibility_restoration_cycle_detector.hpp"
26#include "sleipnir/optimization/solver/util/filter.hpp"
27#include "sleipnir/optimization/solver/util/fraction_to_the_boundary_rule.hpp"
28#include "sleipnir/optimization/solver/util/kkt_error.hpp"
29#include "sleipnir/optimization/solver/util/kkt_solver.hpp"
30#include "sleipnir/optimization/solver/util/time_trace.hpp"
31#include "sleipnir/util/assert.hpp"
32#include "sleipnir/util/print_diagnostics.hpp"
33#include "sleipnir/util/scope_exit.hpp"
34#include "sleipnir/util/symbol_exports.hpp"
35#include "sleipnir/util/time_tracer.hpp"
66template <
typename Scalar>
67ExitStatus ipm(
const IPMMatrixCallbacks<Scalar>& matrix_callbacks,
68 std::span<std::function<
bool(
const IterationInfo<Scalar>& info)>>
70 const Options<Scalar>& options,
71#ifdef SLEIPNIR_ENABLE_BOUND_PROJECTION
72 const Eigen::ArrayX<bool>& bound_constraint_mask,
74 Eigen::VectorX<Scalar>& x) {
75 using DenseVector = Eigen::VectorX<Scalar>;
79 DenseVector c_i = matrix_callbacks.c_i(x);
80 DenseVector s = (c_i.array() >= Scalar(1e-2)).select(c_i, Scalar(1));
82 DenseVector y = DenseVector::Zero(matrix_callbacks.num_equality_constraints);
84 DenseVector::Ones(matrix_callbacks.num_inequality_constraints);
85 Scalar μ = Scalar(0.1) * matrix_callbacks.scaling.f;
88 return ipm(matrix_callbacks, iteration_callbacks, options,
false,
89#ifdef SLEIPNIR_ENABLE_BOUND_PROJECTION
90 bound_constraint_mask,
92 x, s, y, z, μ, iterations);
128template <
typename Scalar>
129ExitStatus ipm(
const IPMMatrixCallbacks<Scalar>& matrix_callbacks,
130 std::span<std::function<
bool(
const IterationInfo<Scalar>& info)>>
132 const Options<Scalar>& options,
bool in_feasibility_restoration,
133#ifdef SLEIPNIR_ENABLE_BOUND_PROJECTION
134 const Eigen::ArrayX<bool>& bound_constraint_mask,
136 Eigen::VectorX<Scalar>& x, Eigen::VectorX<Scalar>& s,
137 Eigen::VectorX<Scalar>& y, Eigen::VectorX<Scalar>& z, Scalar& μ,
139 using DenseVector = Eigen::VectorX<Scalar>;
140 using SparseMatrix = Eigen::SparseMatrix<Scalar>;
141 using SparseVector = Eigen::SparseVector<Scalar>;
157 Scalar primal_inf_norm()
const {
158 return std::max(p_x.template lpNorm<Eigen::Infinity>(),
159 p_s.template lpNorm<Eigen::Infinity>());
165 Scalar dual_inf_norm()
const {
166 return std::max(p_y.template lpNorm<Eigen::Infinity>(),
167 p_z.template lpNorm<Eigen::Infinity>());
177 Scalar tiny_step_size(
const DenseVector& x,
const DenseVector& s)
const {
192 return Scalar(10) * std::numeric_limits<Scalar>::epsilon() /
193 max((p_x.array().abs() / (Scalar(1) + x.array().abs()))
195 .template lpNorm<Eigen::Infinity>(),
196 (p_s.array().abs() / (Scalar(1) + s.array().abs()))
198 .template lpNorm<Eigen::Infinity>());
204 const auto solve_start_time = std::chrono::steady_clock::now();
206 using enum SolveTimeTracerID;
212 SolveDiagnostics<ipm_time_trace> diagnostics{options.diagnostics,
213 !in_feasibility_restoration};
217#ifndef SLEIPNIR_DISABLE_DIAGNOSTICS
218 std::optional<IPMMatrixCallbacks<Scalar>> traced_matrices;
219 if (options.diagnostics && !in_feasibility_restoration) {
220 traced_matrices.emplace(
221 matrix_callbacks.num_decision_variables,
222 matrix_callbacks.num_equality_constraints,
223 matrix_callbacks.num_inequality_constraints,
224 traced(diagnostics[COST], matrix_callbacks.f),
225 traced(diagnostics[GRADIENT], matrix_callbacks.g),
226 traced(diagnostics[HESSIAN], matrix_callbacks.H),
227 traced(diagnostics[HESSIAN_CONSTRAINTS], matrix_callbacks.H_c),
228 traced(diagnostics[EQUALITY_CONSTRAINTS], matrix_callbacks.c_e),
229 traced(diagnostics[EQUALITY_JACOBIAN], matrix_callbacks.A_e),
230 traced(diagnostics[INEQUALITY_CONSTRAINTS], matrix_callbacks.c_i),
231 traced(diagnostics[INEQUALITY_JACOBIAN], matrix_callbacks.A_i),
232 matrix_callbacks.scaling);
234 const auto& matrices = traced_matrices ? *traced_matrices : matrix_callbacks;
236 const auto& matrices = matrix_callbacks;
239 ScopedTimeTracer setup_tracer{diagnostics[SETUP]};
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);
290 const Scalar μ_min = matrices.scaling.f * options.tolerance / Scalar(10);
293 constexpr Scalar τ_min(0.99);
298 Filter<Scalar> filter{constraint_violation(c_e, c_i, s)};
304 Scalar fallback_kkt_error = std::numeric_limits<Scalar>::infinity();
308 auto update_barrier_parameter_and_reset_filter = [&] {
310 constexpr Scalar κ_μ(0.2);
314 constexpr Scalar θ_μ(1.5);
322 μ = std::max(μ_min, std::min(κ_μ * μ, pow(μ, θ_μ)));
329 τ = std::max(τ_min, Scalar(1) - μ);
333 fallback_kkt_error = std::numeric_limits<Scalar>::infinity();
337 gch::small_vector<Eigen::Triplet<Scalar>> triplets;
340 matrices.num_decision_variables + matrices.num_equality_constraints;
341 KKTSolver<Scalar> solver{
344 (A_i.transpose() * A_i)
345 .
template triangularView<Eigen::Lower>()
349 0.25 * lhs_rows * lhs_rows,
350 matrices.num_decision_variables, matrices.num_equality_constraints,
353 in_feasibility_restoration ? Scalar(0) : Scalar(1e-10)};
356 constexpr Scalar α_reduction_factor(0.5);
358 int full_step_rejected_counter = 0;
360 StallDetector<Scalar> stall_detector;
362 FeasibilityRestorationCycleDetector<Scalar>
363 feasibility_restoration_cycle_detector;
366 Scalar E_0 = unscaled_kkt_error<Scalar, KKTErrorType::INF_NORM_SCALED>(
367 matrices.scaling, g, A_e, c_e, A_i, c_i, s, y, z, Scalar(0),
377 IterateDiagnosticsTable table{
378 options.diagnostics && !in_feasibility_restoration,
381 constraint_violation(c_e, c_i, s),
385 while (E_0 > options.tolerance) {
386 ScopedTimeTracer inner_iter_tracer{diagnostics[ITERATION]};
389 if (x.template lpNorm<Eigen::Infinity>() > Scalar(1e10) || !x.allFinite() ||
390 s.template lpNorm<Eigen::Infinity>() > Scalar(1e10) || !s.allFinite()) {
391 return ExitStatus::DIVERGING_ITERATES;
394 PhaseTimeTracer phase_tracer{diagnostics[CALLBACKS]};
397 for (
const auto& callback : iteration_callbacks) {
398 if (callback({iterations, x, s, y, z, g, H, A_e, A_i})) {
399 return ExitStatus::CALLBACK_REQUESTED_STOP;
403 phase_tracer.next(diagnostics[KKT_MATRIX_BUILD]);
408 const SparseMatrix Σ{s.cwiseInverse().asDiagonal() * z.asDiagonal()};
414 const SparseMatrix top_left =
415 H + (A_i.transpose() * Σ * A_i).
template triangularView<Eigen::Lower>();
417 triplets.reserve(top_left.nonZeros() + A_e.nonZeros());
418 append_as_triplets(triplets, 0, 0, {top_left, A_e});
420 matrices.num_decision_variables + matrices.num_equality_constraints,
421 matrices.num_decision_variables + matrices.num_equality_constraints);
422 lhs.setFromSortedTriplets(triplets.begin(), triplets.end());
426 DenseVector rhs{x.rows() + y.rows()};
427 rhs.segment(0, x.rows()) =
428 -g + A_e.transpose() * y +
429 A_i.transpose() * (-Σ * c_i + μ * s.cwiseInverse() + z);
430 rhs.segment(x.rows(), y.rows()) = -c_e;
432 phase_tracer.next(diagnostics[KKT_MATRIX_DECOMP]);
438 bool call_feasibility_restoration =
false;
444 if (solver.compute(lhs).info() != Eigen::Success) [[unlikely]] {
445 return ExitStatus::FACTORIZATION_FAILED;
448 phase_tracer.next(diagnostics[KKT_SYSTEM_SOLVE]);
450 auto compute_step = [&](Step& step,
const DenseVector& c_i_minus_s) {
453 DenseVector p = solver.solve(rhs);
454 step.p_x = p.segment(0, x.rows());
455 step.p_y = -p.segment(x.rows(), y.rows());
459 step.p_s = c_i_minus_s + A_i * step.p_x;
460 step.p_z = μ * s.cwiseInverse() - z - Σ * step.p_s;
462 compute_step(step, c_i - s);
464 phase_tracer.next(diagnostics[LINE_SEARCH]);
467 α_max = fraction_to_the_boundary_rule<Scalar>(s, step.p_s, τ);
471 α_z = fraction_to_the_boundary_rule<Scalar>(z, step.p_z, τ);
473 const FilterEntry<Scalar> current_entry{f, s, c_e, c_i, μ};
483 g.transpose() * step.p_x - μ * s.cwiseInverse().dot(step.p_s);
485 const Scalar α_min = filter.min_step_size(current_entry, D_ϕ);
489 call_feasibility_restoration =
true;
495 const Scalar α_tiny = step.tiny_step_size(x, s);
498 while (!call_feasibility_restoration) {
499 trial_x = x + α * step.p_x;
500 trial_c_i = matrices.c_i(trial_x);
501 if (options.feasible_ipm && c_i.cwiseGreater(Scalar(0)).all()) {
508 trial_s = s + α * step.p_s;
510 trial_y = y + α * step.p_y;
511 trial_z = z + α_z * step.p_z;
513 trial_f = matrices.f(trial_x);
514 trial_c_e = matrices.c_e(trial_x);
518 if (!isfinite(trial_f) || !trial_c_e.allFinite() ||
519 !trial_c_i.allFinite()) {
521 α *= α_reduction_factor;
523 if (α < α_min || α < α_tiny) {
524 call_feasibility_restoration =
true;
531 FilterEntry trial_entry{trial_f, trial_s, trial_c_e, trial_c_i, μ};
532 if (filter.try_add(current_entry, trial_entry, D_ϕ, α)) {
537 Scalar prev_constraint_violation = constraint_violation(c_e, c_i, s);
538 Scalar next_constraint_violation =
539 constraint_violation(trial_c_e, trial_c_i, trial_s);
546 next_constraint_violation >= prev_constraint_violation) {
548 auto soc_step = step;
551 Scalar α_z_soc = α_z;
552 DenseVector c_e_soc = c_e;
553 DenseVector c_i_minus_s_soc = c_i - s;
555 Scalar soc_constraint_violation = next_constraint_violation;
557 bool step_acceptable =
false;
558 for (
int soc_iteration = 0; soc_iteration < 5 && !step_acceptable;
560 ScopedTimeTracer soc_tracer{diagnostics[SOC]};
562 scope_exit soc_exit{[&] {
565 if (options.diagnostics && step_acceptable) {
566 print_iterate_diagnostics(
567 iterations, IterationType::SECOND_ORDER_CORRECTION,
568 soc_tracer.current_duration(),
569 unscaled_kkt_error<Scalar, KKTErrorType::INF_NORM_SCALED>(
570 matrices.scaling, g, A_e, trial_c_e, A_i, trial_c_i,
571 trial_s, trial_y, trial_z, Scalar(0), options.tolerance),
572 trial_f, constraint_violation(trial_c_e, trial_c_i, trial_s),
573 trial_s.dot(trial_z), μ, solver.hessian_regularization(),
574 solver.constraint_jacobian_regularization(),
575 soc_step.primal_inf_norm(), soc_step.dual_inf_norm(), α_soc,
576 Scalar(1), α_reduction_factor, α_z_soc);
590 c_e_soc = α_soc * c_e_soc + trial_c_e;
591 c_i_minus_s_soc = α_soc * c_i_minus_s_soc + trial_c_i - trial_s;
592 rhs.segment(0, x.rows()) =
593 -g + A_e.transpose() * y +
594 A_i.transpose() * (μ * s.cwiseInverse() - Σ * c_i_minus_s_soc);
595 rhs.segment(x.rows(), y.rows()) = -c_e_soc;
598 compute_step(soc_step, c_i_minus_s_soc);
602 α_soc = fraction_to_the_boundary_rule<Scalar>(s, soc_step.p_s, τ);
603 α_z_soc = fraction_to_the_boundary_rule<Scalar>(z, soc_step.p_z, τ);
605 trial_x = x + α_soc * soc_step.p_x;
606 trial_s = s + α_soc * soc_step.p_s;
607 trial_y = y + α_soc * soc_step.p_y;
608 trial_z = z + α_z_soc * soc_step.p_z;
610 trial_f = matrices.f(trial_x);
611 trial_c_e = matrices.c_e(trial_x);
612 trial_c_i = matrices.c_i(trial_x);
615 FilterEntry trial_entry{trial_f, trial_s, trial_c_e, trial_c_i, μ};
616 if (filter.try_add(current_entry, trial_entry, D_ϕ, α)) {
620 step_acceptable =
true;
625 constexpr Scalar κ_soc(0.99);
629 next_constraint_violation =
630 constraint_violation(trial_c_e, trial_c_i, trial_s);
631 if (next_constraint_violation > κ_soc * soc_constraint_violation) {
635 soc_constraint_violation = next_constraint_violation;
638 if (step_acceptable) {
648 ++full_step_rejected_counter;
653 if (full_step_rejected_counter >= 4 && filter.try_reset(current_entry)) {
658 α *= α_reduction_factor;
662 if (α < α_min || α < α_tiny) {
663 Scalar current_kkt_error = kkt_error<Scalar, KKTErrorType::ONE_NORM>(
664 g, A_e, c_e, A_i, c_i, s, y, z, μ, options.tolerance);
666 trial_x = x + α_max * step.p_x;
667 trial_s = s + α_max * step.p_s;
668 trial_y = y + α_max * step.p_y;
669 trial_z = z + α_z * step.p_z;
671 trial_f = matrices.f(trial_x);
672 trial_c_e = matrices.c_e(trial_x);
673 trial_c_i = matrices.c_i(trial_x);
675 Scalar next_kkt_error = kkt_error<Scalar, KKTErrorType::ONE_NORM>(
676 matrices.g(trial_x), matrices.A_e(trial_x), trial_c_e,
677 matrices.A_i(trial_x), trial_c_i, trial_s, trial_y, trial_z, μ,
682 if (next_kkt_error <= Scalar(0.999) * std::min(current_kkt_error,
683 fallback_kkt_error) &&
684 constraint_violation(trial_c_e, trial_c_i, trial_s) <=
685 filter.max_constraint_violation) {
687 fallback_kkt_error = next_kkt_error;
692 call_feasibility_restoration =
true;
700 if (!call_feasibility_restoration && !in_feasibility_restoration) {
701 FilterEntry trial_entry{trial_f, trial_s, trial_c_e, trial_c_i, μ};
702 if (stall_detector.update(current_entry, trial_entry, α,
703 filter.min_constraint_violation)) {
704 call_feasibility_restoration =
true;
708 if (call_feasibility_restoration) {
709 ScopedTimeTracer feasibility_restoration_tracer{
710 diagnostics[FEASIBILITY_RESTORATION]};
713 if (in_feasibility_restoration) {
714 return ExitStatus::FEASIBILITY_RESTORATION_FAILED;
717 FilterEntry initial_entry{matrices.f(x), s, c_e, c_i, μ};
721 filter.add(initial_entry);
723 feasibility_restoration_cycle_detector.record_entry(
724 initial_entry.constraint_violation);
725 const bool run_feasibility_restoration_to_convergence =
726 feasibility_restoration_cycle_detector.is_cycling();
729 gch::small_vector<std::function<bool(
const IterationInfo<Scalar>& info)>>
731 for (
auto& callback : iteration_callbacks) {
732 callbacks.emplace_back(callback);
734 callbacks.emplace_back([&](
const IterationInfo<Scalar>& info) {
735 if (run_feasibility_restoration_to_convergence) {
739 DenseVector trial_x =
740 info.x.segment(0, matrices.num_decision_variables);
742 DenseVector trial_c_e = matrices.c_e(trial_x);
743 DenseVector trial_c_i = matrices.c_i(trial_x);
747 DenseVector trial_s =
748 (trial_c_i.array() >= Scalar(1e-2))
750 info.s.segment(0, matrices.num_inequality_constraints));
754 FilterEntry trial_entry{matrices.f(trial_x), trial_s, trial_c_e,
756 return trial_entry.constraint_violation <
757 Scalar(0.9) * initial_entry.constraint_violation &&
758 filter.is_acceptable(trial_entry);
761 feasibility_restoration<Scalar>(matrices, callbacks, options,
762#ifdef SLEIPNIR_ENABLE_BOUND_PROJECTION
763 bound_constraint_mask,
765 x, s, y, z, μ, iterations);
767 if (status != ExitStatus::SUCCESS) {
773 c_e = matrices.c_e(x);
774 c_i = matrices.c_i(x);
784 if (!filter.is_acceptable(FilterEntry{f, s, c_e, c_i, μ})) {
785 return ExitStatus::FEASIBILITY_RESTORATION_FAILED;
790 full_step_rejected_counter = 0;
817 for (
int row = 0; row < z.rows(); ++row) {
818 constexpr Scalar κ_Σ(1e10);
820 std::clamp(z[row], Scalar(1) / κ_Σ * μ / s[row], κ_Σ * μ / s[row]);
824 A_e = matrices.A_e(x);
825 A_i = matrices.A_i(x);
827 H = matrices.H(x, y, z);
830 E_0 = unscaled_kkt_error<Scalar, KKTErrorType::INF_NORM_SCALED>(
831 matrices.scaling, g, A_e, c_e, A_i, c_i, s, y, z, Scalar(0),
835 if (E_0 > options.tolerance) {
837 constexpr Scalar κ_ε(10);
841 Scalar E_μ = kkt_error<Scalar, KKTErrorType::INF_NORM_SCALED>(
842 g, A_e, c_e, A_i, c_i, s, y, z, μ, options.tolerance);
843 while (μ > μ_min && E_μ <= κ_ε * μ) {
844 update_barrier_parameter_and_reset_filter();
845 E_μ = kkt_error<Scalar, KKTErrorType::INF_NORM_SCALED>(
846 g, A_e, c_e, A_i, c_i, s, y, z, μ, options.tolerance);
850 inner_iter_tracer.stop();
852 if (options.diagnostics) {
853 print_iterate_diagnostics(
855 in_feasibility_restoration ? IterationType::FEASIBILITY_RESTORATION
856 : IterationType::NORMAL,
857 inner_iter_tracer.current_duration(), E_0, f,
858 constraint_violation(c_e, c_i, s), s.dot(z), μ,
859 solver.hessian_regularization(),
860 solver.constraint_jacobian_regularization(), step.primal_inf_norm(),
861 step.dual_inf_norm(), α, α_max, α_reduction_factor, α_z);
867 if (iterations >= options.max_iterations) {
868 return ExitStatus::MAX_ITERATIONS_EXCEEDED;
872 if (std::chrono::steady_clock::now() - solve_start_time > options.timeout) {
873 return ExitStatus::TIMEOUT;
877 if (!isfinite(E_0)) {
878 return ExitStatus::DIVERGING_ITERATES;
880 return ExitStatus::SUCCESS;
884extern template SLEIPNIR_DLLEXPORT ExitStatus
885ipm(
const IPMMatrixCallbacks<double>& matrix_callbacks,
886 std::span<std::function<
bool(
const IterationInfo<double>& info)>>
888 const Options<double>& options,
889#ifdef SLEIPNIR_ENABLE_BOUND_PROJECTION
890 const Eigen::ArrayX<bool>& bound_constraint_mask,
892 Eigen::VectorX<double>& x);