numerics
Loading...
Searching...
No Matches
jacobi_eig.hpp
Go to the documentation of this file.
1/// @file eigen/jacobi_eig.hpp
2/// @brief Full symmetric eigendecomposition via cyclic Jacobi sweeps
3///
4/// Computes ALL eigenvalues and eigenvectors of a real symmetric matrix.
5///
6/// Algorithm -- cyclic Jacobi:
7/// Repeatedly apply plane (Givens) rotations in the (p,q) plane to zero
8/// the off-diagonal element A[p,q]. Each rotation is a similarity transform
9/// so eigenvalues are preserved. Convergence is quadratic: after each full
10/// sweep the sum of squared off-diagonal elements decreases by at least a
11/// constant factor.
12///
13/// Why Jacobi instead of the implicit QR algorithm?
14/// Jacobi is conceptually simpler and each rotation is the exact same
15/// 2x2 eigendecomposition students encounter first. For dense n < 1000 the
16/// performance is acceptable. LAPACK uses the Francis QR algorithm (dsyev)
17/// which is O(n^2) per iteration vs O(n^2) per sweep here, but harder to
18/// explain.
19///
20/// Complexity: O(n^2) per sweep, O(n) sweeps typical -> O(n^3) total.
21#pragma once
22
23#include "core/matrix.hpp"
24#include "core/vector.hpp"
25#include "core/policy.hpp"
26#include <cmath>
27#include <algorithm>
28#include <stdexcept>
29
30namespace num {
31
32/// @brief Full eigendecomposition result: A = V * diag(values) * V^T
34 Vector values; ///< Eigenvalues in ascending order
35 Matrix vectors; ///< Eigenvectors as columns of an nxn orthogonal matrix
36 idx sweeps; ///< Number of Jacobi sweeps performed
37 bool converged; ///< Whether off-diagonal norm fell below tol
38};
39
40/// @brief Full eigendecomposition of a real symmetric matrix.
41///
42/// The rotation accumulation loop is parallelised when backend == Backend::omp.
43///
44/// @param A Real symmetric nxn matrix (upper/lower triangle used)
45/// @param backend Execution backend (Backend::omp parallelises the rotation loop)
46/// @param tol Convergence: stop when Frobenius norm of off-diagonal < tol
47/// @param max_sweeps Safety cap on Jacobi sweeps (typically < 20 for n < 500)
48/// @return EigenResult with eigenvalues in ascending order and corresponding
49/// eigenvectors as columns of the V matrix (A = V diag(lambda) Vᵀ)
51 real tol = 1e-12, idx max_sweeps = 100,
52 Backend backend = Backend::seq) {
53 if (A.rows() != A.cols())
54 throw std::invalid_argument("eig_sym: matrix must be square");
55
56 constexpr real rotation_tol = 1e-15;
57 idx n = A.rows();
58 Matrix Aw = A;
59 Matrix V(n, n, 0.0);
60 for (idx i = 0; i < n; ++i) V(i,i) = 1.0;
61
62 idx sweeps = 0;
63 bool converged = false;
64
65 for (idx sweep = 0; sweep < max_sweeps; ++sweep) {
66 real off = 0;
67 for (idx p = 0; p < n; ++p)
68 for (idx q = p+1; q < n; ++q)
69 off += Aw(p,q) * Aw(p,q);
70
71 if (std::sqrt(2.0 * off) < tol) { converged = true; break; }
72
73 for (idx p = 0; p < n-1; ++p) {
74 for (idx q = p+1; q < n; ++q) {
75 real apq = Aw(p,q);
76 if (std::abs(apq) < rotation_tol) continue;
77
78 real app = Aw(p,p), aqq = Aw(q,q);
79 real tau = (aqq - app) / (2.0 * apq);
80 real t = std::copysign(1.0, tau)
81 / (std::abs(tau) + std::sqrt(1.0 + tau*tau));
82 real c = 1.0 / std::sqrt(1.0 + t*t);
83 real s = c * t;
84
85 Aw(p,p) = c*c*app - 2.0*c*s*apq + s*s*aqq;
86 Aw(q,q) = s*s*app + 2.0*c*s*apq + c*c*aqq;
87 Aw(p,q) = Aw(q,p) = 0.0;
88
89 // Update off-diagonal rows/cols; parallel when omp backend
90#ifdef NUMERICS_HAS_OMP
91 #pragma omp parallel for schedule(static) if(backend == Backend::omp)
92#endif
93 for (idx r = 0; r < n; ++r) {
94 if (r == p || r == q) continue;
95 real arp = Aw(r,p), arq = Aw(r,q);
96 Aw(r,p) = Aw(p,r) = c*arp - s*arq;
97 Aw(r,q) = Aw(q,r) = s*arp + c*arq;
98 }
99#ifdef NUMERICS_HAS_OMP
100 #pragma omp parallel for schedule(static) if(backend == Backend::omp)
101#endif
102 for (idx r = 0; r < n; ++r) {
103 real vrp = V(r,p), vrq = V(r,q);
104 V(r,p) = c*vrp - s*vrq;
105 V(r,q) = s*vrp + c*vrq;
106 }
107 }
108 }
109 ++sweeps;
110 }
111
112 Vector values(n);
113 for (idx i = 0; i < n; ++i) values[i] = Aw(i,i);
114
115 for (idx i = 0; i < n-1; ++i) {
116 idx min_j = i;
117 for (idx j = i+1; j < n; ++j)
118 if (values[j] < values[min_j]) min_j = j;
119 if (min_j != i) {
120 std::swap(values[i], values[min_j]);
121 for (idx r = 0; r < n; ++r)
122 std::swap(V(r,i), V(r,min_j));
123 }
124 }
125
126 return {values, V, sweeps, converged};
127}
128
129} // namespace num
Dense row-major matrix with optional GPU storage.
Definition matrix.hpp:12
Matrix operations.
double real
Definition types.hpp:10
Backend
Selects which backend handles a linalg operation.
Definition policy.hpp:19
@ seq
Naive textbook loops – always available.
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.
Backend enum for linear algebra operations.
Full eigendecomposition result: A = V * diag(values) * V^T.
Vector values
Eigenvalues in ascending order.
bool converged
Whether off-diagonal norm fell below tol.
idx sweeps
Number of Jacobi sweeps performed.
Matrix vectors
Eigenvectors as columns of an nxn orthogonal matrix.
Vector operations.