rustyqlib/core/linalg/decomp/
svd.rs1pub fn svd(a: &[Vec<f64>]) -> (Vec<Vec<f64>>, Vec<f64>, Vec<Vec<f64>>) {
10 let m = a.len();
11 let n = if m == 0 { 0 } else { a[0].len() };
12 assert!(m > 0 && n > 0, "empty matrix");
13 assert!(a.iter().all(|row| row.len() == n), "ragged matrix");
14 if m < n {
15 let at: Vec<Vec<f64>> = (0..n).map(|j| (0..m).map(|i| a[i][j]).collect()).collect();
17 let (u_t, s, v_t) = svd(&at);
18 return (v_t, s, u_t);
19 }
20
21 let mut b = a.to_vec();
23 let mut v: Vec<Vec<f64>> = (0..n)
24 .map(|i| (0..n).map(|j| if i == j { 1.0 } else { 0.0 }).collect())
25 .collect();
26 for _sweep in 0..60 {
27 let mut off = 0.0_f64;
28 for i in 0..n {
29 for j in (i + 1)..n {
30 let mut alpha = 0.0;
31 let mut beta = 0.0;
32 let mut g = 0.0;
33 for row in &b {
34 alpha += row[i] * row[i];
35 beta += row[j] * row[j];
36 g += row[i] * row[j];
37 }
38 if g.abs() <= 1e-14 * (alpha * beta).sqrt().max(1e-300) {
39 continue;
40 }
41 off = off.max(g.abs());
42 let zeta = (beta - alpha) / (2.0 * g);
43 let t = zeta.signum() / (zeta.abs() + (1.0 + zeta * zeta).sqrt());
44 let c = 1.0 / (1.0 + t * t).sqrt();
45 let s = c * t;
46 for row in b.iter_mut() {
47 let (bi, bj) = (row[i], row[j]);
48 row[i] = c * bi - s * bj;
49 row[j] = s * bi + c * bj;
50 }
51 for row in v.iter_mut() {
52 let (vi, vj) = (row[i], row[j]);
53 row[i] = c * vi - s * vj;
54 row[j] = s * vi + c * vj;
55 }
56 }
57 }
58 if off == 0.0 {
59 break;
60 }
61 }
62
63 let mut s: Vec<f64> = (0..n)
65 .map(|j| b.iter().map(|row| row[j] * row[j]).sum::<f64>().sqrt())
66 .collect();
67 let mut u = vec![vec![0.0; n]; m];
68 for j in 0..n {
69 if s[j] > 1e-300 {
70 for i in 0..m {
71 u[i][j] = b[i][j] / s[j];
72 }
73 }
74 }
75 let mut order: Vec<usize> = (0..n).collect();
77 order.sort_by(|&i, &j| s[j].total_cmp(&s[i]));
78 let s_sorted: Vec<f64> = order.iter().map(|&k| s[k]).collect();
79 let permute = |mat: &[Vec<f64>]| -> Vec<Vec<f64>> {
80 mat.iter().map(|row| order.iter().map(|&k| row[k]).collect()).collect()
81 };
82 let (u, v) = (permute(&u), permute(&v));
83 s = s_sorted;
84 (u, s, v)
85}
86
87pub fn pseudo_solve(a: &[Vec<f64>], b: &[f64], tol: f64) -> Vec<f64> {
91 let (u, s, v) = svd(a);
92 let m = a.len();
93 let n = s.len();
94 assert_eq!(b.len(), m, "dimension mismatch");
95 let cutoff = tol * s.first().copied().unwrap_or(0.0);
96 let mut x = vec![0.0; n];
97 for k in 0..n {
98 if s[k] <= cutoff || s[k] == 0.0 {
99 continue;
100 }
101 let utb: f64 = (0..m).map(|i| u[i][k] * b[i]).sum();
102 let coeff = utb / s[k];
103 for (j, xj) in x.iter_mut().enumerate() {
104 *xj += coeff * v[j][k];
105 }
106 }
107 x
108}
109
110#[cfg(test)]
111mod tests {
112 use super::super::qr::least_squares;
113 use super::*;
114
115 fn fixture() -> Vec<Vec<f64>> {
116 (0..5)
117 .map(|i| (0..3).map(|j| ((2 * i + 3 * j) as f64).cos() + if i == j { 1.5 } else { 0.0 }).collect())
118 .collect()
119 }
120
121 fn reconstruct(u: &[Vec<f64>], s: &[f64], v: &[Vec<f64>]) -> Vec<Vec<f64>> {
122 let (m, n) = (u.len(), s.len());
123 (0..m)
124 .map(|i| {
125 (0..v.len())
126 .map(|j| (0..n).map(|k| u[i][k] * s[k] * v[j][k]).sum())
127 .collect()
128 })
129 .collect()
130 }
131
132 #[test]
133 fn decomposition_reconstructs_and_is_orthogonal() {
134 let a = fixture();
135 let (u, s, v) = svd(&a);
136 let recon = reconstruct(&u, &s, &v);
137 for i in 0..5 {
138 for j in 0..3 {
139 assert!((recon[i][j] - a[i][j]).abs() < 1e-10, "[{i}][{j}]");
140 }
141 }
142 assert!(s.windows(2).all(|w| w[0] >= w[1]) && s.iter().all(|&x| x >= 0.0));
144 for j1 in 0..3 {
146 for j2 in 0..3 {
147 let want = if j1 == j2 { 1.0 } else { 0.0 };
148 let uu: f64 = (0..5).map(|i| u[i][j1] * u[i][j2]).sum();
149 let vv: f64 = (0..3).map(|i| v[i][j1] * v[i][j2]).sum();
150 assert!((uu - want).abs() < 1e-11, "U [{j1}][{j2}]");
151 assert!((vv - want).abs() < 1e-11, "V [{j1}][{j2}]");
152 }
153 }
154 }
155
156 #[test]
157 fn known_singular_values_of_a_diagonal_matrix() {
158 let (_, s, _) = svd(&[vec![2.0, 0.0], vec![0.0, -3.0]]);
159 assert!((s[0] - 3.0).abs() < 1e-12 && (s[1] - 2.0).abs() < 1e-12, "{s:?}");
160 }
161
162 #[test]
163 fn rank_deficiency_is_revealed_and_wide_matrices_work() {
164 let a = vec![vec![1.0, 2.0, 3.0], vec![2.0, 4.0, 6.0]];
166 let (u, s, v) = svd(&a);
167 assert!(s[0] > 1.0 && s[1].abs() < 1e-10, "{s:?}");
168 let recon = reconstruct(&u, &s, &v);
169 for i in 0..2 {
170 for j in 0..3 {
171 assert!((recon[i][j] - a[i][j]).abs() < 1e-10);
172 }
173 }
174 }
175
176 #[test]
177 fn pseudo_solve_matches_qr_on_full_rank_and_handles_deficiency() {
178 let a = fixture();
179 let b = [1.0, -2.0, 0.5, 3.0, -1.0];
180 let via_qr = least_squares(&a, &b).unwrap();
181 let via_svd = pseudo_solve(&a, &b, 1e-12);
182 for (x, y) in via_qr.iter().zip(&via_svd) {
183 assert!((x - y).abs() < 1e-10, "{via_qr:?} vs {via_svd:?}");
184 }
185 let deficient: Vec<Vec<f64>> =
188 (0..4).map(|i| vec![i as f64 + 1.0, 2.0 * (i as f64 + 1.0)]).collect();
189 let rhs = [1.0, 2.0, 3.0, 4.0];
190 assert!(least_squares(&deficient, &rhs).is_err());
191 let x = pseudo_solve(&deficient, &rhs, 1e-12);
192 for j in 0..2 {
195 let r: f64 = (0..4)
196 .map(|i| {
197 let ax: f64 = (0..2).map(|k| deficient[i][k] * x[k]).sum();
198 deficient[i][j] * (ax - rhs[i])
199 })
200 .sum();
201 assert!(r.abs() < 1e-10, "column {j} residual {r}");
202 }
203 assert!((x[1] - 2.0 * x[0]).abs() < 1e-10, "{x:?}");
205 }
206}