multicalc/linear_algebra/svd.rs
1//! Singular value decomposition by one-sided Jacobi, for tall or square matrices.
2//!
3//! The method follows Golub & Van Loan, *Matrix Computations*, and Demmel & Veselić for high
4//! relative accuracy — a fixed-size `no_std` implementation on this crate's own
5//! [`Vector`] and [`Matrix`] types. Reference values for the tests come from numpy/LAPACK.
6
7use crate::linear_algebra::{Matrix, Vector};
8use crate::scalar::Numeric;
9use crate::utils::error_codes::CalcError;
10
11/// A thin singular value decomposition `A = U · diag(σ) · Vᵀ` for a matrix with `M ≥ N`.
12///
13/// `u` has orthonormal columns, `singular_values` holds the σ in descending order (all ≥ 0), and
14/// `v` has orthonormal columns.
15#[derive(Debug, Clone, Copy)]
16#[must_use]
17pub struct Svd<const M: usize, const N: usize, T = f64> {
18 /// Left factor `U` with orthonormal columns.
19 pub(crate) u: Matrix<M, N, T>,
20 /// Singular values in descending order.
21 pub(crate) singular_values: Vector<N, T>,
22 /// Right factor `V` with orthonormal columns.
23 pub(crate) v: Matrix<N, N, T>,
24}
25
26impl<const M: usize, const N: usize, T: Numeric> Matrix<M, N, T> {
27 /// Decomposes `self` as `U · diag(σ) · Vᵀ` by one-sided Jacobi (thin form, `M ≥ N`).
28 ///
29 /// `U` has orthonormal columns, the σ are non-negative and descending, and `V` has orthonormal
30 /// columns. Returns [`CalcError::Underdetermined`] for a wide matrix (`M < N`) — transpose it
31 /// first — or [`CalcError::NonFiniteValue`] if any entry is not finite.
32 ///
33 /// ```
34 /// use multicalc::linear_algebra::Matrix;
35 /// let a = Matrix::<3, 2>::new([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]]);
36 /// let svd = a.svd().unwrap();
37 /// let (u, s, v) = (svd.u(), svd.singular_values(), svd.v());
38 /// // U · diag(σ) · Vᵀ == A.
39 /// for r in 0..3 {
40 /// for c in 0..2 {
41 /// let mut acc = 0.0;
42 /// for k in 0..2 {
43 /// acc += u[(r, k)] * s[k] * v[(c, k)];
44 /// }
45 /// assert!((acc - a[(r, c)]).abs() < 1e-12);
46 /// }
47 /// }
48 /// ```
49 ///
50 /// A wide matrix (`M < N`) has no thin form here; take the SVD of its transpose, whose singular
51 /// values are the same. Its pseudo-inverse then follows from `A⁺ = ((Aᵀ)⁺)ᵀ`, which
52 /// [`Matrix::pseudo_inverse`] applies for any shape.
53 ///
54 /// ```
55 /// use multicalc::linear_algebra::Matrix;
56 /// // For a wide matrix, decompose its transpose.
57 /// let a = Matrix::<2, 3>::new([[1.0, 0.0, 2.0], [0.0, 1.0, 1.0]]);
58 /// let at = a.transpose();
59 /// let svd = at.svd().unwrap();
60 /// let (u, s, v) = (svd.u(), svd.singular_values(), svd.v());
61 /// for r in 0..3 {
62 /// for c in 0..2 {
63 /// let mut acc = 0.0;
64 /// for k in 0..2 {
65 /// acc += u[(r, k)] * s[k] * v[(c, k)];
66 /// }
67 /// assert!((acc - at[(r, c)]).abs() < 1e-12);
68 /// }
69 /// }
70 /// ```
71 pub fn svd(self) -> Result<Svd<M, N, T>, CalcError> {
72 if M < N {
73 return Err(CalcError::Underdetermined);
74 }
75 for r in 0..M {
76 for c in 0..N {
77 if !self[(r, c)].is_finite() {
78 return Err(CalcError::NonFiniteValue);
79 }
80 }
81 }
82
83 let mut u = self;
84 let mut v = Matrix::<N, N, T>::identity();
85
86 // One-sided Jacobi: rotate column pairs of U until its columns are orthogonal.
87 let max_sweeps = 60;
88 for _ in 0..max_sweeps {
89 let mut off_max = T::ZERO;
90 for p in 0..N {
91 for q in (p + 1)..N {
92 let cp = u.column(p);
93 let cq = u.column(q);
94 let alpha = cp.norm_squared();
95 let beta = cq.norm_squared();
96 let gamma = cp.dot(cq);
97 if alpha == T::ZERO || beta == T::ZERO {
98 continue;
99 }
100 let scale = (alpha * beta).sqrt();
101 let off = gamma.abs() / scale;
102 if off > off_max {
103 off_max = off;
104 }
105 if gamma.abs() <= T::EPSILON * scale {
106 continue;
107 }
108 // Rotation that makes columns p and q orthogonal.
109 let zeta = (beta - alpha) / (T::TWO * gamma);
110 let sign = if zeta < T::ZERO { -T::ONE } else { T::ONE };
111 let t = sign / (zeta.abs() + (T::ONE + zeta * zeta).sqrt());
112 let c = T::ONE / (T::ONE + t * t).sqrt();
113 let s = c * t;
114 for i in 0..M {
115 let up = u[(i, p)];
116 let uq = u[(i, q)];
117 u[(i, p)] = c * up - s * uq;
118 u[(i, q)] = s * up + c * uq;
119 }
120 for i in 0..N {
121 let vp = v[(i, p)];
122 let vq = v[(i, q)];
123 v[(i, p)] = c * vp - s * vq;
124 v[(i, q)] = s * vp + c * vq;
125 }
126 }
127 }
128 if off_max <= T::EPSILON {
129 break;
130 }
131 }
132
133 // The column norms are the singular values; normalize U's columns by them.
134 let mut singular_values = Vector::<N, T>::zeros();
135 for k in 0..N {
136 let sigma = u.column(k).norm();
137 singular_values[k] = sigma;
138 if sigma > T::ZERO {
139 for i in 0..M {
140 u[(i, k)] /= sigma;
141 }
142 }
143 }
144
145 // Sort the singular values descending, carrying the matching U and V columns.
146 for k in 0..N {
147 let mut top = k;
148 for j in (k + 1)..N {
149 if singular_values[j] > singular_values[top] {
150 top = j;
151 }
152 }
153 if top != k {
154 let tmp = singular_values[k];
155 singular_values[k] = singular_values[top];
156 singular_values[top] = tmp;
157 for i in 0..M {
158 let tmp = u[(i, k)];
159 u[(i, k)] = u[(i, top)];
160 u[(i, top)] = tmp;
161 }
162 for i in 0..N {
163 let tmp = v[(i, k)];
164 v[(i, k)] = v[(i, top)];
165 v[(i, top)] = tmp;
166 }
167 }
168 }
169
170 // Sign convention: the largest-magnitude entry of each U column is positive.
171 for k in 0..N {
172 let mut row = 0;
173 let mut best = T::ZERO;
174 for i in 0..M {
175 let mag = u[(i, k)].abs();
176 if mag > best {
177 best = mag;
178 row = i;
179 }
180 }
181 if u[(row, k)] < T::ZERO {
182 for i in 0..M {
183 u[(i, k)] = -u[(i, k)];
184 }
185 for i in 0..N {
186 v[(i, k)] = -v[(i, k)];
187 }
188 }
189 }
190
191 Ok(Svd {
192 u,
193 singular_values,
194 v,
195 })
196 }
197
198 /// The Moore–Penrose pseudo-inverse of `self`, for any shape.
199 ///
200 /// Tall or square inputs go straight through [`Matrix::svd`]; a wide input (`M < N`) is handled
201 /// as `((Aᵀ)⁺)ᵀ`. Returns [`CalcError::NonFiniteValue`] if any entry is not finite.
202 ///
203 /// ```
204 /// use multicalc::linear_algebra::Matrix;
205 /// // A wide matrix, handled through the transpose route.
206 /// let a = Matrix::<2, 3>::new([[1.0, 0.0, 2.0], [0.0, 1.0, 1.0]]);
207 /// let pinv = a.pseudo_inverse().unwrap();
208 /// let recon = a * pinv * a; // A·A⁺·A == A
209 /// for r in 0..2 {
210 /// for c in 0..3 {
211 /// assert!((recon[(r, c)] - a[(r, c)]).abs() < 1e-12);
212 /// }
213 /// }
214 /// ```
215 pub fn pseudo_inverse(self) -> Result<Matrix<N, M, T>, CalcError> {
216 if M >= N {
217 Ok(self.svd()?.pseudo_inverse())
218 } else {
219 Ok(self.transpose().svd()?.pseudo_inverse().transpose())
220 }
221 }
222}
223
224impl<const M: usize, const N: usize, T: Numeric> Svd<M, N, T> {
225 /// The singular values, descending and non-negative.
226 pub fn singular_values(&self) -> Vector<N, T> {
227 self.singular_values
228 }
229
230 /// The left factor `U`, with orthonormal columns.
231 pub fn u(&self) -> Matrix<M, N, T> {
232 self.u
233 }
234
235 /// The right factor `V`, with orthonormal columns.
236 pub fn v(&self) -> Matrix<N, N, T> {
237 self.v
238 }
239
240 /// The number of singular values greater than `tol`.
241 ///
242 /// ```
243 /// use multicalc::linear_algebra::Matrix;
244 /// // Column 2 is twice column 1, so the matrix has rank 1.
245 /// let a = Matrix::<3, 2>::new([[1.0, 2.0], [2.0, 4.0], [3.0, 6.0]]);
246 /// assert_eq!(a.svd().unwrap().rank(1e-9), 1);
247 /// ```
248 #[inline]
249 #[must_use]
250 pub fn rank(&self, tol: T) -> usize {
251 let mut count = 0;
252 for k in 0..N {
253 if self.singular_values[k] > tol {
254 count += 1;
255 }
256 }
257 count
258 }
259
260 /// The ratio `σ_max / σ_min`, or infinity when the smallest singular value is zero.
261 ///
262 /// ```
263 /// use multicalc::linear_algebra::Matrix;
264 /// let a = Matrix::<2, 2>::new([[2.0, 0.0], [0.0, 1.0]]);
265 /// assert!((a.svd().unwrap().condition_number() - 2.0).abs() < 1e-12);
266 /// ```
267 #[inline]
268 #[must_use]
269 pub fn condition_number(&self) -> T {
270 if N == 0 {
271 return T::INFINITY;
272 }
273 let smallest = self.singular_values[N - 1];
274 if smallest <= T::ZERO {
275 T::INFINITY
276 } else {
277 self.singular_values[0] / smallest
278 }
279 }
280
281 /// The default cutoff below which a singular value counts as zero.
282 fn default_tol(&self) -> T {
283 if N == 0 {
284 return T::ZERO;
285 }
286 T::from_usize(M.max(N)) * T::EPSILON * self.singular_values[0]
287 }
288
289 /// The Moore–Penrose pseudo-inverse `V · Σ⁺ · Uᵀ`, dropping singular values `<= tol`.
290 pub fn pseudo_inverse_tol(&self, tol: T) -> Matrix<N, M, T> {
291 Matrix::from_fn(|i, j| {
292 let mut acc = T::ZERO;
293 for k in 0..N {
294 let sigma = self.singular_values[k];
295 if sigma > tol {
296 acc += self.v[(i, k)] * self.u[(j, k)] / sigma;
297 }
298 }
299 acc
300 })
301 }
302
303 /// The Moore–Penrose pseudo-inverse, using a default cutoff from the largest singular value.
304 ///
305 /// ```
306 /// use multicalc::linear_algebra::Matrix;
307 /// let a = Matrix::<3, 2>::new([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]]);
308 /// let pinv = a.svd().unwrap().pseudo_inverse();
309 /// let recon = a * pinv * a; // A·A⁺·A == A
310 /// for r in 0..3 {
311 /// for c in 0..2 {
312 /// assert!((recon[(r, c)] - a[(r, c)]).abs() < 1e-12);
313 /// }
314 /// }
315 /// ```
316 pub fn pseudo_inverse(&self) -> Matrix<N, M, T> {
317 self.pseudo_inverse_tol(self.default_tol())
318 }
319
320 /// The minimum-norm least-squares solution of `A·x = b`, from `V · Σ⁺ · Uᵀ · b`.
321 ///
322 /// The pseudo-inverse is never formed. Singular values `<= tol` are dropped.
323 ///
324 /// ```
325 /// use multicalc::linear_algebra::{Matrix, Vector};
326 /// // Overdetermined and consistent: the exact solution is x = [1, 2].
327 /// let a = Matrix::<3, 2>::new([[1.0, 0.0], [0.0, 1.0], [1.0, 1.0]]);
328 /// let x = a.svd().unwrap().solve(Vector::new([1.0, 2.0, 3.0]));
329 /// assert!((x[0] - 1.0).abs() < 1e-12);
330 /// assert!((x[1] - 2.0).abs() < 1e-12);
331 /// ```
332 pub fn solve(&self, b: Vector<M, T>) -> Vector<N, T> {
333 let tol = self.default_tol();
334 let mut z = Vector::<N, T>::zeros();
335 for k in 0..N {
336 let sigma = self.singular_values[k];
337 if sigma > tol {
338 z[k] = self.u.column(k).dot(b) / sigma;
339 }
340 }
341 self.v * z
342 }
343}