16#include <Eigen/Cholesky>
18#include <Eigen/Sparse>
19#include <Eigen/SparseCholesky>
24namespace smooth::feedback {
57 std::optional<std::chrono::nanoseconds>
max_time = {};
74 decltype(Pbm::A)::RowsAtCompileTime,
75 decltype(Pbm::A)::ColsAtCompileTime,
76 typename decltype(Pbm::A)::Scalar>;
92template<
typename Pbm,
typename D1,
typename D2>
97 const typename decltype(Pbm::A)::Scalar c,
98 const Eigen::MatrixBase<D1> & sx,
99 const Eigen::MatrixBase<D2> & sy)
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>;
105 static constexpr Scalar inf = std::numeric_limits<Scalar>::infinity();
106 static constexpr Scalar eps = std::numeric_limits<Scalar>::epsilon();
108 static constexpr Eigen::Index N = AmatT::ColsAtCompileTime;
109 const Eigen::Index n = pbm.A.cols(), m = pbm.A.rows();
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++; }
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; }
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);
132 if constexpr (sparse) {
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)];
140 Hp.reserve(nnz + Eigen::VectorXi::Ones(n + nl + nu));
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;
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;
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();
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; }
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();
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)); }
187 std::conditional_t<sparse, Eigen::SimplicialLDLT<
decltype(H), Eigen::Upper>, Eigen::LDLT<
decltype(H), Eigen::Upper>>
190 if (ldlt.info()) {
return false; }
192 Eigen::VectorX<Scalar> t_hat = Eigen::VectorX<Scalar>::Zero(n + nl + nu);
194 t_hat += ldlt.solve(h - H.template selfadjointView<Eigen::Upper>() * t_hat);
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); }
209template<
typename LDLTt>
220 if (
this == &rhs) {
return *
this; }
242template<
typename Pbm>
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>;
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;
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>>;
261 static inline const Scalar inf = std::numeric_limits<Scalar>::infinity();
299 const Eigen::Index n = pbm.A.cols(), m = pbm.A.rows(), k = n + m;
303 sol_.
dual.setZero(m);
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; }
350 const Eigen::Index n = pbm.A.cols(), m = pbm.A.rows();
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);
359 std::optional<QPSolutionStatus> ret_code = std::nullopt;
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;
367 if (pbm.l(i) == -inf && pbm.u(i) == inf) {
368 rho_(i) = Scalar(1e-6);
369 }
else if (sy_(i) * std::fabs(pbm.l(i) - pbm.u(i)) < 1e-5) {
370 rho_(i) = Scalar(1e3) * rho_bar;
376 const auto t0 = std::chrono::high_resolution_clock::now();
379 if constexpr (sparse) {
380 if (H_.isCompressed()) { H_.coeffs().setZero(); }
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();
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();
395 for (
auto row = 0u; row < m; ++row) { H_.coeffRef(n + row, n + row) = Scalar(-1) / rho_(row); }
397 if (!H_.isCompressed()) { H_.makeCompressed(); }
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();
407 const auto t_fill = std::chrono::high_resolution_clock::now();
410 using std::cout, std::left, std::setw, std::right;
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';
423 if constexpr (sparse) {
424 if (ldlt_.first) { ldlt_.ldlt.analyzePattern(H_); }
425 ldlt_.ldlt.factorize(H_);
428 ldlt_.ldlt.compute(H_);
431 const auto t_factor = std::chrono::high_resolution_clock::now();
433 if (ldlt_.ldlt.info()) { ret_code = QPSolutionStatus::Unknown; }
436 if (warmstart.has_value()) {
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;
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) {
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_;
462 ldlt_.ldlt.solveInPlace(p_);
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_);
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_;
491 using std::cout, std::setw, std::right, std::chrono::microseconds;
493 cout << setw(7) << right << iter <<
":"
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()
505 if (prm_.
max_time && std::chrono::high_resolution_clock::now() > t0 + prm_.
max_time.value()) {
506 ret_code = QPSolutionStatus::MaxTime;
512 const auto t_iter = std::chrono::high_resolution_clock::now();
515 if (ret_code.has_value() && ret_code.value() == QPSolutionStatus::Optimal && prm_.
polish) {
516 if (detail::polish_qp(pbm, sol_, prm_, c_, sx_, sy_)) {
519 x_us_ = sx_.cwiseProduct(sol_.
primal);
520 y_us_ = sy_.cwiseProduct(sol_.
dual) / c_;
521 z_us_ = sy_.cwiseInverse().cwiseProduct(z_);
523 using std::cout, std::setw, std::right, std::chrono::microseconds;
525 cout << setw(8) << right <<
"polish:"
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()
536 if (prm_.
verbose) { std::cout <<
"Polish failed" <<
'\n'; }
537 sol_.
code = QPSolutionStatus::PolishFailed;
541 const auto t_polish = std::chrono::high_resolution_clock::now();
544 sol_.
code = ret_code.value_or(QPSolutionStatus::MaxIterations);
546 sol_.
dual = sy_.cwiseProduct(sol_.
dual) / c_;
551 using std::cout, std::left, std::right, std::setw, std::chrono::microseconds;
554 cout <<
"QP solver summary:" <<
'\n';
555 cout <<
"Result " <<
static_cast<int>(sol_.
code) <<
'\n';
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';
576 const Eigen::Index m = pbm.A.rows();
579 static const auto norm = [](
auto && t) -> Scalar {
return t.template lpNorm<Eigen::Infinity>(); };
584 Ax_.noalias() = pbm.A * x_us_;
585 const Scalar Ax_norm = norm(Ax_);
587 if (norm(Ax_) <= prm_.
eps_abs + prm_.
eps_rel * std::max<Scalar>(Ax_norm, norm(z_us_))) {
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_)});
593 if (norm(Px_) <= prm_.
eps_abs + prm_.
eps_rel * dual_scale) {
return QPSolutionStatus::Optimal; }
598 Aty_.noalias() = pbm.A.transpose() * dy_us_;
599 const Scalar Edy_norm = norm(dy_us_);
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));
607 u_dyp_plus_l_dyn = inf;
610 if (pbm.l(i) != -inf) {
611 u_dyp_plus_l_dyn += pbm.l(i) * std::min<Scalar>(Scalar(0), dy_us_(i));
614 u_dyp_plus_l_dyn = inf;
619 if (std::max<Scalar>(norm(Aty_), u_dyp_plus_l_dyn) < prm_.
eps_primal_inf * Edy_norm) {
620 return QPSolutionStatus::PrimalInfeasible;
625 Ax_.noalias() = pbm.A * dx_us_;
626 const Scalar dx_norm = norm(dx_us_);
627 Px_.noalias() = pbm.P * dx_us_;
629 bool dual_infeasible =
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);
637 dual_infeasible &= std::fabs(Ax_(i)) < prm_.
eps_dual_inf * dx_norm;
641 if (dual_infeasible) {
return QPSolutionStatus::DualInfeasible; }
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()));
688 for (
auto i = 0u; i != sx_.size(); ++i) {
689 if (sx_inc_(i) == 0) { sx_inc_(i) = 1; }
693 c_ = Scalar(1) / std::max({1e-6, sx_inc_.mean(), pbm.q.template lpNorm<Eigen::Infinity>()});
701 for (
auto k = 0u; k < pbm.P.outerSize(); ++k) {
702 for (Eigen::InnerIterator it(pbm.P, k); it; ++it) {
704 sx_inc_(it.col()) = std::max({
706 std::fabs(c_ * sx_(it.row()) * sx_(it.col()) * it.value()),
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);
714 sy_inc_(it.row()) = std::max(sy_inc_(it.row()), Aij);
719 for (
auto i = 0u; i < sx_.size(); ++i) {
720 if (sx_inc_(i) == 0) { sx_inc_(i) = 1; }
722 for (
auto i = 0u; i < sy_.size(); ++i) {
723 if (sy_inc_(i) == 0) { sy_inc_(i) = 1; }
726 sx_.applyOnTheLeft(sx_inc_.cwiseMax(1e-8).cwiseInverse().cwiseSqrt().asDiagonal());
727 sy_.applyOnTheLeft(sy_inc_.cwiseMax(1e-8).cwiseInverse().cwiseSqrt().asDiagonal());
729 && std::max((sx_inc_.array() - 1).abs().maxCoeff(), (sy_inc_.array() - 1).abs().maxCoeff()) > 0.1);
737 QPSolution<M, N, Scalar> sol_{};
745 Rm z_{}, z_next_{}, rho_{};
749 Rn x_us_{}, dx_us_{};
752 Rm y_us_{}, z_us_{}, dy_us_{};
756 detail::LDLTWrapper<LDLTt> ldlt_{};
779template<
typename Pbm>
785 QPSolver solver(pbm, prm);
786 return solver.solve(pbm, warmstart);
Solver for quadratic programs.
QPSolver(const QPSolverParams &prm={})
Default constructor.
const QPSolution< M, N, Scalar > & solve(const Pbm &pbm, std::optional< std::reference_wrapper< const QPSolution< M, N, Scalar > > > warmstart={})
Solve quadratic program.
QPSolver(const QPSolver &)=default
Default copy constructor.
void scale(const Pbm &pbm)
Re-scale QP.
void analyze(const Pbm &pbm)
Prepare for solving problems.
QPSolver(QPSolver &&) noexcept=default
Default move constructor.
QPSolver(const Pbm &pbm, const QPSolverParams &prm={})
Construct and allocate working memory.
const QPSolution< M, N, Scalar > & sol() const
Access most recent QP solution.
std::optional< QPSolutionStatus > check_stopping(const Pbm &pbm)
Check stopping criteria for solver.
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.
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.
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.
QPSolutionStatus code
Exit code.
Eigen::Matrix< Scalar, N, 1 > primal
Primal vector.
Scalar objective
Solution objective value.
uint32_t iter
Number of iterations.
Eigen::Matrix< Scalar, M, 1 > dual
Dual vector.
float rho
first dual step size
uint32_t stop_check_iter
iterations between checking stopping criterion
std::optional< uint32_t > max_iter
max number of iterations (default no limit)
bool scaling
scale problem
float alpha
relaxation parameter
float eps_dual_inf
threshold for dual infeasibility
std::optional< std::chrono::nanoseconds > max_time
max solution time (default no limit)
float eps_rel
relative threshold for convergence
bool verbose
print solver info to stdout
bool polish
run solution polishing (uses dynamic memory)
float eps_primal_inf
threshold for primal infeasibility
float eps_abs
absolute threshold for convergence
uint32_t polish_iter
number of iterations to refine polish
float delta
regularization parameter for polishing
float sigma
second dual step length
Copy-able wrapper around Eigen LDLT types.