numerics
Loading...
Searching...
No Matches
svd.cpp
Go to the documentation of this file.
1/// @file svd/svd.cpp
2/// @brief SVD via one-sided Jacobi + randomized truncated SVD
3///
4/// -- One-sided Jacobi SVD --------------------------------------------------
5///
6/// Apply Givens rotations G to the columns of A from the right until the
7/// columns are mutually orthogonal. Each rotation zeros (AᵀA)[p,q]:
8///
9/// Given col_p and col_q of the working matrix A:
10/// alpha = col_p * col_p, beta = col_q * col_q, gamma = col_p * col_q
11/// zeta = (beta - alpha) / (2gamma)
12/// t = sign(zeta) / (|zeta| + sqrt(1 + zeta^2)) <- same formula as Jacobi eig
13/// c = 1/sqrt(1+t^2), s = c*t
14///
15/// col_p' = c*col_p + s*col_q
16/// col_q' = -s*col_p + c*col_q
17/// V[:,p]' = c*V[:,p] + s*V[:,q] <- accumulate right sing. vecs
18/// V[:,q]' = -s*V[:,p] + c*V[:,q]
19///
20/// After convergence:
21/// S[i] = ||A[:,i]||, U[:,i] = A[:,i]/S[i]
22///
23/// -- Randomized SVD --------------------------------------------------------
24///
25/// Halko, Martinsson, Tropp (2011) "Finding Structure with Randomness".
26/// The key insight: if Y = A*Omega has range ~= range(A), a small SVD of Qᵀ*A
27/// gives the dominant singular triplets cheaply.
28
29#include "linalg/svd/svd.hpp"
31#include <algorithm>
32#include <cmath>
33#include <stdexcept>
34
35namespace num {
36
37// Full SVD -- one-sided Jacobi
38
40 constexpr real tiny = 1e-300;
41 idx m = A_in.rows(), n = A_in.cols();
42 idx r = std::min(m, n); // rank of economy SVD
43
44 // Work on a copy; rotate columns in-place
45 Matrix A = A_in;
46
47 // V accumulates right singular vectors (n x n), initialised to identity
48 Matrix V(n, n, 0.0);
49 for (idx i = 0; i < n; ++i) V(i,i) = 1.0;
50
51 idx sweeps = 0;
52 bool converged = false;
53
54 for (idx sweep = 0; sweep < max_sweeps; ++sweep) {
55 // Max relative off-orthogonality across column pairs
56 real max_cos = 0;
57 for (idx p = 0; p < r-1; ++p) {
58 for (idx q = p+1; q < r; ++q) {
59 // Inner products of columns p and q
60 real alpha = 0, beta = 0, gamma = 0;
61 for (idx i = 0; i < m; ++i) {
62 alpha += A(i,p) * A(i,p);
63 beta += A(i,q) * A(i,q);
64 gamma += A(i,p) * A(i,q);
65 }
66 if (alpha < tiny || beta < tiny) continue;
67
68 real cos_pq = std::abs(gamma) / std::sqrt(alpha * beta);
69 max_cos = std::max(max_cos, cos_pq);
70
71 if (cos_pq < tol) continue; // already orthogonal enough
72
73 // Jacobi rotation angle (identical structure to eig_sym)
74 real zeta = (beta - alpha) / (2.0 * gamma);
75 real t = std::copysign(1.0, zeta)
76 / (std::abs(zeta) + std::sqrt(1.0 + zeta*zeta));
77 real c = 1.0 / std::sqrt(1.0 + t*t);
78 real s = c * t;
79
80 // Update columns of A
81 for (idx i = 0; i < m; ++i) {
82 real aip = A(i,p), aiq = A(i,q);
83 A(i,p) = c*aip - s*aiq;
84 A(i,q) = s*aip + c*aiq;
85 }
86
87 // Accumulate right singular vectors V
88 for (idx i = 0; i < n; ++i) {
89 real vip = V(i,p), viq = V(i,q);
90 V(i,p) = c*vip - s*viq;
91 V(i,q) = s*vip + c*viq;
92 }
93 }
94 }
95
96 ++sweeps;
97 if (max_cos < tol) { converged = true; break; }
98 }
99
100 // Singular values = column norms; left singular vectors = normalised cols
101 Vector S(r);
102 Matrix U(m, r, 0.0);
103 for (idx j = 0; j < r; ++j) {
104 real nrm = 0;
105 for (idx i = 0; i < m; ++i) nrm += A(i,j)*A(i,j);
106 S[j] = std::sqrt(nrm);
107 if (S[j] > tiny)
108 for (idx i = 0; i < m; ++i) U(i,j) = A(i,j) / S[j];
109 }
110
111 // Sort singular values descending; permute U, V columns to match
112 for (idx i = 0; i < r-1; ++i) {
113 idx max_j = i;
114 for (idx j = i+1; j < r; ++j)
115 if (S[j] > S[max_j]) max_j = j;
116
117 if (max_j != i) {
118 std::swap(S[i], S[max_j]);
119 for (idx k = 0; k < m; ++k) std::swap(U(k,i), U(k,max_j));
120 for (idx k = 0; k < n; ++k) std::swap(V(k,i), V(k,max_j));
121 }
122 }
123
124 // Return economy Vt (r x n): transpose columns of V[:, 0..r-1]
125 Matrix Vt(r, n, 0.0);
126 for (idx i = 0; i < r; ++i)
127 for (idx j = 0; j < n; ++j)
128 Vt(i,j) = V(j,i);
129
130 return {U, S, Vt, sweeps, converged};
131}
132
133// Randomized truncated SVD -- Halko, Martinsson, Tropp (2011)
134//
135// Steps:
136// 1. Draw Omega in R^{nx(k+p)} (Gaussian random sketch)
137// 2. Y = A * Omega
138// 3. Q, _ = QR(Y) (orthonormal basis for approx. range of A)
139// 4. B = Qᵀ * A (project to small k-dimensional subspace)
140// 5. Ũ, Sigma, Vᵀ = svd(B) (cheap: B is (k+p)xn, small)
141// 6. U = Q * Ũ (lift back to full dimension)
142
144 Backend backend, idx oversampling, Rng* rng) {
145 const idx m = A.rows(), n = A.cols();
146 if (k == 0 || k > std::min(m, n))
147 throw std::invalid_argument("svd_truncated: k out of range");
148
149 const idx l = k + oversampling; // sketch width
150
151 // Optional local RNG if none supplied
153 if (!rng) rng = &local_rng;
154
155 // 1. Gaussian sketch matrix Omega in R^{nxl}
156 Matrix Omega(n, l);
157 for (idx j = 0; j < l; ++j)
158 for (idx i = 0; i < n; ++i)
159 Omega(i,j) = rng_normal(rng, 0.0, 1.0);
160
161 // 2. Y = A * Omega (m x l)
162 Matrix Y(m, l, 0.0);
163 matmul(A, Omega, Y, backend);
164
165 // 3. Q = QR(Y).Q (m x l, orthonormal columns)
166 QRResult qr_res = qr(Y);
167 const Matrix& Q = qr_res.Q; // m x m; we only need first l columns
168
169 // 4. B = Qᵀ * A (l x n): B[i,j] = sum_k Q[k,i] * A[k,j]
170 Matrix B(l, n, 0.0);
171 for (idx i = 0; i < l; ++i)
172 for (idx kk = 0; kk < m; ++kk) {
173 const real q_ki = Q(kk, i);
174 for (idx j = 0; j < n; ++j)
175 B(i,j) += q_ki * A(kk,j);
176 }
177
178 // 5. SVD of B (small: l x n)
179 SVDResult small = svd(B, backend);
180
181 // 6. U = Q * Ũ[:, 0..k-1] (m x k)
182 Matrix U(m, k, 0.0);
183 for (idx j = 0; j < k; ++j)
184 for (idx i = 0; i < m; ++i)
185 for (idx ii = 0; ii < l; ++ii)
186 U(i,j) += Q(i,ii) * small.U(ii,j);
187
188 // Slice top-k from small SVD
189 Vector S(k);
190 for (idx i = 0; i < k; ++i) S[i] = small.S[i];
191
192 Matrix Vt(k, n, 0.0);
193 for (idx i = 0; i < k; ++i)
194 for (idx j = 0; j < n; ++j)
195 Vt(i,j) = small.Vt(i,j);
196
197 return {U, S, Vt, 0, true};
198}
199
200} // namespace num
Dense row-major matrix with optional GPU storage.
Definition matrix.hpp:12
SVDResult svd_truncated(const Matrix &A, idx k, Backend backend=default_backend, idx oversampling=10, Rng *rng=nullptr)
Randomized truncated SVD – top-k singular triplets.
Definition svd.cpp:143
double real
Definition types.hpp:10
Backend
Selects which backend handles a linalg operation.
Definition policy.hpp:19
real beta(real a, real b)
B(a, b) – beta function.
Definition math.hpp:242
constexpr T ipow(T x) noexcept
Compute x^N at compile time via repeated squaring.
std::size_t idx
Definition types.hpp:11
QRResult qr(const Matrix &A)
QR factorization of an mxn matrix A (m >= n) via Householder reflections.
Definition qr.cpp:41
real rng_normal(Rng *r, real mean, real stddev)
Normal (Gaussian) sample with given mean and standard deviation.
Definition math.hpp:289
constexpr real e
Definition math.hpp:41
real zeta(real x)
zeta(x) – Riemann zeta function
Definition math.hpp:233
SVDResult svd(const Matrix &A, Backend backend=Backend::seq, real tol=1e-12, idx max_sweeps=100)
Full SVD of an mxn matrix via one-sided Jacobi.
Definition svd.cpp:39
void matmul(const Matrix &A, const Matrix &B, Matrix &C, Backend b=default_backend)
C = A * B.
Definition matrix.cpp:83
QR factorization via Householder reflections.
Result of a QR factorization: A = Q * R.
Definition qr.hpp:18
Seeded pseudo-random number generator (Mersenne Twister). Pass a pointer to rng_* functions to draw s...
Definition math.hpp:274
Result of a Singular Value Decomposition: A = U * diag(S) * Vᵀ
Definition svd.hpp:41
Singular Value Decomposition – dense and randomized truncated.