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