akreon-linalg
Fixed-size linear algebra and numerical methods in C++26.
Loading...
Searching...
No Matches
eig_decomp.hpp
Go to the documentation of this file.
1/*
2 * Copyright © 2025-2026 Akreon LLC
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6#pragma once
7
8#include <algorithm>
9#include <cmath>
10#include <limits>
11
13#include "qr.hpp"
14
15namespace linalg {
16
26template <SquareMatrix Mat>
28 // @brief alias for underlying type
30
31 // @brief aliasing complex and real types for eigen decomposition
33 using Complex = std::conditional_t<linalg::Complex<T>, T, std::complex<Real>>;
34
35 static constexpr std::size_t N = mat_rows_v<Mat>;
36 static_assert(N > 0, "Eigen decomposition requires a nonempty matrix");
37
44 return {.steps = 256,
45 .tol = Real(8) * std::numeric_limits<Real>::epsilon(),
46 .floor_eps = std::numeric_limits<Real>::min()};
47 }
48
55 constexpr explicit EigenDecomposition(const Mat& A) { solve_qr(A); }
56
64 constexpr explicit EigenDecomposition(
65 const Mat& A, const config::FactorConfig<Real>& config_in)
66 : config(config_in) {
67 solve_qr(A);
68 }
69
76 constexpr auto spectral_radius() const {
77 Complex rho(0.0);
78 for (std::size_t i = 0; i < N; ++i) {
79 const auto mag_rho = constexpr_sqrt_safe(rho.real() * rho.real() +
80 rho.imag() * rho.imag());
81 const auto mag_lambda =
83 lambda(i).imag() * lambda(i).imag());
85 }
86 return rho;
87 }
88
92 bool converged{false};
93
94private:
100 static constexpr Real spectral_sqrt(Real value) {
101 return constexpr_sqrt_safe(value, Real(0), Real(1),
102 Real(4) * std::numeric_limits<Real>::epsilon() *
103 value);
104 }
105
111 constexpr void solve_qr(const Mat& A) {
112 if (!std::isfinite(config.tol) || config.tol <= Real(0) ||
113 !std::isfinite(config.floor_eps) || config.floor_eps <= Real(0))
114 return;
115 for (std::size_t r = 0; r < N; ++r)
116 for (std::size_t c = 0; c < N; ++c)
117 if (!std::isfinite(constexpr_abs(A(r, c)))) return;
118
119 Real scale = 0;
120 for (std::size_t r = 0; r < N; ++r)
121 for (std::size_t c = 0; c < N; ++c)
122 scale = std::max(scale, constexpr_abs(A(r, c)));
123 // Normalize A by its largest entry; restore eigenvalue units at the end.
124 auto normalized = A;
125 if (scale > Real(0))
126 for (std::size_t r = 0; r < N; ++r)
127 for (std::size_t c = 0; c < N; ++c) normalized(r, c) /= scale;
128 // Reduce to H = U^* (A/scale) U; orders one and two are already Hessenberg.
129 auto [H, U] = [&] {
130 if constexpr (N > 2) {
131 const auto reduction = Hessenberg(normalized);
132 return std::tuple{reduction.H(), reduction.U()};
133 } else return std::tuple{normalized, make_identity<T, N>()};
134 }();
135 const Real threshold = config.tol;
136 const bool hermitian = [&] {
137 if constexpr (linalg::Real<T>)
139 else
141 threshold);
142 }();
143 Vector<Complex, N> eigs;
144
145 // Split blocks at negligible subdiagonals. Convergence requires triangular
146 // form, or isolated real 2x2 blocks representing conjugate eigenvalues.
147 auto deflate = [&] {
148 for (std::size_t r = 1; r < N; ++r)
149 if (constexpr_abs(H(r, r - 1)) <= threshold) H(r, r - 1) = T(0);
150 bool done = true;
151 for (std::size_t r = 1; r < N; ++r) {
152 if (H(r, r - 1) == T(0)) continue;
153 if constexpr (linalg::Complex<T>) done = false;
154 else {
155 const Real half_difference = (H(r - 1, r - 1) - H(r, r)) / 2;
156 const Real discriminant =
157 half_difference * half_difference + H(r - 1, r) * H(r, r - 1);
158 if (hermitian || discriminant >= 0 ||
159 (r > 1 && H(r - 1, r - 2) != T(0)) ||
160 (r + 1 < N && H(r + 1, r) != T(0)))
161 done = false;
162 }
163 }
164 return done;
165 };
166 converged = deflate();
167 for (std::size_t iteration = 0; !converged && iteration < config.steps;
168 ++iteration) {
169 // Locate the last unresolved block, skipping deflated scalar roots and
170 // isolated real blocks with conjugate eigenvalues.
171 std::size_t end = N;
172 while (end > 1 && H(end - 1, end - 2) == T(0)) --end;
173 if constexpr (linalg::Real<T>) {
174 while (!hermitian && end > 2 && H(end - 2, end - 3) == T(0)) {
175 const Real d = (H(end - 2, end - 2) - H(end - 1, end - 1)) / 2;
176 if (d * d + H(end - 2, end - 1) * H(end - 1, end - 2) >= 0) break;
177 end -= 2;
178 while (end > 1 && H(end - 1, end - 2) == T(0)) --end;
179 }
180 }
181 // Choose the trailing 2x2 root nearest the last diagonal as the shift.
182 T shift = H(end - 1, end - 1);
183 if constexpr (linalg::Real<T>) {
184 if (end > 1) {
185 const Real a = H(end - 2, end - 2), d = H(end - 1, end - 1);
186 const Real delta = (a - d) / 2;
187 const Real discriminant =
188 delta * delta + H(end - 2, end - 1) * H(end - 1, end - 2);
189 if (discriminant >= 0) {
190 const Real root = spectral_sqrt(discriminant);
191 shift = (a + d) / 2 + (delta >= 0 ? -root : root);
192 }
193 }
194 }
195 if constexpr (linalg::Complex<T>) {
196 if (end > 1) {
197 const Complex center =
198 (H(end - 2, end - 2) + H(end - 1, end - 1)) / Real(2);
199 const Complex delta =
200 (H(end - 2, end - 2) - H(end - 1, end - 1)) / Real(2);
201 const Complex disc =
202 delta * delta + H(end - 2, end - 1) * H(end - 1, end - 2);
203 const Real radius = constexpr_abs(disc);
204 const Complex root(
205 spectral_sqrt(
206 std::max(Real(0), (radius + disc.real()) / Real(2))),
207 (disc.imag() < Real(0) ? Real(-1) : Real(1)) *
208 spectral_sqrt(
209 std::max(Real(0), (radius - disc.real()) / Real(2))));
210 const Complex plus = center + root, minus = center - root;
212 ? plus
213 : minus;
214 }
215 }
216 auto shifted = H;
217 for (std::size_t j = 0; j < N; ++j) shifted(j, j) -= shift;
218 bool double_shift = false;
219 T trace = T(0), determinant = T(0);
220 if constexpr (linalg::Real<T>) {
221 if (end > 1 && !hermitian) {
222 const T a = H(end - 2, end - 2), d = H(end - 1, end - 1);
223 const T b = H(end - 2, end - 1), c = H(end - 1, end - 2);
224 double_shift = (a - d) * (a - d) / 4 + b * c < 0;
225 trace = a + d;
226 determinant = a * d - b * c;
227 }
228 }
229 if (double_shift) {
230 // Use the real quadratic H^2 - trace*H + determinant*I for a pair.
231 shifted = H * H - trace * H;
232 for (std::size_t j = 0; j < N; ++j) shifted(j, j) += determinant;
233 }
234 // Update H by QR similarity and accumulate its basis change in U.
235 const auto qr = QR(shifted);
236 const auto Q = qr.Q();
237 if (double_shift) H = transpose(Q) * (H * Q);
238 else {
239 H = qr.R() * Q;
240 for (std::size_t j = 0; j < N; ++j) H(j, j) += shift;
241 }
242 // Restore the exact Hessenberg zeros after the similarity update.
243 for (std::size_t r = 2; r < N; ++r)
244 for (std::size_t c = 0; c + 1 < r; ++c) H(r, c) = T(0);
245 U = U * Q;
246 converged = deflate();
247 }
248 if (!converged) return;
249
250 // Extract eigenvalues from diagonal entries and real 2x2 Schur blocks.
251 for (std::size_t j = 0; j < N; ++j) {
252 eigs(j) = Complex(H(j, j));
253 if constexpr (linalg::Real<T>) {
254 if (j + 1 < N && H(j + 1, j) != T(0)) {
255 const Real center = (H(j, j) + H(j + 1, j + 1)) / 2;
256 const Real delta = (H(j, j) - H(j + 1, j + 1)) / 2;
257 const Real discriminant = delta * delta + H(j, j + 1) * H(j + 1, j);
258 const Real imaginary = spectral_sqrt(-discriminant);
260 eigs(j + 1) = Complex(center, -imaginary);
261 ++j;
262 }
263 }
264 }
265 // Populate the spectrum used by Schur-block seeding and back substitution.
266 lambda = eigs;
267 if (hermitian) {
268 // Hermitian Schur form is diagonal; its eigenvectors are U.
269 V = as_complex_matrix(U);
270 // Sort coupled Hermitian eigenpairs; keep diagonal input order.
271 bool diagonal_input = true;
272 for (std::size_t r = 0; r < N; ++r)
273 for (std::size_t c = 0; c < N; ++c)
274 if (r != c && A(r, c) != T(0)) diagonal_input = false;
275 if (!diagonal_input) {
276 for (std::size_t j = 0; j < N; ++j) {
277 std::size_t largest = j;
278 for (std::size_t k = j + 1; k < N; ++k)
279 if (lambda(k).real() > lambda(largest).real()) largest = k;
280 std::swap(lambda(j), lambda(largest));
281 for (std::size_t r = 0; r < N; ++r) std::swap(V(r, j), V(r, largest));
282 }
283 }
284 for (std::size_t j = 0; j < N; ++j) lambda(j) *= scale;
285 return;
286 }
287
288 // Track columns assigned to repeated roots and generalized eigenvectors.
289 std::array<bool, N> used{false};
290
291 auto indices_equal = [&](const Vector<Complex, N>& D,
292 Complex val) -> std::array<int, N> {
293 std::array<int, N> idx;
294 idx.fill(-1);
295 for (int j = 0; j < int(N); ++j)
296 if (is_same(D(j), val)) idx[j] = j;
297 return idx;
298 };
299
300 auto take_last_unused = [&](std::array<int, N>& slots) -> int {
301 for (int j = 0; j < int(N); ++j) {
302 if (!used[j] && slots[j] != -1) {
303 used[j] = true;
304 return j;
305 }
306 }
307 return -1;
308 };
309
310 // Solve in Schur coordinates from the last block upward, then map by U.
311 for (int i = static_cast<int>(N) - 1; i >= 0; --i) {
312 if (used[i]) continue;
313
314 // Estimate the Jordan defect from multiplicity and nullity.
315 int AM = 0;
316 std::for_each(eigs.begin(), eigs.end(), [&](const auto& val) {
317 AM += constexpr_abs(eigs(i) - val) < config.tol;
318 });
319 int GM = 0, defect = 0;
320 if (AM > 1) {
321 const auto I = make_identity<Complex, N>();
322 GM = N - QR(as_complex_matrix(normalized) - eigs(i) * I).rank();
323 defect = std::max(0, AM - GM);
324 }
325
326 if (0 == defect) {
327 Vector<Complex, N> x;
328
329 // Recover ordinary eigenvectors when no Jordan defect is detected.
330 if (linalg::Real<T> && eigs(i).imag() != Real(0) && i > 0) {
331 // Seed the Schur block and solve (H - lambda_i I)x = 0.
332 seed_complex_block(H, i, x);
333 back_sub_1x1_block(H, i, x);
334
335 // Map both conjugate eigenvectors to the original coordinates.
336 auto v = map_to_A(U, x);
337 for (std::size_t j = 0; j < N; ++j) V(j, i) = v(j);
338
339 i--;
340
341 v = map_to_A(U, conjugate(x));
342 for (std::size_t j = 0; j < N; ++j) V(j, i) = v(j);
343
344 lambda(i) = eigs(i);
345
346 used[i] = true;
347 } else {
348 // Seed the Schur block and solve (H - lambda_i I)x = 0.
349 seed_real_block(i, x);
350 back_sub_1x1_block(H, i, x);
351
352 // Map the Schur-coordinate eigenvector by U.
353 auto v = map_to_A(U, x);
354 for (std::size_t j = 0; j < N; ++j) V(j, i) = v(j);
355
356 lambda(i) = eigs(i);
357
358 used[i] = true;
359 }
360 } else {
361 // Build generalized eigenvectors for a defective repeated root.
362 if (linalg::Real<T> && eigs(i).imag() != Real(0) && i > 0) {
363 // Bound the chain length by the defect and available Schur columns.
364 const int chain_len = std::min(1 + defect, (i + 1) / 2);
366
367 // Start the chain with a solution of (H - lambda_i I)x_0 = 0.
368 seed_complex_block(H, i, chain(0));
369 back_sub_1x1_block(H, i, chain(0));
370
371 // Extend the chain with (H - lambda_i I)x_k = x_{k-1}.
372 for (int k = 1; k < chain_len; ++k)
373 back_sub_shifted_rhs(H, lambda(i), chain(k - 1), chain(k));
374
375 // Assign mapped chain vectors to matching eigenvalue columns.
376 auto slots = indices_equal(eigs, eigs(i));
377
378 for (int k = 0; k < chain_len; ++k) {
379 const auto slot_idx = take_last_unused(slots);
380 if (slot_idx < 0) break;
381
382 auto v = map_to_A(U, chain(k));
383
384 for (std::size_t j = 0; j < N; ++j) V(j, slot_idx) = v(j);
385
386 lambda(slot_idx) = eigs(i);
387 }
388
389 // Map the conjugate chain to columns for the conjugate root.
390 auto slots_conj = indices_equal(eigs, std::conj(eigs(i)));
391
392 for (int k = 0; k < chain_len; ++k) {
394 if (slot_idx < 0) break;
395
396 auto vbar = map_to_A(U, conjugate(chain(k)));
397
398 for (std::size_t j = 0; j < N; ++j) V(j, slot_idx) = vbar(j);
399
400 lambda(slot_idx) = std::conj(eigs(i));
401 }
402 } else {
403 // Bound the chain length by the defect and available Schur columns.
404 const int chain_len = std::min(1 + defect, 1 + i);
406
407 // Start the chain with a solution of (H - lambda_i I)x_0 = 0.
408 seed_real_block(i, chain(0));
409 back_sub_1x1_block(H, i, chain(0));
410
411 // Extend the chain with (H - lambda_i I)x_k = x_{k-1}.
412 for (int k = 1; k < chain_len; ++k)
413 back_sub_shifted_rhs(H, eigs(i), chain(k - 1), chain(k));
414
415 // Map the chain to the original coordinates.
416 auto slots = indices_equal(eigs, eigs(i));
417
418 for (int k = 0; k < chain_len; ++k) {
419 const auto slot_idx = take_last_unused(slots);
420 if (slot_idx < 0) break;
421
422 auto v = map_to_A(U, chain(k));
423
424 for (std::size_t j = 0; j < N; ++j) V(j, slot_idx) = v(j);
425
426 lambda(slot_idx) = eigs(i);
427 }
428 }
429 }
430 }
431 for (std::size_t j = 0; j < N; ++j) lambda(j) *= scale;
432 }
433
443 constexpr void back_sub_1x1_block(const Mat& H, int idx,
444 Vector<Complex, N>& x) {
445 // Start above the seeded scalar or 2x2 block and solve toward the first
446 // row.
447 for (int r = ((linalg::Real<T> && lambda(idx).imag() != Real(0)) ? idx - 2
448 : idx - 1);
449 r >= 0; --r) {
450 Complex rhs(0);
451 for (int c = r + 1; c <= idx; ++c) rhs -= Complex(H(r, c)) * x(c);
452 if constexpr (linalg::Real<T>) {
453 if (r > 0 && H(r, r - 1) != T(0)) {
454 // Solve the coupled 2x2 system for the preceding conjugate block.
456 for (int c = r + 1; c <= idx; ++c)
457 upper_rhs -= Complex(H(r - 1, c)) * x(c);
458 const Complex a = Complex(H(r - 1, r - 1)) - lambda(idx);
459 const Complex b = Complex(H(r - 1, r));
460 const Complex c = Complex(H(r, r - 1));
461 const Complex d = Complex(H(r, r)) - lambda(idx);
462 const Complex det = a * d - b * c;
463 if (det != Complex(0)) {
464 x(r - 1) = (d * upper_rhs - b * rhs) / det;
465 x(r) = (a * rhs - c * upper_rhs) / det;
466 }
467 --r;
468 continue;
469 }
470 }
471 const auto denom = Complex(H(r, r)) - lambda(idx);
472 x(r) = denom == Complex(0) ? Complex(0) : rhs / denom;
473 }
474 }
475
484 constexpr void back_sub_shifted_rhs(const Mat& H, const Complex& eig,
485 const Vector<Complex, N>& rhs,
486 Vector<Complex, N>& x) {
487 // Solve (H - eig*I)x = rhs from the last row upward.
488 for (int r = int(N) - 1; r >= 0; --r) {
489 Complex sum(0);
490 for (int c = r + 1; c < int(N); ++c) sum += H(r, c) * x(c);
491 const auto denom = Complex(H(r, r)) - eig;
492 if (denom == Complex(0)) x(r) = Complex(0);
493 else x(r) = (rhs(r) - sum) / denom;
494 }
495 }
496
504 constexpr void seed_complex_block(const Mat& H, int idx,
505 Vector<Complex, N>& x) {
506 const auto& a = H(idx - 1, idx - 1);
507 const auto& b = H(idx - 1, idx);
508 const auto& c = H(idx, idx - 1);
509 const auto& d = H(idx, idx);
510
511 // Choose between two null vectors of the 2x2 block using the larger pivot.
512 const Complex la = lambda(idx) - Complex(a);
513 Complex z0, z1;
514 if (L2<Vector<T, 1>>()(Vector<T, 1>{b}) >= L2<Vector<Complex, 1>>()(la)) {
515 // Use [b; lambda - a]
516 z0 = Complex(b);
517 z1 = la;
518 } else {
519 // Use [lambda - d; c]
520 z0 = lambda(idx) - Complex(d);
521 z1 = Complex(c);
522 }
524 L2<Vector<Complex, 1>>()(z1));
525 if (s > Real(0)) {
526 z0 /= s;
527 z1 /= s;
528 }
529
530 x(idx - 1) = z0;
531 x(idx) = z1;
532 }
533
540 constexpr void seed_real_block(int idx, Vector<Complex, N>& x) {
541 for (std::size_t i = 0; i < N; ++i) x(i) = Complex(0);
542 x(idx) = Complex(1);
543 }
544
553 constexpr auto map_to_A(const Mat& U, const Vector<Complex, N>& x) {
554 Vector<Complex, N> v;
555
556 for (std::size_t j = 0; j < N; ++j) {
557 Complex sum(0);
558 for (std::size_t k = 0; k < N; ++k) sum += Complex(U(j, k)) * x(k);
559 v(j) = sum;
560 }
561
562 // Normalize v = U*x with a lower bound on its norm.
563 const auto norm = std::max(L2<Vector<Complex, N>>()(v), config.floor_eps);
564 return T(1) / norm * v;
565 }
566
569};
570
571} // namespace linalg
Field classified by scalar_traits as complex.
Field classified by scalar_traits as real.
typename scalar_traits_t< T >::real_type real_type_t
Associated real component type registered for T.
constexpr auto as_complex_matrix(const Mat &A)
express matrix as complex one
constexpr auto conjugate_transpose(const Mat &A)
conjugate transpose of a matrix
constexpr auto transpose(const Vec &x)
tranpose of a vector
constexpr T constexpr_abs(T x)
Compute real absolute value or complex magnitude.
constexpr T constexpr_sqrt_safe(T x, T floor=T(0), T g=1e-10, T tol=1e-10)
Return a scalar square root with a sanitized fallback radicand.
constexpr bool is_same(const T &a, const T &b, real_type_t< T > tol=1e-10)
Compare complex scalars by magnitude of their difference.
constexpr std::size_t mat_rows_v
Registered compile-time row count after cv/ref normalization.
constexpr auto max(const Vec &x)
max element
constexpr auto conjugate(const Vec &x)
conjugate a vector
constexpr bool all_equal(const Vec &X, const Vec &Y, real_type_t< vector_element_t< Vec > > tol=1e-10)
checks if two vectors are equivalent to a given tolerance (different than == operator on vector which...
constexpr auto trace(const Mat &A)
trace of a matrix
typename mat_traits< remove_cvref_t< Mat > >::value_type matrix_scalar_t
Registered matrix scalar type after cv/ref normalization.
Configuration for generic numerical factorizations.
std::size_t steps
total number of iter
Computes eigenvalues and eigenvectors satisfying A V = V diag(lambda).
Matrix< Complex, N, N > V
eigenvectors
matrix_scalar_t< Mat > T
static constexpr config::FactorConfig< Real > default_config()
Construct the nominal shifted-QR configuration.
bool converged
eigenvalue convergence flag
Vector< Complex, N > lambda
eigendecomposition member variables
constexpr EigenDecomposition(const Mat &A, const config::FactorConfig< Real > &config_in)
Compute eigenvalues and eigenvectors with explicit settings.
constexpr auto spectral_radius() const
Select the eigenvalue whose magnitude is the spectral radius.
static constexpr std::size_t N
constexpr EigenDecomposition(const Mat &A)
Compute eigenvalues and eigenvectors using the nominal configuration.
std::conditional_t< linalg::Complex< T >, T, std::complex< Real > > Complex
Owning, fixed-size, row-major matrix container over a scalar field.
Definition matrix.hpp:44
Owning, fixed-size vector container.
Definition vector.hpp:36