consteig
Compile-time eigenvalue and eigenvector computation for C++17
Loading...
Searching...
No Matches
eigen.hpp
1#ifndef EIGEN_HPP
2#define EIGEN_HPP
3
4#include "../consteig_options.hpp"
5#include "../consteig_types.hpp"
6#include "../math/constmath.hpp"
7#include "../matrix/decompositions/hessenberg.hpp"
8#include "../matrix/decompositions/lu.hpp"
9#include "../matrix/decompositions/qr.hpp"
10#include "../matrix/matrix.hpp"
11#include "../matrix/operations.hpp"
12
13namespace consteig
14{
15
19
20#ifdef CONSTEIG_USE_LONG_DOUBLE
21using InternalScalar = long double;
22#else
23using InternalScalar = double;
24#endif
25
26// Forward declaration
27template <typename T, Size S>
28constexpr Matrix<T, S, S> eig(
29 Matrix<T, S, S> a,
30 const T symmetryTolerance = CONSTEIG_DEFAULT_SYMMETRIC_TOLERANCE);
31
35// Algorithm: Balancing
36template <typename T, Size S>
37constexpr Matrix<T, S, S> balance(Matrix<T, S, S> a)
38{
39 bool converged = false;
40 T factor = static_cast<T>(2);
41
42 for (Size iter = 0; iter < 10 && !converged; ++iter)
43 {
44 converged = true;
45 for (Size row = 0; row < S; ++row)
46 {
47 T row_norm = 0;
48 T col_norm = 0;
49 for (Size col = 0; col < S; ++col)
50 {
51 if (row != col)
52 {
53 row_norm += abs(a(row, col));
54 col_norm += abs(a(col, row));
55 }
56 }
57
58 if (row_norm > 0 && col_norm > 0)
59 {
60 T f = 1;
61 T s = row_norm + col_norm;
62 while (row_norm < col_norm / factor)
63 {
64 f *= factor;
65 row_norm *= factor;
66 col_norm /= factor;
67 }
68 while (row_norm > col_norm * factor)
69 {
70 f /= factor;
71 row_norm /= factor;
72 col_norm *= factor;
73 }
74
75 // Stopping criterion from Parlett & Reinsch (1969); see also
76 // James, Langou & Lowery, "On Matrix Balancing and Eigenvector
77 // Computation" (2014), https://arxiv.org/pdf/1401.5766,
78 // Algorithm 2 line 12.
79 if ((row_norm + col_norm) <
81 {
82 converged = false;
83 for (Size col = 0; col < S; ++col)
84 {
85 a(row, col) *= f;
86 }
87 for (Size scale_row = 0; scale_row < S; ++scale_row)
88 {
89 a(scale_row, row) /= f;
90 }
91 }
92 }
93 }
94 }
95 return a;
96}
97
101// Algorithm: Wilkinson Shifts
102template <typename T>
103constexpr T wilkinsonShift(const T a, const T b, const T c)
104{
105 T delta{(a - c) / 2};
106 if (delta == static_cast<T>(0))
107 {
108 delta = consteig::epsilon<T>();
109 }
110 T disc = delta * delta + b * b;
111 T s = (delta < 0) ? -sqrt(disc) : sqrt(disc);
112 return c - (b * b) / (delta + s);
113}
114
117// Algorithm: Implicit Double-Shift QR (Francis QR Step)
118template <typename T, Size S>
119constexpr void francis_qr_step(Matrix<T, S, S> &H, Size l, Size n, T s, T t)
120{
121 T p1 = H(l, l) * H(l, l) + H(l, l + 1) * H(l + 1, l) - s * H(l, l) + t;
122 T p2 = H(l + 1, l) * (H(l, l) + H(l + 1, l + 1) - s);
123 T p3 = (l + 2 <= n) ? H(l + 1, l) * H(l + 2, l + 1) : static_cast<T>(0);
124
125 for (Size pos = l; pos < n; ++pos)
126 {
127 Size m = (pos + 2 <= n) ? 3 : 2;
128 T v1 = 0, v2 = 0, v3 = 0, norm = 0;
129
130 if (m == 3)
131 {
132 norm = sqrt(p1 * p1 + p2 * p2 + p3 * p3);
133 v1 = p1 + (p1 < 0 ? -norm : norm);
134 v2 = p2;
135 v3 = p3;
136 }
137 else
138 {
139 norm = sqrt(p1 * p1 + p2 * p2);
140 v1 = p1 + (p1 < 0 ? -norm : norm);
141 v2 = p2;
142 v3 = 0;
143 }
144
145 if (norm > 0)
146 {
147 T v_sum_sq = v1 * v1 + v2 * v2 + v3 * v3;
148 T beta = static_cast<T>(2) / v_sum_sq;
149
150 // Left application: include column pos-1 for pos > l to chase the
151 // bulge
152 Size col_start = (pos > l) ? pos - 1 : pos;
153 for (Size col = col_start; col < S; ++col)
154 {
155 T sum = beta * (v1 * H(pos, col) + v2 * H(pos + 1, col) +
156 (m == 3 ? v3 * H(pos + 2, col) : 0));
157 H(pos, col) -= sum * v1;
158 H(pos + 1, col) -= sum * v2;
159 if (m == 3)
160 {
161 H(pos + 2, col) -= sum * v3;
162 }
163 }
164
165 // Right application
166 Size upper_row = (pos + 3 < n + 1) ? pos + 3 : n;
167 for (Size row = 0; row <= upper_row && row < S; ++row)
168 {
169 T sum = beta * (v1 * H(row, pos) + v2 * H(row, pos + 1) +
170 (m == 3 ? v3 * H(row, pos + 2) : 0));
171 H(row, pos) -= sum * v1;
172 H(row, pos + 1) -= sum * v2;
173 if (m == 3)
174 {
175 H(row, pos + 2) -= sum * v3;
176 }
177 }
178
179 // Explicitly zero bulge elements for numerical stability
180 if (pos > l)
181 {
182 H(pos + 1, pos - 1) = 0;
183 if (m == 3)
184 {
185 H(pos + 2, pos - 1) = 0;
186 }
187 }
188 }
189
190 if (pos < n - 1)
191 {
192 p1 = H(pos + 1, pos);
193 p2 = H(pos + 2, pos);
194 if (pos < n - 2)
195 {
196 p3 = H(pos + 3, pos);
197 }
198 }
199 }
200}
201
207template <typename T, Size S>
208constexpr Matrix<T, S, S> eig_double_shifted_qr(Matrix<T, S, S> a)
209{
210 if constexpr (S <= 1)
211 {
212 return a;
213 }
214 a = balance(a);
215 a = hess(a)._h;
216
219 T eps = ulp * matrix_norm;
220 if (eps == 0)
221 {
222 eps = ulp;
223 }
224
225 Size n = S - 1;
226 Size total_iter = 0;
227 const Size max_total_iter = CONSTEIG_MAX_ITER * S;
228 Size its = 0;
229
230 while (n > 0 && total_iter < max_total_iter)
231 {
232 Size l = n;
233 while (l > 0)
234 {
235 T diagonal_sum = abs(a(l, l)) + abs(a(l - 1, l - 1));
236 // Algorithm: Robust Deflation
237 // Checks for convergence by monitoring the sub-diagonal elements.
238 // Deflates when an element becomes negligible relative to its
239 // neighboring diagonal elements. Dual-mode deflation: Standard
240 // relative check PLUS an absolute check against machine epsilon.
241 // PERFORMANCE NOTE: The absolute check is critical. Some random
242 // non-symmetric matrices have near-zero diagonal entries (|d1| +
243 // |d2| \approx 0), causing the relative check to fail indefinitely
244 // and spinning the solver to CONSTEIG_MAX_ITER. Adding '||
245 // abs(subdiag) <= eps' allows these blocks to deflate early,
246 // reducing build times from ~40m to ~7m even with more complex
247 // robustness tests.
248 if (abs(a(l, l - 1)) <= eps * diagonal_sum ||
249 abs(a(l, l - 1)) <= eps)
250 {
251 a(l, l - 1) = 0;
252 break;
253 }
254 l--;
255 }
256
257 if (l == n)
258 {
259 n--;
260 its = 0;
261 continue;
262 }
263
264 if (l + 1 == n)
265 {
266 if (n < 2)
267 {
268 break;
269 }
270 n -= 2;
271 its = 0;
272 continue;
273 }
274
275 // Compute shifts
276 T s = 0, t = 0;
277 if (its > 0 && its % 10 == 0)
278 {
279 // Algorithm: Exceptional Shifts
280 // LAPACK-style exceptional shift every 10 iterations to prevent
281 // stalling
282 T sshift = 0;
283 if (its % 20 == 0)
284 {
285 // Bottom-based exceptional shift
286 sshift = abs(a(n, n - 1)) + abs(a(n - 1, n - 2));
287 }
288 else
289 {
290 // Top-based exceptional shift
291 sshift = abs(a(l + 1, l));
292 if (l + 2 <= n)
293 {
294 sshift += abs(a(l + 2, l + 1));
295 }
296 }
297 T h11 = static_cast<T>(0.75) * sshift + a(n, n);
298 T h12 = static_cast<T>(-0.4375) * sshift;
299 T h21 = sshift;
300 s = h11 + h11;
301 t = h11 * h11 - h12 * h21;
302 }
303 else
304 {
305 // Standard double shift from bottom-right 2x2
306 s = a(n - 1, n - 1) + a(n, n);
307 t = a(n - 1, n - 1) * a(n, n) - a(n - 1, n) * a(n, n - 1);
308 }
309
310 francis_qr_step(a, l, n, s, t);
311 total_iter++;
312 its++;
313 }
314 return a;
315}
316
319template <typename T, Size S>
320constexpr Matrix<T, S, S> eig_shifted_qr(Matrix<T, S, S> a)
321{
322 if constexpr (S <= 1)
323 {
324 return a;
325 }
326 a = balance(a);
327 a = hess(a)._h;
328
330 if (eps == 0)
331 {
333 }
334
335 Size n = S;
336 Size iter = 0;
337 const Size max_iter = CONSTEIG_MAX_ITER * S;
338
339 while (n > 1 && iter < max_iter)
340 {
341 if (abs(a(n - 1, n - 2)) <=
342 eps * (abs(a(n - 1, n - 1)) + abs(a(n - 2, n - 2))))
343 {
344 a(n - 1, n - 2) = 0;
345 n--;
346 continue;
347 }
348
349 T mu =
350 wilkinsonShift(a(n - 2, n - 2), a(n - 1, n - 2), a(n - 1, n - 1));
351 Matrix<T, S, S> eyeS = eye<T, S>();
352 Matrix<T, S, S> shifted = a - (mu * eyeS);
354 a = (qrm._r * qrm._q) + (mu * eyeS);
355 iter++;
356 }
357 return a;
358}
359
378template <typename T, Size S>
380{
381 static_assert(is_float<T>(), "eig reduction expects floating point");
382 // symmetryTolerance is a routing threshold. If a matrix is "symmetric
383 // enough," we can use the faster Single-Shift QR algorithm (eig_shifted_qr)
384 // which is optimized for real eigenvalues. Otherwise, we must use the
385 // heavier Double-Shift QR (eig_double_shifted_qr) to handle complex pairs.
386 if (a.isSymmetric(static_cast<T>(symmetryTolerance)))
387 {
388 return eig_shifted_qr<T, S>(a);
389 }
390 else
391 {
393 }
394}
395
425template <typename T, Size S>
427{
428 static_assert(is_float<T>(), "eigenvalues expects floating point type");
430 for (Size row = 0; row < S; ++row)
431 {
432 for (Size col = 0; col < S; ++col)
433 {
434 a_internal(row, col) = static_cast<InternalScalar>(a(row, col));
435 }
436 }
437
440 InternalScalar eps = consteig::epsilon<InternalScalar>() *
441 (norm1(out) + static_cast<InternalScalar>(1.0));
442
443 for (Size diag = 0; diag < S; ++diag)
444 {
445 // If subdiag is essentially zero (smaller than eps), we have a 1x1 *
446 // block. If subdiag is significantly larger than zero, it means the
447 // current row and the next row are "tangled" together, forming a 2x2
448 // block (which means there should be complex conjugate eigen values.
449 bool found_2x2 = false;
450 if (diag < S - 1)
451 {
452 InternalScalar subdiag = out(diag + 1, diag);
453 if (abs(subdiag) > eps)
454 {
455 found_2x2 = true;
456 }
457 }
458
459 // Found complex conjugate, use quadratic formula to extract
460 if (found_2x2)
461 {
462 // For a 2x2 matrix, the eigenvalues (L) are the roots of the
463 // characteristic equation: det(A - LI) = 0.
464 //
465 // Expanding this for a 2x2 matrix:
466 // (a00 - L)(a11 - L) - (a01 * a10) = 0
467 // L^2 - (a00 + a11)L + (a00*a11 - a01*a10) = 0
468 //
469 // This simplifies to: L^2 - tr(A)L + det(A) = 0. Solving via
470 // quadratic formula: L = (tr +/- sqrt(tr^2 - 4*det)) / 2
471 InternalScalar a00 = out(diag, diag);
472 InternalScalar a01 = out(diag, diag + 1);
473 InternalScalar a10 = out(diag + 1, diag);
474 InternalScalar a11 = out(diag + 1, diag + 1);
475 InternalScalar tr = a00 + a11;
476 InternalScalar d = a00 * a11 - a01 * a10;
477 InternalScalar disc = tr * tr - 4 * d;
478 if (disc >= 0)
479 {
480 InternalScalar sq = sqrt(disc);
481 result(diag, 0) = Complex<T>{static_cast<T>((tr + sq) / 2), 0};
482 result(diag + 1, 0) =
483 Complex<T>{static_cast<T>((tr - sq) / 2), 0};
484 }
485 else
486 {
487 InternalScalar sq = sqrt(-disc);
488 result(diag, 0) =
489 Complex<T>{static_cast<T>(tr / 2), static_cast<T>(sq / 2)};
490 result(diag + 1, 0) =
491 Complex<T>{static_cast<T>(tr / 2), static_cast<T>(-sq / 2)};
492 }
493 diag++;
494 }
495 else
496 {
497 result(diag, 0) = Complex<T>{static_cast<T>(out(diag, diag)), 0};
498 }
499 }
500 return result;
501}
502
526// Algorithm: Eigenvalue Verification
527template <typename T, Size R, Size C>
528static inline constexpr bool checkEigenValues(
529 const Matrix<T, R, C> &a, const Matrix<Complex<T>, R, 1> &lambda,
530 const T thresh)
531{
532 T tr = trace(a);
534 for (Size row = 0; row < R; ++row)
535 {
536 sum_lambda = sum_lambda + lambda(row, 0);
537 }
538
539 if (abs(sum_lambda.real - tr) > thresh)
540 {
541 return false;
542 }
543 if (abs(sum_lambda.imag) > thresh)
544 {
545 return false;
546 }
547
548 if constexpr (R <= 4)
549 {
550 T d = determinant(a);
552 for (Size row = 0; row < R; ++row)
553 {
554 prod_lambda = prod_lambda * lambda(row, 0);
555 }
556 T det_tol = thresh * (static_cast<T>(1) + abs(d));
557 if (abs(prod_lambda.real - d) > det_tol)
558 {
559 return false;
560 }
561 if (abs(prod_lambda.imag) > det_tol)
562 {
563 return false;
564 }
565 }
566
567 return true;
568}
569
591// Algorithm: Inverse Iteration
592template <typename T, Size S>
594 const Matrix<T, S, S> &A, const Matrix<Complex<T>, S, 1> &eigenvalues)
595{
596 static_assert(is_float<T>(), "eigenvectors expects floating point type");
597 Matrix<Complex<T>, S, S> V{};
598
599 for (Size eig_col = 0; eig_col < S; ++eig_col)
600 {
602
603 // Form shifted matrix: (A - \lambda I)
605 for (Size row = 0; row < S; ++row)
606 {
607 for (Size col = 0; col < S; ++col)
608 {
609 shifted_A(row, col) = Complex<T>{A(row, col), 0};
610 if (row == col)
611 {
612 shifted_A(row, col) = shifted_A(row, col) - lambda;
613 }
614 }
615 }
616
617 // LU decomposition of the shifted matrix
618 LUMatrix<Complex<T>, S> lu_res = lu(shifted_A);
619
620 // Initial random vector b
621 // In a strict constexpr environment, we use a deterministic "random"
622 // vector (e.g., all 1s).
623 Matrix<Complex<T>, S, 1> b{};
624 for (Size row = 0; row < S; ++row)
625 {
626 b(row, 0) = Complex<T>{1.0, 0.0};
627 }
628
629 // Inverse iteration (usually 1 or 2 iterations is sufficient for
630 // convergence given that the eigenvalue is highly accurate).
631 for (Size iter = 0; iter < 2; ++iter)
632 {
633 b = lu_solve(lu_res, b);
634
635 // Safe normalization to prevent overflow during Euclidean norm
636 // calculation. First, scale by the maximum absolute component.
637 T max_val = 0;
638 for (Size row = 0; row < S; ++row)
639 {
640 T abs_real = abs(b(row, 0).real);
641 T abs_imag = abs(b(row, 0).imag);
642 if (abs_real > max_val)
643 {
645 }
646 if (abs_imag > max_val)
647 {
649 }
650 }
651
652 if (max_val > 0)
653 {
654 T inv_max = static_cast<T>(1) / max_val;
655 for (Size row = 0; row < S; ++row)
656 {
657 b(row, 0) = b(row, 0) * inv_max;
658 }
659 }
660
661 // Now compute Euclidean norm safely
662 T norm_sq = 0;
663 for (Size row = 0; row < S; ++row)
664 {
665 norm_sq = norm_sq + b(row, 0).real * b(row, 0).real +
666 b(row, 0).imag * b(row, 0).imag;
667 }
668 T norm = sqrt(norm_sq);
669
670 if (norm > 0)
671 {
672 T inv_norm = static_cast<T>(1) / norm;
673 for (Size row = 0; row < S; ++row)
674 {
675 b(row, 0) = b(row, 0) * inv_norm;
676 }
677 }
678 }
679
680 // Store the computed eigenvector in the eig_col-th column of V
681 for (Size row = 0; row < S; ++row)
682 {
683 V(row, eig_col) = b(row, 0);
684 }
685 }
686
687 return V;
688}
689
703template <typename T, Size S> class EigenSolver
704{
705 public:
708 : _evals(consteig::eigenvalues(mat)),
709 _evecs(consteig::eigenvectors(mat, _evals))
710 {
711 }
712
714 constexpr const Matrix<Complex<T>, S, 1> &eigenvalues() const
715 {
716 return _evals;
717 }
718
721 constexpr const Matrix<Complex<T>, S, S> &eigenvectors() const
722 {
723 return _evecs;
724 }
725
726 private:
727 Matrix<Complex<T>, S, 1> _evals;
728 Matrix<Complex<T>, S, S> _evecs;
729};
730
732
733} // namespace consteig
734
735#endif
Convenience class that computes both eigenvalues and eigenvectors.
Definition eigen.hpp:704
constexpr const Matrix< Complex< T >, S, S > & eigenvectors() const
Return the S×S eigenvector matrix (column eig_col corresponds to eigenvalue eig_col).
Definition eigen.hpp:721
constexpr const Matrix< Complex< T >, S, 1 > & eigenvalues() const
Return the S×1 eigenvalue column vector.
Definition eigen.hpp:714
constexpr EigenSolver(const Matrix< T, S, S > &mat)
Compute eigenvalues and eigenvectors of mat.
Definition eigen.hpp:707
Fixed-size matrix with compile-time dimensions.
Definition matrix.hpp:56
#define CONSTEIG_BALANCE_CONVERGENCE_THRESHOLD
Stopping criterion for the matrix balancing step.
Definition consteig_options.hpp:44
#define CONSTEIG_DEFAULT_SYMMETRIC_TOLERANCE
Symmetry routing threshold for eigensolver selection.
Definition consteig_options.hpp:33
#define CONSTEIG_MAX_ITER
Maximum QR iterations per eigenvalue block.
Definition consteig_options.hpp:21
constexpr PHMatrix< T, R > hess(Matrix< T, R, C > a)
Reduce a square matrix to upper Hessenberg form.
Definition hessenberg.hpp:75
constexpr Matrix< T, S, 1 > lu_solve(const LUMatrix< T, S > &lu, const Matrix< T, S, 1 > &b)
Solve the linear system Ax = b given the LU factorization of A.
Definition lu.hpp:138
constexpr LUMatrix< T, S > lu(const Matrix< T, S, S > &a)
LU decomposition with partial pivoting.
Definition lu.hpp:54
constexpr QRMatrix< T, R > qr_hessenberg(const Matrix< T, R, C > a)
QR decomposition optimized for upper Hessenberg matrices.
Definition qr.hpp:59
constexpr Matrix< T, S, S > eig(Matrix< T, S, S > a, const T symmetryTolerance=CONSTEIG_DEFAULT_SYMMETRIC_TOLERANCE)
Reduce a matrix to quasi-upper-triangular (Schur) form.
Definition eigen.hpp:379
constexpr Matrix< Complex< T >, S, 1 > eigenvalues(const Matrix< T, S, S > &a)
Compute the eigenvalues of a real square matrix.
Definition eigen.hpp:426
static constexpr bool checkEigenValues(const Matrix< T, R, C > &a, const Matrix< Complex< T >, R, 1 > &lambda, const T thresh)
Verify computed eigenvalues against matrix invariants.
Definition eigen.hpp:528
constexpr Matrix< Complex< T >, S, S > eigenvectors(const Matrix< T, S, S > &A, const Matrix< Complex< T >, S, 1 > &eigenvalues)
Compute eigenvectors given a matrix and its eigenvalues.
Definition eigen.hpp:593
constexpr T sqrt(const T x)
Constexpr square root.
Definition sqrt.hpp:67
constexpr T abs(const T x)
Absolute value of a real number.
Definition abs.hpp:17
constexpr T epsilon()
Machine epsilon for type T.
Definition utilities.hpp:82
constexpr T determinant(const Matrix< T, R, C > &mat)
Determinant via Laplace (cofactor) expansion.
Definition operations.hpp:323
constexpr T norm1(const Matrix< T, R, C > &mat)
1-norm (maximum absolute column sum).
Definition operations.hpp:235
constexpr T norm(const Matrix< T, R, C > &mat)
Frobenius (Euclidean) norm: sqrt(sum of squared elements).
Definition operations.hpp:211
constexpr T trace(const Matrix< T, R, C > &mat)
Trace: sum of diagonal elements.
Definition operations.hpp:370
constexpr T normInf(const Matrix< T, R, C > &mat)
Infinity-norm (maximum absolute row sum).
Definition operations.hpp:262
Constexpr complex number type.
Definition complex.hpp:26