Sleipnir C++ API
Loading...
Searching...
No Matches
sparse_kkt_solver.hpp
1// Copyright (c) Sleipnir contributors
2
3#pragma once
4
5#include <algorithm>
6#include <limits>
7
8#include <Eigen/Core>
9#include <Eigen/SparseCholesky>
10#include <Eigen/SparseCore>
11
12#include "sleipnir/optimization/solver/util/inertia.hpp"
13
14namespace slp {
15
21template <typename Scalar>
23 public:
25 using DenseVector = Eigen::VectorX<Scalar>;
27 using SparseMatrix = Eigen::SparseMatrix<Scalar>;
28
35 SparseKKTSolver(int num_decision_variables, int num_equality_constraints)
36 : m_num_decision_variables{num_decision_variables},
37 m_num_equality_constraints{num_equality_constraints} {}
38
46 SparseKKTSolver(int num_decision_variables, int num_equality_constraints,
47 Scalar γ_min)
48 : m_num_decision_variables{num_decision_variables},
49 m_num_equality_constraints{num_equality_constraints},
50 m_γ_min{γ_min} {}
51
55 Eigen::ComputationInfo info() const { return m_info; }
56
65 // Regularization with zeros ensures the pattern analysis in the sparse
66 // solver is reused by all factorizations
67 m_lhs = lhs + regularization(Scalar(0), Scalar(0));
68
69 if (!m_analyzed_pattern) {
70 m_solver.analyzePattern(m_lhs);
71 m_analyzed_pattern = true;
72 }
73
74 // If the previous δ was small and nonzero, skip the δ = 0 attempt because
75 // it's almost always rejected for small pivots. If the previous δ was
76 // large, attempt δ = 0 so δ can drop to zero quickly.
77 if (m_prev_δ == Scalar(0) || m_prev_δ > Scalar(1e-2)) {
78 m_solver.factorize(m_lhs);
79 m_info = m_solver.info();
80
81 if (m_info == Eigen::Success) {
82 // If the inertia is ideal and D from LDLT is sufficiently far from
83 // zero, don't regularize the system
84 if (inertia() == ideal_inertia &&
85 (m_solver.vectorD().cwiseAbs().array() >= Scalar(1e-4)).all()) {
86 m_prev_δ = Scalar(0);
87 m_prev_γ = Scalar(0);
88 return *this;
89 }
90 }
91 }
92
93 // We'll give lhs the correct inertia by adding [δI, 0; 0, −γI] where δ and
94 // γ regularize the Hessian and equality constraint Jacobian respectively.
95
96 // The minimum nonzero Hessian regularization
97 constexpr Scalar δ_min(1e-10);
98
99 // The maximum constraint regularization. Larger values distort the equality
100 // constraint Jacobian more than they help a rank-deficient one.
101 constexpr Scalar γ_max(1e-8);
102
103 // The initial nonzero constraint regularization
104 constexpr Scalar γ_initial(1e-10);
105
106 // If the previous δ was zero, attempt a small value. Otherwise, attempt a
107 // smaller value than the previous δ so δ trends downward. δ has a floor
108 // because Hessian rows with no curvature (e.g., states in linear dynamics)
109 // have only δ on their diagonal, and pivots that small cause element growth
110 // that corrupts the inertia and solution.
111 Scalar δ = m_prev_δ == Scalar(0) ? Scalar(1e-4)
112 : std::max(m_prev_δ / Scalar(2), δ_min);
113
114 // Start γ at the minimum to minimize equality constraint Jacobian
115 // distortion.
116 Scalar γ = m_γ_min;
117
118 while (true) {
119 m_lhs = lhs + regularization(δ, γ);
120 m_solver.factorize(m_lhs);
121 m_info = m_solver.info();
122
123 if (m_info == Eigen::Success) {
124 Inertia inertia = this->inertia();
125
126 if (inertia == ideal_inertia) {
127 // If the inertia is ideal, report success
128 m_prev_δ = δ;
129 m_prev_γ = γ;
130 return *this;
131 } else if (inertia.zero > 0) {
132 if (γ == Scalar(0)) {
133 // If there's zero eigenvalues and γ = 0, increase γ to potentially
134 // compensate for a rank-deficient equality constraint Jacobian
135 γ = γ_initial;
136 } else {
137 // If there's zero eigenvalues and γ > 0, increase δ and γ to drive
138 // all eigenvalues away from zero
139 δ *= Scalar(10);
140 γ = std::min(γ * Scalar(10), γ_max);
141 }
142 } else if (inertia.negative > ideal_inertia.negative) {
143 // If there's too many negative eigenvalues, increase δ to add more
144 // positive eigenvalues
145 δ *= Scalar(10);
146 } else if (inertia.positive > ideal_inertia.positive) {
147 // If there's too many positive eigenvalues and the smallest pivot is
148 // from the equality constraint Jacobian block, increase γ to add more
149 // negative eigenvalues.
150 //
151 // Otherwise, the smallest pivot is from the Hessian block, or γ is
152 // already large enough that increasing it further would distort the
153 // equality constraint Jacobian more than it helps. Increase δ to
154 // reduce element growth from Hessian pivots instead.
155 if (γ < γ_max && !smallest_pivot_is_hessian()) {
156 γ = γ == Scalar(0) ? γ_initial : std::min(γ * Scalar(10), γ_max);
157 } else {
158 δ *= Scalar(10);
159 }
160 }
161 } else {
162 // If the decomposition failed, increase δ and γ to drive all
163 // eigenvalues away from zero
164 δ *= Scalar(10);
165 γ = γ == Scalar(0) ? γ_initial : std::min(γ * Scalar(10), γ_max);
166 }
167
168 // If the lhs perturbation is too high, report failure. This can be caused
169 // by ill-conditioning.
170 if (δ > Scalar(1e20) || γ > Scalar(1e20)) {
171 m_info = Eigen::NumericalIssue;
172 m_prev_δ = δ;
173 m_prev_γ = γ;
174 return *this;
175 }
176 }
177 }
178
183 template <typename Rhs>
184 DenseVector solve(const Eigen::MatrixBase<Rhs>& rhs) const {
185 DenseVector x = m_solver.solve(rhs);
186
187 // Element growth in the factorization can make the solution inaccurate
188 // (e.g., from small Hessian pivots in rows with no curvature), so apply
189 // iterative refinement with the same factorization
190 constexpr int max_refinement_steps = 3;
191 for (int i = 0; i < max_refinement_steps; ++i) {
192 DenseVector r = rhs - m_lhs.template selfadjointView<Eigen::Lower>() * x;
193 if (r.norm() <= Scalar(1e-12) * rhs.norm()) {
194 break;
195 }
196 x += m_solver.solve(r);
197 }
198
199 return x;
200 }
201
206 template <typename Rhs>
207 DenseVector solve(const Eigen::SparseMatrixBase<Rhs>& rhs) const {
208 return solve(rhs.toDense());
209 }
210
214 Scalar hessian_regularization() const { return m_prev_δ; }
215
219 Scalar constraint_jacobian_regularization() const { return m_prev_γ; }
220
221 private:
222 using Solver = Eigen::SimplicialLDLT<SparseMatrix>;
223
224 Solver m_solver;
225 bool m_analyzed_pattern = false;
226
228 SparseMatrix m_lhs;
229
230 Eigen::ComputationInfo m_info = Eigen::Success;
231
233 int m_num_decision_variables = 0;
234
236 int m_num_equality_constraints = 0;
237
239 Scalar m_γ_min{1e-10};
240
242 Inertia ideal_inertia{m_num_decision_variables, m_num_equality_constraints,
243 0};
244
246 Scalar m_prev_δ{0};
247
249 Scalar m_prev_γ{0};
250
258 Inertia inertia() const {
259 constexpr Scalar ε = std::numeric_limits<Scalar>::epsilon();
260
261 Inertia inertia;
262 for (const auto& d : m_solver.vectorD()) {
263 if (d > ε) {
264 ++inertia.positive;
265 } else if (d < -ε) {
266 ++inertia.negative;
267 } else {
268 ++inertia.zero;
269 }
270 }
271
272 return inertia;
273 }
274
279 bool smallest_pivot_is_hessian() const {
280 Eigen::Index k;
281 m_solver.vectorD().cwiseAbs().minCoeff(&k);
282
283 // The factorization is of PAP⁻¹, so the original row of pivot k is P⁻¹k
284 return m_solver.permutationPinv().indices()(k) < m_num_decision_variables;
285 }
286
295 SparseMatrix regularization(Scalar δ, Scalar γ) const {
296 DenseVector vec{m_num_decision_variables + m_num_equality_constraints};
297 vec.segment(0, m_num_decision_variables).setConstant(δ);
298 vec.segment(m_num_decision_variables, m_num_equality_constraints)
299 .setConstant(-γ);
300
301 return SparseMatrix{vec.asDiagonal()};
302 }
303};
304
305} // namespace slp
Definition inertia.hpp:11
int zero
The number of zero eigenvalues.
Definition inertia.hpp:18
int positive
The number of positive eigenvalues.
Definition inertia.hpp:14
int negative
The number of negative eigenvalues.
Definition inertia.hpp:16
Definition sparse_kkt_solver.hpp:22
Scalar constraint_jacobian_regularization() const
Definition sparse_kkt_solver.hpp:219
SparseKKTSolver(int num_decision_variables, int num_equality_constraints, Scalar γ_min)
Definition sparse_kkt_solver.hpp:46
SparseKKTSolver(int num_decision_variables, int num_equality_constraints)
Definition sparse_kkt_solver.hpp:35
Eigen::VectorX< Scalar > DenseVector
Type alias for dense vector.
Definition sparse_kkt_solver.hpp:25
DenseVector solve(const Eigen::MatrixBase< Rhs > &rhs) const
Definition sparse_kkt_solver.hpp:184
Scalar hessian_regularization() const
Definition sparse_kkt_solver.hpp:214
Eigen::ComputationInfo info() const
Definition sparse_kkt_solver.hpp:55
Eigen::SparseMatrix< Scalar > SparseMatrix
Type alias for sparse matrix.
Definition sparse_kkt_solver.hpp:27
DenseVector solve(const Eigen::SparseMatrixBase< Rhs > &rhs) const
Definition sparse_kkt_solver.hpp:207
SparseKKTSolver & compute(const SparseMatrix &lhs)
Definition sparse_kkt_solver.hpp:64