Sleipnir C++ API
Loading...
Searching...
No Matches
sqp.hpp
1// Copyright (c) Sleipnir contributors
2
3#pragma once
4
5#include <chrono>
6#include <cmath>
7#include <functional>
8#include <span>
9
10#include <Eigen/Core>
11#include <Eigen/SparseCore>
12#include <gch/small_vector.hpp>
13
14#include "sleipnir/optimization/solver/exit_status.hpp"
15#include "sleipnir/optimization/solver/iteration_info.hpp"
16#include "sleipnir/optimization/solver/options.hpp"
17#include "sleipnir/optimization/solver/sqp_matrix_callbacks.hpp"
18#include "sleipnir/optimization/solver/util/all_finite.hpp"
19#include "sleipnir/optimization/solver/util/append_as_triplets.hpp"
20#include "sleipnir/optimization/solver/util/feasibility_restoration.hpp"
21#include "sleipnir/optimization/solver/util/filter.hpp"
22#include "sleipnir/optimization/solver/util/is_locally_infeasible.hpp"
23#include "sleipnir/optimization/solver/util/kkt_error.hpp"
24#include "sleipnir/optimization/solver/util/regularized_ldlt.hpp"
25#include "sleipnir/util/assert.hpp"
26#include "sleipnir/util/print_diagnostics.hpp"
27#include "sleipnir/util/profiler.hpp"
28#include "sleipnir/util/scope_exit.hpp"
29#include "sleipnir/util/symbol_exports.hpp"
30
31// See docs/algorithms.md#Works_cited for citation definitions.
32
33namespace slp {
34
55template <typename Scalar>
56ExitStatus sqp(const SQPMatrixCallbacks<Scalar>& matrix_callbacks,
57 std::span<std::function<bool(const IterationInfo<Scalar>& info)>>
58 iteration_callbacks,
59 const Options& options,
60 Eigen::Vector<Scalar, Eigen::Dynamic>& x) {
61 using DenseVector = Eigen::Vector<Scalar, Eigen::Dynamic>;
62
63 DenseVector y = DenseVector::Zero(matrix_callbacks.num_equality_constraints);
64
65 return sqp(matrix_callbacks, iteration_callbacks, options, x, y);
66}
67
90template <typename Scalar>
91ExitStatus sqp(const SQPMatrixCallbacks<Scalar>& matrix_callbacks,
92 std::span<std::function<bool(const IterationInfo<Scalar>& info)>>
93 iteration_callbacks,
94 const Options& options, Eigen::Vector<Scalar, Eigen::Dynamic>& x,
95 Eigen::Vector<Scalar, Eigen::Dynamic>& y) {
96 using DenseVector = Eigen::Vector<Scalar, Eigen::Dynamic>;
97 using SparseMatrix = Eigen::SparseMatrix<Scalar>;
98 using SparseVector = Eigen::SparseVector<Scalar>;
99
101 struct Step {
103 DenseVector p_x;
105 DenseVector p_y;
106 };
107
108 using std::isfinite;
109
110 const auto solve_start_time = std::chrono::steady_clock::now();
111
112 gch::small_vector<SolveProfiler> solve_profilers;
113 solve_profilers.emplace_back("solver");
114 solve_profilers.emplace_back("↳ setup");
115 solve_profilers.emplace_back("↳ iteration");
116 solve_profilers.emplace_back(" ↳ feasibility check");
117 solve_profilers.emplace_back(" ↳ callbacks");
118 solve_profilers.emplace_back(" ↳ KKT matrix build");
119 solve_profilers.emplace_back(" ↳ KKT matrix decomp");
120 solve_profilers.emplace_back(" ↳ KKT system solve");
121 solve_profilers.emplace_back(" ↳ line search");
122 solve_profilers.emplace_back(" ↳ SOC");
123 solve_profilers.emplace_back(" ↳ feas. restoration");
124 solve_profilers.emplace_back(" ↳ f(x)");
125 solve_profilers.emplace_back(" ↳ ∇f(x)");
126 solve_profilers.emplace_back(" ↳ ∇²ₓₓL");
127 solve_profilers.emplace_back(" ↳ ∇²ₓₓL_c");
128 solve_profilers.emplace_back(" ↳ cₑ(x)");
129 solve_profilers.emplace_back(" ↳ ∂cₑ/∂x");
130
131 auto& solver_prof = solve_profilers[0];
132 auto& setup_prof = solve_profilers[1];
133 auto& inner_iter_prof = solve_profilers[2];
134 auto& feasibility_check_prof = solve_profilers[3];
135 auto& iter_callbacks_prof = solve_profilers[4];
136 auto& kkt_matrix_build_prof = solve_profilers[5];
137 auto& kkt_matrix_decomp_prof = solve_profilers[6];
138 auto& kkt_system_solve_prof = solve_profilers[7];
139 auto& line_search_prof = solve_profilers[8];
140 auto& soc_prof = solve_profilers[9];
141 auto& feasibility_restoration_prof = solve_profilers[10];
142
143 // Set up profiled matrix callbacks
144#ifndef SLEIPNIR_DISABLE_DIAGNOSTICS
145 auto& f_prof = solve_profilers[11];
146 auto& g_prof = solve_profilers[12];
147 auto& H_prof = solve_profilers[13];
148 auto& H_c_prof = solve_profilers[14];
149 auto& c_e_prof = solve_profilers[15];
150 auto& A_e_prof = solve_profilers[16];
151
152 SQPMatrixCallbacks<Scalar> matrices{
153 matrix_callbacks.num_decision_variables,
154 matrix_callbacks.num_equality_constraints,
155 [&](const DenseVector& x) -> Scalar {
156 ScopedProfiler prof{f_prof};
157 return matrix_callbacks.f(x);
158 },
159 [&](const DenseVector& x) -> SparseVector {
160 ScopedProfiler prof{g_prof};
161 return matrix_callbacks.g(x);
162 },
163 [&](const DenseVector& x, const DenseVector& y) -> SparseMatrix {
164 ScopedProfiler prof{H_prof};
165 return matrix_callbacks.H(x, y);
166 },
167 [&](const DenseVector& x, const DenseVector& y) -> SparseMatrix {
168 ScopedProfiler prof{H_c_prof};
169 return matrix_callbacks.H_c(x, y);
170 },
171 [&](const DenseVector& x) -> DenseVector {
172 ScopedProfiler prof{c_e_prof};
173 return matrix_callbacks.c_e(x);
174 },
175 [&](const DenseVector& x) -> SparseMatrix {
176 ScopedProfiler prof{A_e_prof};
177 return matrix_callbacks.A_e(x);
178 },
179 matrix_callbacks.scaling};
180#else
181 const auto& matrices = matrix_callbacks;
182#endif
183
184 solver_prof.start();
185 setup_prof.start();
186
187 Scalar f = matrices.f(x);
188 SparseVector g = matrices.g(x);
189 SparseMatrix H = matrices.H(x, y);
190 DenseVector c_e = matrices.c_e(x);
191 SparseMatrix A_e = matrices.A_e(x);
192
193 // Ensure matrix callback dimensions are consistent
194 slp_assert(g.rows() == matrices.num_decision_variables);
195 slp_assert(H.rows() == matrices.num_decision_variables);
196 slp_assert(H.cols() == matrices.num_decision_variables);
197 slp_assert(c_e.rows() == matrices.num_equality_constraints);
198 slp_assert(A_e.rows() == matrices.num_equality_constraints);
199 slp_assert(A_e.cols() == matrices.num_decision_variables);
200
201 DenseVector trial_x;
202 DenseVector trial_y;
203
204 Scalar trial_f;
205 DenseVector trial_c_e;
206
207 // Check for overconstrained problem
208 if (matrices.num_equality_constraints > matrices.num_decision_variables) {
209 if (options.diagnostics) {
210 print_too_few_dofs_error(c_e);
211 }
212
213 return ExitStatus::TOO_FEW_DOFS;
214 }
215
216 // Check whether initial guess has finite cost, constraints, and derivatives
217 if (!isfinite(f) || !all_finite(g) || !all_finite(H) || !c_e.allFinite() ||
218 !all_finite(A_e)) {
219 return ExitStatus::NONFINITE_INITIAL_GUESS;
220 }
221
222 int iterations = 0;
223
224 Filter<Scalar> filter{c_e.template lpNorm<1>()};
225
226 // Kept outside the loop so its storage can be reused
227 gch::small_vector<Eigen::Triplet<Scalar>> triplets;
228
229 const int lhs_rows =
230 matrices.num_decision_variables + matrices.num_equality_constraints;
231 RegularizedLDLT<Scalar> solver{
232 // Use sparse solver if lower triangle fills < 25% of system
233 H.nonZeros() + A_e.nonZeros() < 0.25 * lhs_rows * lhs_rows,
234 matrices.num_decision_variables, matrices.num_equality_constraints};
235
236 // Variables for determining when a step is acceptable
237 constexpr Scalar α_reduction_factor(0.5);
238 constexpr Scalar α_min(1e-7);
239
240 int full_step_rejected_counter = 0;
241
242 // Error
243 Scalar E_0 = unscaled_kkt_error<Scalar, KKTErrorType::INF_NORM_SCALED>(
244 matrices.scaling, g, A_e, c_e, y);
245
246 setup_prof.stop();
247
248 // Prints final solver diagnostics when the solver exits
249 scope_exit exit{[&] {
250 if (options.diagnostics) {
251 solver_prof.stop();
252 if (iterations > 0) {
253 print_bottom_iteration_diagnostics();
254 }
255 print_solver_diagnostics(solve_profilers);
256 }
257 }};
258
259 while (E_0 > Scalar(options.tolerance)) {
260 ScopedProfiler inner_iter_profiler{inner_iter_prof};
261 ScopedProfiler feasibility_check_profiler{feasibility_check_prof};
262
263 // Check for local equality constraint infeasibility
264 if (is_equality_locally_infeasible(A_e, c_e)) {
265 if (options.diagnostics) {
266 print_c_e_local_infeasibility_error(c_e);
267 }
268
269 return ExitStatus::LOCALLY_INFEASIBLE;
270 }
271
272 // Check for diverging iterates
273 if (x.template lpNorm<Eigen::Infinity>() > Scalar(1e10) || !x.allFinite()) {
274 return ExitStatus::DIVERGING_ITERATES;
275 }
276
277 feasibility_check_profiler.stop();
278 ScopedProfiler iter_callbacks_profiler{iter_callbacks_prof};
279
280 // Call iteration callbacks
281 for (const auto& callback : iteration_callbacks) {
282 if (callback({iterations, x, {}, y, {}, g, H, A_e, {}})) {
283 return ExitStatus::CALLBACK_REQUESTED_STOP;
284 }
285 }
286
287 iter_callbacks_profiler.stop();
288 ScopedProfiler kkt_matrix_build_profiler{kkt_matrix_build_prof};
289
290 // lhs = [H Aₑᵀ]
291 // [Aₑ 0 ]
292 //
293 // Don't assign upper triangle because solver only uses lower triangle.
294 triplets.clear();
295 triplets.reserve(H.nonZeros() + A_e.nonZeros());
296 append_as_triplets(triplets, 0, 0, {H, A_e});
297 SparseMatrix lhs(
298 matrices.num_decision_variables + matrices.num_equality_constraints,
299 matrices.num_decision_variables + matrices.num_equality_constraints);
300 lhs.setFromSortedTriplets(triplets.begin(), triplets.end());
301
302 // rhs = −[∇f − Aₑᵀy]
303 // [ cₑ ]
304 DenseVector rhs{x.rows() + y.rows()};
305 rhs.segment(0, x.rows()) = -g + A_e.transpose() * y;
306 rhs.segment(x.rows(), y.rows()) = -c_e;
307
308 kkt_matrix_build_profiler.stop();
309 ScopedProfiler kkt_matrix_decomp_profiler{kkt_matrix_decomp_prof};
310
311 Step step;
312 constexpr Scalar α_max(1);
313 Scalar α(1);
314 bool call_feasibility_restoration = false;
315
316 // Solve the Newton-KKT system
317 //
318 // [H Aₑᵀ][ pˣ] = −[∇f − Aₑᵀy]
319 // [Aₑ 0 ][−pʸ] [ cₑ ]
320 if (solver.compute(lhs).info() != Eigen::Success) [[unlikely]] {
321 return ExitStatus::FACTORIZATION_FAILED;
322 }
323
324 kkt_matrix_decomp_profiler.stop();
325 ScopedProfiler kkt_system_solve_profiler{kkt_system_solve_prof};
326
327 auto compute_step = [&](Step& step) {
328 // p = [ pˣ]
329 // [−pʸ]
330 DenseVector p = solver.solve(rhs);
331 step.p_x = p.segment(0, x.rows());
332 step.p_y = -p.segment(x.rows(), y.rows());
333 };
334 compute_step(step);
335
336 kkt_system_solve_profiler.stop();
337 ScopedProfiler line_search_profiler{line_search_prof};
338
339 α = α_max;
340
341 const FilterEntry<Scalar> current_entry{f, c_e};
342 const Scalar D_ϕ = g.transpose() * step.p_x;
343
344 // Loop until a step is accepted
345 while (1) {
346 trial_x = x + α * step.p_x;
347 trial_y = y + α * step.p_y;
348
349 trial_f = matrices.f(trial_x);
350 trial_c_e = matrices.c_e(trial_x);
351
352 // If f(xₖ + αpₖˣ) or cₑ(xₖ + αpₖˣ) aren't finite, reduce step size
353 // immediately
354 if (!isfinite(trial_f) || !trial_c_e.allFinite()) {
355 // Reduce step size
356 α *= α_reduction_factor;
357
358 if (α < α_min) {
359 call_feasibility_restoration = true;
360 break;
361 }
362 continue;
363 }
364
365 // Check whether filter accepts trial iterate
366 FilterEntry trial_entry{trial_f, trial_c_e};
367 if (filter.try_add(current_entry, trial_entry, D_ϕ, α)) {
368 // Accept step
369 break;
370 }
371
372 Scalar prev_constraint_violation = c_e.template lpNorm<1>();
373 Scalar next_constraint_violation = trial_c_e.template lpNorm<1>();
374
375 // Second-order corrections
376 //
377 // If first trial point was rejected and constraint violation stayed the
378 // same or went up, apply second-order corrections
379 if (α == α_max &&
380 next_constraint_violation >= prev_constraint_violation) {
381 // Apply second-order corrections. See section 2.4 of [2].
382 auto soc_step = step;
383
384 Scalar α_soc = α;
385 DenseVector c_e_soc = c_e;
386
387 Scalar soc_constraint_violation = next_constraint_violation;
388
389 bool step_acceptable = false;
390 for (int soc_iteration = 0; soc_iteration < 5 && !step_acceptable;
391 ++soc_iteration) {
392 ScopedProfiler soc_profiler{soc_prof};
393
394 scope_exit soc_exit{[&] {
395 soc_profiler.stop();
396
397 if (options.diagnostics && step_acceptable) {
398 print_iteration_diagnostics(
399 iterations, IterationType::SECOND_ORDER_CORRECTION,
400 soc_profiler.current_duration(),
401 kkt_error<Scalar, KKTErrorType::INF_NORM_SCALED>(
402 g, A_e, trial_c_e, trial_y),
403 trial_f, trial_c_e.template lpNorm<1>(), Scalar(0), Scalar(0),
404 solver.hessian_regularization(),
405 solver.constraint_jacobian_regularization(),
406 soc_step.p_x.template lpNorm<Eigen::Infinity>(),
407 soc_step.p_y.template lpNorm<Eigen::Infinity>(), α_soc,
408 Scalar(1), α_reduction_factor, Scalar(1));
409 }
410 }};
411
412 // Rebuild Newton-KKT rhs with updated constraint values.
413 //
414 // rhs = −[∇f − Aₑᵀy]
415 // [ cₑˢᵒᶜ ]
416 //
417 // where cₑˢᵒᶜ = αc(xₖ) + c(xₖ + αpₖˣ)
418 c_e_soc = α_soc * c_e_soc + trial_c_e;
419 rhs.bottomRows(y.rows()) = -c_e_soc;
420
421 // Solve the Newton-KKT system
422 compute_step(soc_step);
423
424 trial_x = x + α_soc * soc_step.p_x;
425 trial_y = y + α_soc * soc_step.p_y;
426
427 trial_f = matrices.f(trial_x);
428 trial_c_e = matrices.c_e(trial_x);
429
430 // Check whether the filter accepts trial iterate
431 FilterEntry trial_entry{trial_f, trial_c_e};
432 if (filter.try_add(current_entry, trial_entry, D_ϕ, α)) {
433 step = soc_step;
434 α = α_soc;
435 step_acceptable = true;
436 break;
437 }
438
439 // Constraint violation scale factor for second-order corrections
440 constexpr Scalar κ_soc(0.99);
441
442 // If constraint violation hasn't been sufficiently reduced, stop
443 // making second-order corrections
444 next_constraint_violation = trial_c_e.template lpNorm<1>();
445 if (next_constraint_violation > κ_soc * soc_constraint_violation) {
446 break;
447 }
448
449 soc_constraint_violation = next_constraint_violation;
450 }
451
452 if (step_acceptable) {
453 // Accept step
454 break;
455 }
456 }
457
458 // If we got here and α is the full step, the full step was rejected.
459 // Increment the full-step rejected counter to keep track of how many full
460 // steps have been rejected in a row.
461 if (α == α_max) {
462 ++full_step_rejected_counter;
463 }
464
465 // If the full step was rejected enough times in a row, reset the filter
466 // because it may be impeding progress.
467 //
468 // See section 3.2 case I of [2].
469 if (full_step_rejected_counter >= 4 &&
470 filter.max_constraint_violation >
471 current_entry.constraint_violation / Scalar(10) &&
472 filter.last_rejection_due_to_filter()) {
473 filter.max_constraint_violation *= Scalar(0.1);
474 filter.reset();
475 continue;
476 }
477
478 // Reduce step size
479 α *= α_reduction_factor;
480
481 // If step size hit a minimum, check if the KKT error was reduced. If it
482 // wasn't, invoke feasibility restoration.
483 if (α < α_min) {
484 Scalar current_kkt_error =
485 kkt_error<Scalar, KKTErrorType::ONE_NORM>(g, A_e, c_e, y);
486
487 trial_x = x + α_max * step.p_x;
488 trial_y = y + α_max * step.p_y;
489
490 trial_f = matrices.f(trial_x);
491 trial_c_e = matrices.c_e(trial_x);
492
493 Scalar next_kkt_error = kkt_error<Scalar, KKTErrorType::ONE_NORM>(
494 matrices.g(trial_x), matrices.A_e(trial_x), trial_c_e, trial_y);
495
496 // If the step using αᵐᵃˣ reduced the KKT error, accept it anyway
497 if (next_kkt_error <= Scalar(0.999) * current_kkt_error) {
498 // Accept step
499 break;
500 }
501
502 call_feasibility_restoration = true;
503 break;
504 }
505 }
506
507 line_search_profiler.stop();
508
509 if (call_feasibility_restoration) {
510 ScopedProfiler feasibility_restoration_profiler{
511 feasibility_restoration_prof};
512
513 FilterEntry initial_entry{matrices.f(x), c_e};
514
515 // Feasibility restoration phase
516 gch::small_vector<std::function<bool(const IterationInfo<Scalar>& info)>>
517 callbacks;
518 for (auto& callback : iteration_callbacks) {
519 callbacks.emplace_back(callback);
520 }
521 callbacks.emplace_back([&](const IterationInfo<Scalar>& info) {
522 DenseVector trial_x =
523 info.x.segment(0, matrices.num_decision_variables);
524
525 DenseVector trial_c_e = matrices.c_e(trial_x);
526
527 FilterEntry trial_entry{matrices.f(trial_x), trial_c_e};
528 const Scalar D_ϕ_restoration = g.transpose() * (trial_x - x);
529
530 // If the current iterate sufficiently reduces constraint violation and
531 // is accepted by the normal filter, stop feasibility restoration
532 return trial_entry.constraint_violation <
533 Scalar(0.9) * initial_entry.constraint_violation &&
534 filter.try_add(initial_entry, trial_entry, D_ϕ_restoration, α);
535 });
536 auto status = feasibility_restoration<Scalar>(matrices, callbacks,
537 options, x, y, iterations);
538
539 if (status != ExitStatus::SUCCESS) {
540 // Report failure
541 return status;
542 }
543
544 f = matrices.f(x);
545 c_e = matrices.c_e(x);
546 } else {
547 // If full step was accepted, reset full-step rejected counter
548 if (α == α_max) {
549 full_step_rejected_counter = 0;
550 }
551
552 // Update iterates
553 x = trial_x;
554 y = trial_y;
555
556 f = trial_f;
557 c_e = trial_c_e;
558 }
559
560 // Update autodiff for Jacobians and Hessian
561 A_e = matrices.A_e(x);
562 g = matrices.g(x);
563 H = matrices.H(x, y);
564
565 // Update the error
566 E_0 = unscaled_kkt_error<Scalar, KKTErrorType::INF_NORM_SCALED>(
567 matrices.scaling, g, A_e, c_e, y);
568
569 inner_iter_profiler.stop();
570
571 if (options.diagnostics) {
572 print_iteration_diagnostics(iterations, IterationType::NORMAL,
573 inner_iter_profiler.current_duration(), E_0,
574 f, c_e.template lpNorm<1>(), Scalar(0),
575 Scalar(0), solver.hessian_regularization(),
576 solver.constraint_jacobian_regularization(),
577 step.p_x.template lpNorm<Eigen::Infinity>(),
578 step.p_y.template lpNorm<Eigen::Infinity>(),
579 α, α_max, α_reduction_factor, α);
580 }
581
582 ++iterations;
583
584 // Check for max iterations
585 if (iterations >= options.max_iterations) {
586 return ExitStatus::MAX_ITERATIONS_EXCEEDED;
587 }
588
589 // Check for max wall clock time
590 if (std::chrono::steady_clock::now() - solve_start_time > options.timeout) {
591 return ExitStatus::TIMEOUT;
592 }
593 }
594
595 return ExitStatus::SUCCESS;
596}
597
598extern template SLEIPNIR_DLLEXPORT ExitStatus
599sqp(const SQPMatrixCallbacks<double>& matrix_callbacks,
600 std::span<std::function<bool(const IterationInfo<double>& info)>>
601 iteration_callbacks,
602 const Options& options, Eigen::Vector<double, Eigen::Dynamic>& x);
603
604} // namespace slp