smooth_feedback
Control and estimation on Lie groups
Loading...
Searching...
No Matches
qp_solver.hpp
Go to the documentation of this file.
1// Copyright (C) 2022 Petter Nilsson, John B. Mains. MIT License.
2
3#pragma once
4
10#include <chrono>
11#include <iomanip>
12#include <iostream>
13#include <limits>
14#include <optional>
15
16#include <Eigen/Cholesky>
17#include <Eigen/Dense>
18#include <Eigen/Sparse>
19#include <Eigen/SparseCholesky>
20
21#include "qp.hpp"
22#include "utils/sparse.hpp"
23
24namespace smooth::feedback {
25
30{
32 bool verbose = false;
33
35 float alpha = 1.6f;
37 float rho = 0.1f;
39 float sigma = 1e-6f;
40
42 bool scaling = true;
43
45 float eps_abs = 1e-3f;
47 float eps_rel = 1e-3f;
49 float eps_primal_inf = 1e-4f;
51 float eps_dual_inf = 1e-4f;
52
54 std::optional<uint32_t> max_iter = {};
55
57 std::optional<std::chrono::nanoseconds> max_time = {};
58
60 uint32_t stop_check_iter = 25;
61
63 bool polish = true;
65 uint32_t polish_iter = 5;
67 float delta = 1e-6f;
68};
69
70namespace detail {
71
72template<typename Pbm>
73using qp_solution_t = QPSolution<
74 decltype(Pbm::A)::RowsAtCompileTime,
75 decltype(Pbm::A)::ColsAtCompileTime,
76 typename decltype(Pbm::A)::Scalar>;
77
92template<typename Pbm, typename D1, typename D2>
94 const Pbm & pbm,
96 const QPSolverParams & prm,
97 const typename decltype(Pbm::A)::Scalar c,
98 const Eigen::MatrixBase<D1> & sx,
99 const Eigen::MatrixBase<D2> & sy)
100{
101 using AmatT = decltype(Pbm::A);
102 using Scalar = typename AmatT::Scalar;
103 static constexpr bool sparse = std::is_base_of_v<Eigen::SparseMatrixBase<AmatT>, AmatT>;
104
105 static constexpr Scalar inf = std::numeric_limits<Scalar>::infinity();
106 static constexpr Scalar eps = std::numeric_limits<Scalar>::epsilon();
107
108 static constexpr Eigen::Index N = AmatT::ColsAtCompileTime;
109 const Eigen::Index n = pbm.A.cols(), m = pbm.A.rows();
110
111 // FIND ACTIVE CONSTRAINT SETS
112
113 Eigen::Index nl = 0, nu = 0;
114 for (auto idx = 0; idx < m; ++idx) {
115 if (sol.dual[idx] < -100 * eps && pbm.l[idx] != -inf) { nl++; }
116 if (sol.dual[idx] > 100 * eps && pbm.u[idx] != inf) { nu++; }
117 }
118
119 Eigen::VectorXi LU_idx(nl + nu);
120 for (auto idx = 0, lcntr = 0, ucntr = 0; idx < m; ++idx) {
121 if (sol.dual[idx] < -100 * eps && pbm.l[idx] != -inf) { LU_idx(lcntr++) = idx; }
122 if (sol.dual[idx] > 100 * eps && pbm.u[idx] != inf) { LU_idx(nl + ucntr++) = idx; }
123 }
124
125 // FORM REDUCED SYSTEMS (27) AND (30)
126
127 // square symmetric system matrix
128 using HT = std::conditional_t<sparse, Eigen::SparseMatrix<Scalar>, Eigen::Matrix<Scalar, -1, -1>>;
129 HT H(n + nl + nu, n + nl + nu), Hp(n + nl + nu, n + nl + nu);
130
131 // fill up H
132 if constexpr (sparse) {
133 // preallocate nonzeros
134 Eigen::VectorXi nnz(n + nl + nu);
135 for (auto i = 0u; i != n; ++i) { nnz(i) = pbm.P.outerIndexPtr()[i + 1] - pbm.P.outerIndexPtr()[i]; }
136 for (auto i = 0u; i != nl + nu; ++i) {
137 nnz(n + i) = pbm.A.outerIndexPtr()[LU_idx(i) + 1] - pbm.A.outerIndexPtr()[LU_idx(i)];
138 }
139 H.reserve(nnz);
140 Hp.reserve(nnz + Eigen::VectorXi::Ones(n + nl + nu));
141
142 // fill P in top left block
143 for (auto k = 0u; k < n; ++k) {
144 for (Eigen::InnerIterator it(pbm.P, k); it; ++it) {
145 const Scalar pij = c * sx(it.col()) * sx(it.row()) * it.value();
146 H.insert(it.row(), it.col()) = pij;
147 Hp.insert(it.row(), it.col()) = pij;
148 }
149 }
150
151 // fill selected rows of A in top right block
152 for (auto a_row = 0u; a_row != nl + nu; ++a_row) {
153 for (Eigen::InnerIterator it(pbm.A, LU_idx(a_row)); it; ++it) {
154 const Scalar Aij = sy(it.row()) * sx(it.col()) * it.value();
155 H.insert(it.col(), n + a_row) = Aij;
156 Hp.insert(it.col(), n + a_row) = Aij;
157 }
158 }
159 } else {
160 H.setZero();
161 H.topLeftCorner(n, n) = c * sx.asDiagonal() * pbm.P * sx.asDiagonal();
162 for (auto i = 0u; i != nl + nu; ++i) {
163 H.col(n + i).template head<N>(n) = sy(LU_idx(i)) * pbm.A.row(LU_idx(i)) * sx.asDiagonal();
164 }
165 Hp = H;
166 }
167
168 // add perturbing diagonal elements to Hp
169 if constexpr (sparse) {
170 for (auto i = 0u; i != n; ++i) { Hp.coeffRef(i, i) += prm.delta; }
171 for (auto i = 0u; i != nl + nu; ++i) { Hp.coeffRef(n + i, n + i) -= prm.delta; }
172 H.makeCompressed();
173 Hp.makeCompressed();
174 } else {
175 Hp.topLeftCorner(n, n) += Eigen::VectorX<Scalar>::Constant(n, prm.delta).asDiagonal();
176 Hp.bottomRightCorner(nl + nu, nl + nu) -= Eigen::VectorX<Scalar>::Constant(nl + nu, prm.delta).asDiagonal();
177 }
178
179 Eigen::VectorX<Scalar> h(n + nl + nu);
180 h.head(n) = -c * sx.cwiseProduct(pbm.q);
181 for (auto i = 0u; i != nl; ++i) { h(n + i) = sy(LU_idx(i)) * pbm.l(LU_idx(i)); }
182 for (auto i = 0u; i != nu; ++i) { h(n + nl + i) = sy(LU_idx(nl + i)) * pbm.u(LU_idx(nl + i)); }
183
184 // ITERATIVE REFINEMENT
185
186 // factorize Hp
187 std::conditional_t<sparse, Eigen::SimplicialLDLT<decltype(H), Eigen::Upper>, Eigen::LDLT<decltype(H), Eigen::Upper>>
188 ldlt(Hp);
189
190 if (ldlt.info()) { return false; }
191
192 Eigen::VectorX<Scalar> t_hat = Eigen::VectorX<Scalar>::Zero(n + nl + nu);
193 for (auto i = 0u; i != prm.polish_iter; ++i) {
194 t_hat += ldlt.solve(h - H.template selfadjointView<Eigen::Upper>() * t_hat);
195 }
196
197 // UPDATE SOLUTION
198
199 sol.primal = t_hat.template head<N>(n);
200 for (auto i = 0u; i < nl; ++i) { sol.dual(LU_idx(i)) = t_hat(n + i); }
201 for (auto i = 0u; i < nu; ++i) { sol.dual(LU_idx(nl + i)) = t_hat(n + nl + i); }
202
203 return true;
204}
205
209template<typename LDLTt>
211{
212 bool first{true}; // true if ldlt object needs factorization
213 LDLTt ldlt{};
214
215 LDLTWrapper() = default;
216 LDLTWrapper(const LDLTWrapper &) : first{true}, ldlt{} {}
217 LDLTWrapper(LDLTWrapper &&) noexcept : first{true}, ldlt{} {}
218 LDLTWrapper & operator=(const LDLTWrapper & rhs)
219 {
220 if (this == &rhs) { return *this; }
221
222 first = true;
223 return *this;
224 }
225 LDLTWrapper & operator=(LDLTWrapper &&) noexcept
226 {
227 first = true;
228 return *this;
229 }
230 ~LDLTWrapper() = default;
231};
232
233} // namespace detail
234
242template<typename Pbm>
244{
245 using AmatT = decltype(Pbm::A);
246 using Scalar = typename AmatT::Scalar;
247 static constexpr bool sparse = std::is_base_of_v<Eigen::SparseMatrixBase<AmatT>, AmatT>;
248
249 // static sizes
250 static constexpr Eigen::Index M = AmatT::RowsAtCompileTime;
251 static constexpr Eigen::Index N = AmatT::ColsAtCompileTime;
252 static constexpr Eigen::Index K = (N == -1 || M == -1) ? Eigen::Index(-1) : N + M;
253
254 // typedefs
255 using Rn = Eigen::Vector<Scalar, N>;
256 using Rm = Eigen::Vector<Scalar, M>;
257 using Rk = Eigen::Vector<Scalar, K>;
258 using Ht = std::conditional_t<sparse, Eigen::SparseMatrix<Scalar>, Eigen::Matrix<Scalar, K, K>>;
259 using LDLTt = std::conditional_t<sparse, Eigen::SimplicialLDLT<Ht, Eigen::Upper>, Eigen::LDLT<Ht, Eigen::Upper>>;
260
261 static inline const Scalar inf = std::numeric_limits<Scalar>::infinity();
262
263public:
267 QPSolver(const QPSolverParams & prm = {}) : prm_(prm) {}
268
276 QPSolver(const Pbm & pbm, const QPSolverParams & prm = {}) : prm_(prm) { analyze(pbm); }
277
279 QPSolver(const QPSolver &) = default;
281 QPSolver(QPSolver &&) noexcept = default;
283 QPSolver & operator=(const QPSolver &) = default;
285 QPSolver & operator=(QPSolver &&) noexcept = default;
287 ~QPSolver() = default;
288
292 const QPSolution<M, N, Scalar> & sol() const { return sol_; }
293
297 void analyze(const Pbm & pbm)
298 {
299 const Eigen::Index n = pbm.A.cols(), m = pbm.A.rows(), k = n + m;
300
301 // solution
302 sol_.primal.setZero(n);
303 sol_.dual.setZero(m);
304
305 // scaling variables and working memory
306 c_ = 1;
307 sx_.setOnes(n);
308 sy_.setOnes(m);
309 sx_inc_.setZero(n);
310 sy_inc_.setZero(m);
311
312 // solve working memory
313 z_.resize(m);
314 z_next_.resize(m);
315 rho_.resize(m);
316 p_.resize(k);
317 pt_.resize(k);
318
319 // stopping working memory
320 x_us_.resize(n);
321 dx_us_.resize(n);
322 y_us_.resize(m);
323 dy_us_.resize(m);
324 z_us_.resize(m);
325 Px_.resize(n);
326 Aty_.resize(n);
327 Ax_.resize(m);
328
329 // preallocate nonzeros in H
330 H_.setZero();
331 H_.resize(k, k);
332 if constexpr (sparse) {
333 Eigen::VectorXi nnz(k);
334 for (auto i = 0u; i < n; ++i) { nnz(i) = pbm.P.outerIndexPtr()[i + 1] - pbm.P.outerIndexPtr()[i] + 1; }
335 for (auto i = 0u; i < m; ++i) { nnz(n + i) = pbm.A.outerIndexPtr()[i + 1] - pbm.A.outerIndexPtr()[i] + 1; }
336 H_.reserve(nnz);
337 }
338 }
339
344 solve(const Pbm & pbm, std::optional<std::reference_wrapper<const QPSolution<M, N, Scalar>>> warmstart = {})
345 {
346 // update problem scaling
347 if (prm_.scaling) { scale(pbm); }
348
349 // dynamic sizes
350 const Eigen::Index n = pbm.A.cols(), m = pbm.A.rows();
351
352 // cast parameters to scalar type
353 const Scalar rho_bar = static_cast<Scalar>(prm_.rho);
354 const Scalar alpha = static_cast<Scalar>(prm_.alpha);
355 const Scalar alpha_comp = Scalar(1) - alpha;
356 const Scalar sigma = static_cast<Scalar>(prm_.sigma);
357
358 // return code: when set algorithm is finished
359 std::optional<QPSolutionStatus> ret_code = std::nullopt;
360
361 for (auto i = 0u; i != m; ++i) {
362 if (pbm.l(i) == inf || pbm.u(i) == -inf || pbm.u(i) - pbm.l(i) < Scalar(0.)) {
363 ret_code = QPSolutionStatus::PrimalInfeasible; // feasible set trivially empty
364 }
365
366 // set rho depending on constraint type
367 if (pbm.l(i) == -inf && pbm.u(i) == inf) {
368 rho_(i) = Scalar(1e-6); // unbounded
369 } else if (sy_(i) * std::fabs(pbm.l(i) - pbm.u(i)) < 1e-5) {
370 rho_(i) = Scalar(1e3) * rho_bar; // equality
371 } else {
372 rho_(i) = rho_bar; // inequality
373 }
374 }
375
376 const auto t0 = std::chrono::high_resolution_clock::now();
377
378 // fill square symmetric system matrix H = [P A'; A 0]
379 if constexpr (sparse) {
380 if (H_.isCompressed()) { H_.coeffs().setZero(); }
381
382 for (auto i = 0u; i < pbm.P.outerSize(); ++i) {
383 for (Eigen::InnerIterator it(pbm.P, i); it; ++it) {
384 if (it.col() >= it.row()) {
385 H_.coeffRef(it.row(), it.col()) = c_ * sx_(it.row()) * sx_(it.col()) * it.value();
386 }
387 }
388 }
389 block_add_identity(H_, 0, 0, n, sigma);
390 for (auto i = 0u; i < pbm.A.outerSize(); ++i) {
391 for (Eigen::InnerIterator it(pbm.A, i); it; ++it) {
392 H_.coeffRef(it.col(), n + it.row()) = sy_(it.row()) * sx_(it.col()) * it.value();
393 }
394 }
395 for (auto row = 0u; row < m; ++row) { H_.coeffRef(n + row, n + row) = Scalar(-1) / rho_(row); }
396
397 if (!H_.isCompressed()) { H_.makeCompressed(); }
398 } else {
399 H_.setZero();
400
401 H_.template topLeftCorner<N, N>(n, n) = c_ * sx_.asDiagonal() * pbm.P * sx_.asDiagonal();
402 H_.template topLeftCorner<N, N>(n, n) += Eigen::Vector<Scalar, N>::Constant(n, sigma).asDiagonal();
403 H_.template topRightCorner<N, M>(n, m) = (sy_.asDiagonal() * pbm.A * sx_.asDiagonal()).transpose();
404 H_.template bottomRightCorner<M, M>(m, m) = (-rho_).cwiseInverse().asDiagonal();
405 }
406
407 const auto t_fill = std::chrono::high_resolution_clock::now();
408
409 if (prm_.verbose) {
410 using std::cout, std::left, std::setw, std::right;
411 // clang-format off
412 cout << "========================= QP Solver =========================" << '\n';
413 cout << "Solving " << (sparse ? "sparse" : "dense") << " QP with n=" << n << ", m=" << m << '\n';
414 cout << setw(8) << right << "ITER"
415 << setw(14) << right << "OBJ"
416 << setw(14) << right << "PRI_RES"
417 << setw(14) << right << "DUA_RES"
418 << setw(10) << right << "TIME" << '\n';
419 // clang-format on
420 }
421
422 // factorize H
423 if constexpr (sparse) {
424 if (ldlt_.first) { ldlt_.ldlt.analyzePattern(H_); }
425 ldlt_.ldlt.factorize(H_); // makes copy for permuted matrix..
426 ldlt_.first = false;
427 } else {
428 ldlt_.ldlt.compute(H_);
429 }
430
431 const auto t_factor = std::chrono::high_resolution_clock::now();
432
433 if (ldlt_.ldlt.info()) { ret_code = QPSolutionStatus::Unknown; }
434
435 // initialize solver variables
436 if (warmstart.has_value()) {
437 // warmstart variables must be scaled
438 sol_.primal = sx_.cwiseInverse().cwiseProduct(warmstart.value().get().primal);
439 sol_.dual = c_ * sy_.cwiseInverse().cwiseProduct(warmstart.value().get().dual);
440 z_.noalias() = sy_.asDiagonal() * pbm.A * warmstart.value().get().primal;
441 } else {
442 sol_.primal.setZero();
443 sol_.dual.setZero();
444 z_.setZero();
445 }
446
447 // main optimization loop
448 auto iter = 0u;
449 for (; (!prm_.max_iter || iter != prm_.max_iter.value()) && !ret_code; ++iter) {
450 p_.template segment<N>(0, n) = sigma * sol_.primal - c_ * sx_.asDiagonal() * pbm.q;
451 p_.template segment<M>(n, m) = z_ - rho_.cwiseInverse().cwiseProduct(sol_.dual);
452 if constexpr (sparse) {
453 // p_ = ldlt_.solve(p_);
454 // manual solve because ldlt_.solve() uses temporaries...
455 // Pinv L D L' P x = b <==> x = Pinv * (L' \ Dinv(L \ (P * b)))
456 pt_.noalias() = ldlt_.ldlt.permutationP() * p_;
457 ldlt_.ldlt.matrixL().solveInPlace(pt_);
458 pt_.applyOnTheLeft(ldlt_.ldlt.vectorD().cwiseInverse().asDiagonal());
459 ldlt_.ldlt.matrixU().solveInPlace(pt_);
460 p_.noalias() = ldlt_.ldlt.permutationPinv() * pt_;
461 } else {
462 ldlt_.ldlt.solveInPlace(p_);
463 }
464
465 if (iter % prm_.stop_check_iter == 1) {
466 // termination checking requires difference, store old scaled values
467 dx_us_ = sol_.primal, dy_us_ = sol_.dual;
468 }
469
470 sol_.primal = alpha * p_.template segment<N>(0, n) + alpha_comp * sol_.primal;
471 z_next_ = (alpha * rho_.cwiseInverse().cwiseProduct(p_.template segment<M>(n, m))
472 + alpha_comp * rho_.cwiseInverse().cwiseProduct(sol_.dual) + z_)
473 .cwiseMax(sy_.cwiseProduct(pbm.l))
474 .cwiseMin(sy_.cwiseProduct(pbm.u));
475 sol_.dual = alpha_comp * sol_.dual + alpha * p_.template segment<M>(n, m) + rho_.cwiseProduct(z_)
476 - rho_.cwiseProduct(z_next_);
477 std::swap(z_, z_next_);
478
479 if (iter % prm_.stop_check_iter == 1) {
480 // unscale solution
481 x_us_ = sx_.cwiseProduct(sol_.primal);
482 y_us_ = sy_.cwiseProduct(sol_.dual) / c_;
483 z_us_ = sy_.cwiseInverse().cwiseProduct(z_);
484 dx_us_ = sx_.cwiseProduct(sol_.primal - dx_us_);
485 dy_us_ = sy_.cwiseProduct(sol_.dual - dy_us_) / c_;
486
487 // check stopping criteria for unscaled problem and unscaled variables
488 ret_code = check_stopping(pbm);
489
490 if (prm_.verbose) {
491 using std::cout, std::setw, std::right, std::chrono::microseconds;
492 // clang-format off
493 cout << setw(7) << right << iter << ":"
494 << std::scientific
495 << setw(14) << right << (0.5 * pbm.P * x_us_ + pbm.q).dot(x_us_)
496 << setw(14) << right << (pbm.A * x_us_ - z_us_).template lpNorm<Eigen::Infinity>()
497 << setw(14) << right << (pbm.P * x_us_ + pbm.q + pbm.A.transpose() * y_us_).template lpNorm<Eigen::Infinity>()
498 << setw(10) << right << duration_cast<microseconds>(std::chrono::high_resolution_clock::now() - t0).count()
499 << '\n';
500 // clang-format on
501 }
502
503 // check for timeout
504 if (!ret_code) {
505 if (prm_.max_time && std::chrono::high_resolution_clock::now() > t0 + prm_.max_time.value()) {
506 ret_code = QPSolutionStatus::MaxTime;
507 }
508 }
509 }
510 }
511
512 const auto t_iter = std::chrono::high_resolution_clock::now();
513
514 // polish solution if optimal
515 if (ret_code.has_value() && ret_code.value() == QPSolutionStatus::Optimal && prm_.polish) {
516 if (detail::polish_qp(pbm, sol_, prm_, c_, sx_, sy_)) {
517 if (prm_.verbose) {
518 // unscale solution
519 x_us_ = sx_.cwiseProduct(sol_.primal);
520 y_us_ = sy_.cwiseProduct(sol_.dual) / c_;
521 z_us_ = sy_.cwiseInverse().cwiseProduct(z_);
522
523 using std::cout, std::setw, std::right, std::chrono::microseconds;
524 // clang-format off
525 cout << setw(8) << right << "polish:"
526 << std::scientific
527 << setw(14) << right << (0.5 * pbm.P * x_us_ + pbm.q).dot(x_us_)
528 << setw(14) << right << (pbm.A * x_us_ - z_us_).template lpNorm<Eigen::Infinity>()
529 << setw(14) << right << (pbm.P * x_us_ + pbm.q + pbm.A.transpose() * y_us_).template lpNorm<Eigen::Infinity>()
530 << setw(10) << right << duration_cast<microseconds>(std::chrono::high_resolution_clock::now() - t0).count()
531 << '\n';
532 // clang-format on
533 }
534
535 } else {
536 if (prm_.verbose) { std::cout << "Polish failed" << '\n'; }
537 sol_.code = QPSolutionStatus::PolishFailed;
538 }
539 }
540
541 const auto t_polish = std::chrono::high_resolution_clock::now();
542
543 // unscale solution
544 sol_.code = ret_code.value_or(QPSolutionStatus::MaxIterations);
545 sol_.primal = sx_.cwiseProduct(sol_.primal);
546 sol_.dual = sy_.cwiseProduct(sol_.dual) / c_;
547 sol_.objective = sol_.primal.dot(0.5 * pbm.P * sol_.primal + pbm.q);
548 sol_.iter = iter;
549
550 if (prm_.verbose) {
551 using std::cout, std::left, std::right, std::setw, std::chrono::microseconds;
552
553 // clang-format off
554 cout << "QP solver summary:" << '\n';
555 cout << "Result " << static_cast<int>(sol_.code) << '\n';
556
557 cout << setw(25) << left << "Iterations" << setw(10) << right << iter - 1 << '\n';
558 cout << setw(26) << left << "Total time (µs)" << setw(10) << right << duration_cast<microseconds>(t_polish - t0).count() << '\n';
559 cout << setw(25) << left << " Matrix filling" << setw(10) << right << duration_cast<microseconds>(t_fill - t0).count() << '\n';
560 cout << setw(25) << left << " Factorization" << setw(10) << right << duration_cast<microseconds>(t_factor - t_fill).count() << '\n';
561 cout << setw(25) << left << " Iteration" << setw(10) << right << duration_cast<microseconds>(t_iter - t_factor).count() << '\n';
562 cout << setw(25) << left << " Polish" << setw(10) << right << duration_cast<microseconds>(t_polish - t_iter).count() << '\n';
563 cout << "=============================================================" << '\n';
564 // clang-format on
565 }
566
567 return sol_;
568 }
569
570protected:
574 std::optional<QPSolutionStatus> check_stopping(const Pbm & pbm)
575 {
576 const Eigen::Index m = pbm.A.rows();
577
578 // norm function
579 static const auto norm = [](auto && t) -> Scalar { return t.template lpNorm<Eigen::Infinity>(); };
580
581 // OPTIMALITY
582
583 // check primal
584 Ax_.noalias() = pbm.A * x_us_;
585 const Scalar Ax_norm = norm(Ax_);
586 Ax_ -= z_us_;
587 if (norm(Ax_) <= prm_.eps_abs + prm_.eps_rel * std::max<Scalar>(Ax_norm, norm(z_us_))) {
588 // primal succeeded, check dual
589 Px_.noalias() = pbm.P * x_us_;
590 Aty_.noalias() = pbm.A.transpose() * y_us_;
591 const Scalar dual_scale = std::max<Scalar>({norm(Px_), norm(pbm.q), norm(Aty_)});
592 Px_ += pbm.q + Aty_;
593 if (norm(Px_) <= prm_.eps_abs + prm_.eps_rel * dual_scale) { return QPSolutionStatus::Optimal; }
594 }
595
596 // PRIMAL INFEASIBILITY
597
598 Aty_.noalias() = pbm.A.transpose() * dy_us_; // NOTE new value A' * dy
599 const Scalar Edy_norm = norm(dy_us_);
600
601 Scalar u_dyp_plus_l_dyn = Scalar(0);
602 for (auto i = 0u; i != m; ++i) {
603 if (pbm.u(i) != inf) {
604 u_dyp_plus_l_dyn += pbm.u(i) * std::max<Scalar>(Scalar(0), dy_us_(i));
605 } else if (dy_us_(i) > prm_.eps_primal_inf * Edy_norm) {
606 // contributes +inf to sum --> no certificate
607 u_dyp_plus_l_dyn = inf;
608 break;
609 }
610 if (pbm.l(i) != -inf) {
611 u_dyp_plus_l_dyn += pbm.l(i) * std::min<Scalar>(Scalar(0), dy_us_(i));
612 } else if (dy_us_(i) < -prm_.eps_primal_inf * Edy_norm) {
613 // contributes +inf to sum --> no certificate
614 u_dyp_plus_l_dyn = inf;
615 break;
616 }
617 }
618
619 if (std::max<Scalar>(norm(Aty_), u_dyp_plus_l_dyn) < prm_.eps_primal_inf * Edy_norm) {
620 return QPSolutionStatus::PrimalInfeasible;
621 }
622
623 // DUAL INFEASIBILITY
624
625 Ax_.noalias() = pbm.A * dx_us_; // note new value A * dx
626 const Scalar dx_norm = norm(dx_us_);
627 Px_.noalias() = pbm.P * dx_us_;
628
629 bool dual_infeasible =
630 (norm(Px_) <= prm_.eps_dual_inf * dx_norm) && (pbm.q.dot(dx_us_) <= prm_.eps_dual_inf * dx_norm);
631 for (auto i = 0u; i != m && dual_infeasible; ++i) {
632 if (pbm.u(i) == inf) {
633 dual_infeasible &= (Ax_(i) >= -prm_.eps_dual_inf * dx_norm);
634 } else if (pbm.l(i) == -inf) {
635 dual_infeasible &= (Ax_(i) <= prm_.eps_dual_inf * dx_norm);
636 } else {
637 dual_infeasible &= std::fabs(Ax_(i)) < prm_.eps_dual_inf * dx_norm;
638 }
639 }
640
641 if (dual_infeasible) { return QPSolutionStatus::DualInfeasible; }
642
643 return std::nullopt;
644 }
645
673 void scale(const Pbm & pbm)
674 {
675 sx_.setOnes();
676 sy_.setOnes();
677
678 sx_inc_.setZero();
679
680 // find "norm" of cost function
681 for (auto i = 0u; i < pbm.P.outerSize(); ++i) {
682 for (Eigen::InnerIterator it(pbm.P, i); it; ++it) {
683 sx_inc_(it.col()) = std::max(sx_inc_(it.col()), std::fabs(it.value()));
684 }
685 }
686
687 // if there are "zero cols"
688 for (auto i = 0u; i != sx_.size(); ++i) {
689 if (sx_inc_(i) == 0) { sx_inc_(i) = 1; }
690 }
691
692 // scale cost function
693 c_ = Scalar(1) / std::max({1e-6, sx_inc_.mean(), pbm.q.template lpNorm<Eigen::Infinity>()});
694
695 int iter = 0;
696
697 // calculate inf-norm for every column of [Ps As' ; As 0]
698 do {
699 sx_inc_.setZero();
700 sy_inc_.setZero();
701 for (auto k = 0u; k < pbm.P.outerSize(); ++k) {
702 for (Eigen::InnerIterator it(pbm.P, k); it; ++it) {
703 // upper left block of H
704 sx_inc_(it.col()) = std::max({
705 sx_inc_(it.col()),
706 std::fabs(c_ * sx_(it.row()) * sx_(it.col()) * it.value()),
707 });
708 }
709 }
710 for (auto k = 0u; k < pbm.A.outerSize(); ++k) {
711 for (Eigen::InnerIterator it(pbm.A, k); it; ++it) {
712 const Scalar Aij = std::fabs(sy_(it.row()) * sx_(it.col()) * it.value());
713 sx_inc_(it.col()) = std::max(sx_inc_(it.col()), Aij); // bottom left block of H
714 sy_inc_(it.row()) = std::max(sy_inc_(it.row()), Aij); // upper right block of H
715 }
716 }
717
718 // if there are "zero cols" we don't scale
719 for (auto i = 0u; i < sx_.size(); ++i) {
720 if (sx_inc_(i) == 0) { sx_inc_(i) = 1; }
721 }
722 for (auto i = 0u; i < sy_.size(); ++i) {
723 if (sy_inc_(i) == 0) { sy_inc_(i) = 1; }
724 }
725
726 sx_.applyOnTheLeft(sx_inc_.cwiseMax(1e-8).cwiseInverse().cwiseSqrt().asDiagonal());
727 sy_.applyOnTheLeft(sy_inc_.cwiseMax(1e-8).cwiseInverse().cwiseSqrt().asDiagonal());
728 } while (iter++ < 10
729 && std::max((sx_inc_.array() - 1).abs().maxCoeff(), (sy_inc_.array() - 1).abs().maxCoeff()) > 0.1);
730 }
731
732private:
733 // solver parameters
734 QPSolverParams prm_{};
735
736 // solution
737 QPSolution<M, N, Scalar> sol_{};
738
739 // scaling variables and working memory
740 Scalar c_{0};
741 Rn sx_{}, sx_inc_{};
742 Rm sy_{}, sy_inc_{};
743
744 // solve working memory
745 Rm z_{}, z_next_{}, rho_{};
746 Rk p_{}, pt_{};
747
748 // stopping working memory
749 Rn x_us_{}, dx_us_{};
750 Rn Px_{}, Aty_{};
751 Rm Ax_{};
752 Rm y_us_{}, z_us_{}, dy_us_{};
753
754 // system matrix and decomposition
755 Ht H_{};
756 detail::LDLTWrapper<LDLTt> ldlt_{};
757};
758
779template<typename Pbm>
781 const Pbm & pbm,
782 const QPSolverParams & prm,
783 std::optional<std::reference_wrapper<const detail::qp_solution_t<Pbm>>> warmstart = {})
784{
785 QPSolver solver(pbm, prm);
786 return solver.solve(pbm, warmstart);
787}
788
789} // namespace smooth::feedback
Solver for quadratic programs.
Definition: qp_solver.hpp:244
QPSolver(const QPSolverParams &prm={})
Default constructor.
Definition: qp_solver.hpp:267
const QPSolution< M, N, Scalar > & solve(const Pbm &pbm, std::optional< std::reference_wrapper< const QPSolution< M, N, Scalar > > > warmstart={})
Solve quadratic program.
Definition: qp_solver.hpp:344
QPSolver(const QPSolver &)=default
Default copy constructor.
void scale(const Pbm &pbm)
Re-scale QP.
Definition: qp_solver.hpp:673
void analyze(const Pbm &pbm)
Prepare for solving problems.
Definition: qp_solver.hpp:297
QPSolver(QPSolver &&) noexcept=default
Default move constructor.
QPSolver(const Pbm &pbm, const QPSolverParams &prm={})
Construct and allocate working memory.
Definition: qp_solver.hpp:276
const QPSolution< M, N, Scalar > & sol() const
Access most recent QP solution.
Definition: qp_solver.hpp:292
std::optional< QPSolutionStatus > check_stopping(const Pbm &pbm)
Check stopping criteria for solver.
Definition: qp_solver.hpp:574
Quadratic Program definition.
detail::qp_solution_t< Pbm > solve_qp(const Pbm &pbm, const QPSolverParams &prm, std::optional< std::reference_wrapper< const detail::qp_solution_t< Pbm > > > warmstart={})
Solve a quadratic program using the operator splitting approach.
Definition: qp_solver.hpp:780
bool polish_qp(const Pbm &pbm, qp_solution_t< Pbm > &sol, const QPSolverParams &prm, const typename decltype(Pbm::A)::Scalar c, const Eigen::MatrixBase< D1 > &sx, const Eigen::MatrixBase< D2 > &sy)
Polish solution of quadratic program.
Definition: qp_solver.hpp:93
Sparse matrix utilities.
void block_add_identity(Eigen::SparseMatrix< double, Options > &dest, Eigen::Index row0, Eigen::Index col0, Eigen::Index n, double scale=1)
Add identity matrix block into sparse matrix.
Definition: sparse.hpp:103
Solver solution.
Definition: qp.hpp:97
QPSolutionStatus code
Exit code.
Definition: qp.hpp:99
Eigen::Matrix< Scalar, N, 1 > primal
Primal vector.
Definition: qp.hpp:103
Scalar objective
Solution objective value.
Definition: qp.hpp:107
uint32_t iter
Number of iterations.
Definition: qp.hpp:101
Eigen::Matrix< Scalar, M, 1 > dual
Dual vector.
Definition: qp.hpp:105
Options for solve_qp.
Definition: qp_solver.hpp:30
float rho
first dual step size
Definition: qp_solver.hpp:37
uint32_t stop_check_iter
iterations between checking stopping criterion
Definition: qp_solver.hpp:60
std::optional< uint32_t > max_iter
max number of iterations (default no limit)
Definition: qp_solver.hpp:54
float alpha
relaxation parameter
Definition: qp_solver.hpp:35
float eps_dual_inf
threshold for dual infeasibility
Definition: qp_solver.hpp:51
std::optional< std::chrono::nanoseconds > max_time
max solution time (default no limit)
Definition: qp_solver.hpp:57
float eps_rel
relative threshold for convergence
Definition: qp_solver.hpp:47
bool verbose
print solver info to stdout
Definition: qp_solver.hpp:32
bool polish
run solution polishing (uses dynamic memory)
Definition: qp_solver.hpp:63
float eps_primal_inf
threshold for primal infeasibility
Definition: qp_solver.hpp:49
float eps_abs
absolute threshold for convergence
Definition: qp_solver.hpp:45
uint32_t polish_iter
number of iterations to refine polish
Definition: qp_solver.hpp:65
float delta
regularization parameter for polishing
Definition: qp_solver.hpp:67
float sigma
second dual step length
Definition: qp_solver.hpp:39
Copy-able wrapper around Eigen LDLT types.
Definition: qp_solver.hpp:211