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