numerics
Loading...
Searching...
No Matches
eig.cpp
Go to the documentation of this file.
1/// @file eigen/eig.cpp
2/// @brief Full symmetric eigendecomposition -- cyclic Jacobi algorithm
3///
4/// Reference: Golub & Van Loan "Matrix Computations" ยง8.4
5///
6/// Each sweep visits every off-diagonal pair (p,q) and applies a 2x2
7/// similarity (Givens) rotation that zeros A[p,q]. The rotation angle theta
8/// satisfies:
9///
10/// tan(2theta) = 2*A[p,q] / (A[q,q] - A[p,p])
11///
12/// which is solved as:
13/// tau = (A[q,q] - A[p,p]) / (2*A[p,q])
14/// t = sign(tau) / (|tau| + sqrt(1 + tau^2)) <- smaller root of t^2 + 2taut - 1 = 0
15/// c = 1/sqrt(1+t^2), s = c*t
16///
17/// After the rotation:
18/// A'[p,p] = c^2*A[p,p] - 2cs*A[p,q] + s^2*A[q,q]
19/// A'[q,q] = s^2*A[p,p] + 2cs*A[p,q] + c^2*A[q,q]
20/// A'[p,q] = A'[q,p] = 0 <- zeroed by construction
21/// A'[r,p] = c*A[r,p] - s*A[r,q] for r != p,q
22/// A'[r,q] = s*A[r,p] + c*A[r,q] for r != p,q
23///
24/// Eigenvectors accumulate: V' = V * G (G is the Givens matrix in (p,q) plane)
25
27#include <algorithm>
28#include <cmath>
29#include <stdexcept>
30
31namespace num {
32
34 if (A_in.rows() != A_in.cols())
35 throw std::invalid_argument("eig_sym: matrix must be square");
36
37 idx n = A_in.rows();
38 Matrix A = A_in; // working copy -- will be diagonalised
39 Matrix V(n, n, 0.0); // eigenvectors -- initialised to identity
40 for (idx i = 0; i < n; ++i) V(i,i) = 1.0;
41
42 idx sweeps = 0;
43 bool converged = false;
44
45 for (idx sweep = 0; sweep < max_sweeps; ++sweep) {
46 // Off-diagonal Frobenius norm (convergence check)
47 real off = 0;
48 for (idx p = 0; p < n; ++p)
49 for (idx q = p+1; q < n; ++q)
50 off += A(p,q) * A(p,q);
51
52 if (std::sqrt(2.0 * off) < tol) {
53 converged = true;
54 break;
55 }
56
57 // Cyclic sweep over all (p,q) pairs
58 for (idx p = 0; p < n-1; ++p) {
59 for (idx q = p+1; q < n; ++q) {
60 real apq = A(p,q);
61 if (std::abs(apq) < 1e-15) continue; // already negligible
62
63 real app = A(p,p), aqq = A(q,q);
64
65 // Jacobi rotation angle
66 real tau = (aqq - app) / (2.0 * apq);
67 real t = std::copysign(1.0, tau)
68 / (std::abs(tau) + std::sqrt(1.0 + tau*tau));
69 real c = 1.0 / std::sqrt(1.0 + t*t);
70 real s = c * t;
71
72 // Update diagonal and zero (p,q)
73 A(p,p) = c*c*app - 2.0*c*s*apq + s*s*aqq;
74 A(q,q) = s*s*app + 2.0*c*s*apq + c*c*aqq;
75 A(p,q) = A(q,p) = 0.0;
76
77 // Update off-diagonal rows/cols
78 for (idx r = 0; r < n; ++r) {
79 if (r == p || r == q) continue;
80 real arp = A(r,p), arq = A(r,q);
81 A(r,p) = A(p,r) = c*arp - s*arq;
82 A(r,q) = A(q,r) = s*arp + c*arq;
83 }
84
85 // Accumulate eigenvectors: V' = V * G
86 // G[:,p] = c*e_p - s*e_q, G[:,q] = s*e_p + c*e_q
87 for (idx r = 0; r < n; ++r) {
88 real vrp = V(r,p), vrq = V(r,q);
89 V(r,p) = c*vrp - s*vrq;
90 V(r,q) = s*vrp + c*vrq;
91 }
92 }
93 }
94 ++sweeps;
95 }
96
97 // Extract eigenvalues from diagonal
98 Vector values(n);
99 for (idx i = 0; i < n; ++i) values[i] = A(i,i);
100
101 // Sort eigenvalues ascending, permute eigenvector columns to match
102 for (idx i = 0; i < n-1; ++i) {
103 idx min_j = i;
104 for (idx j = i+1; j < n; ++j)
105 if (values[j] < values[min_j]) min_j = j;
106
107 if (min_j != i) {
108 std::swap(values[i], values[min_j]);
109 for (idx r = 0; r < n; ++r)
110 std::swap(V(r,i), V(r,min_j));
111 }
112 }
113
114 return {values, V, sweeps, converged};
115}
116
117} // namespace num
Dense row-major matrix with optional GPU storage.
Definition matrix.hpp:12
Full symmetric eigendecomposition via cyclic Jacobi sweeps.
double real
Definition types.hpp:10
constexpr T ipow(T x) noexcept
Compute x^N at compile time via repeated squaring.
std::size_t idx
Definition types.hpp:11
constexpr real e
Definition math.hpp:41
EigenResult eig_sym(const Matrix &A, real tol=1e-12, idx max_sweeps=100, Backend backend=Backend::seq)
Full eigendecomposition of a real symmetric matrix.
Full eigendecomposition result: A = V * diag(values) * V^T.