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