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