1#[derive(Debug, Clone, Copy, PartialEq)]
20pub struct Decomposition<const K: usize> {
21 pub values: [f64; K],
23 pub vectors: [[f64; K]; K],
27}
28
29pub fn singular_values<const K: usize>(matrix: &[[f64; K]; K]) -> [f64; K] {
41 decompose(matrix).values
42}
43
44pub fn decompose<const K: usize>(matrix: &[[f64; K]; K]) -> Decomposition<K> {
51 const TOLERANCE: f64 = 1e-17;
54 const MAX_SWEEPS: usize = 40;
57
58 let mut work = *matrix;
59 let mut rotations = [[0.0f64; K]; K];
60 for (index, row) in rotations.iter_mut().enumerate() {
61 row[index] = 1.0;
62 }
63
64 for _ in 0..MAX_SWEEPS {
65 let mut rotated = false;
66
67 for p in 0..K {
68 for q in (p + 1)..K {
69 let mut alpha = 0.0;
70 let mut beta = 0.0;
71 let mut gamma = 0.0;
72 for row in work.iter() {
73 alpha += row[p] * row[p];
74 beta += row[q] * row[q];
75 gamma += row[p] * row[q];
76 }
77
78 if gamma == 0.0 || alpha == 0.0 || beta == 0.0 {
79 continue;
80 }
81 if gamma.abs() <= TOLERANCE * (alpha * beta).sqrt() {
84 continue;
85 }
86
87 let zeta = (beta - alpha) / (2.0 * gamma);
88 let tangent = zeta.signum() / (zeta.abs() + (1.0 + zeta * zeta).sqrt());
89 let cosine = 1.0 / (1.0 + tangent * tangent).sqrt();
90 let sine = cosine * tangent;
91
92 for row in work.iter_mut().chain(rotations.iter_mut()) {
93 let left = row[p];
94 let right = row[q];
95 row[p] = cosine * left - sine * right;
96 row[q] = sine * left + cosine * right;
97 }
98 rotated = true;
99 }
100 }
101
102 if !rotated {
103 break;
104 }
105 }
106
107 let mut norms = [0.0f64; K];
108 for (column, value) in norms.iter_mut().enumerate() {
109 let mut sum = 0.0;
110 for row in work.iter() {
111 sum += row[column] * row[column];
112 }
113 *value = sum.sqrt();
114 }
115
116 let mut order: [usize; K] = [0; K];
118 for (index, slot) in order.iter_mut().enumerate() {
119 *slot = index;
120 }
121 order.sort_by(|a, b| norms[*b].total_cmp(&norms[*a]));
122
123 let mut values = [0.0f64; K];
124 let mut vectors = [[0.0f64; K]; K];
125 for (target, source) in order.iter().enumerate() {
126 values[target] = norms[*source];
127 for axis in 0..K {
128 vectors[axis][target] = rotations[axis][*source];
129 }
130 }
131 Decomposition { values, vectors }
132}
133
134pub fn condition_number<const K: usize>(values: &[f64; K]) -> f64 {
138 let largest = values[0];
139 let smallest = values[K - 1];
140 if smallest == 0.0 {
141 f64::INFINITY
142 } else {
143 largest / smallest
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
154 fn diagonal_matrix_is_trivial() {
155 let mut matrix = [[0.0f64; 4]; 4];
156 for (i, value) in [3.0, -1.0, 0.25, 8.0].into_iter().enumerate() {
157 matrix[i][i] = value;
158 }
159 let values = singular_values(&matrix);
160 assert!((values[0] - 8.0).abs() < 1e-15);
161 assert!((values[1] - 3.0).abs() < 1e-15);
162 assert!((values[2] - 1.0).abs() < 1e-15);
163 assert!((values[3] - 0.25).abs() < 1e-15);
164 }
165
166 #[test]
168 fn rotation_has_unit_spectrum() {
169 let angle = 0.7f64;
170 let matrix = [
171 [angle.cos(), -angle.sin(), 0.0],
172 [angle.sin(), angle.cos(), 0.0],
173 [0.0, 0.0, 1.0],
174 ];
175 for value in singular_values(&matrix) {
176 assert!((value - 1.0).abs() < 1e-15);
177 }
178 }
179
180 #[test]
182 fn rank_deficient_matrix_yields_zero() {
183 let matrix = [[1.0, 2.0, 3.0], [2.0, 4.0, 6.0], [-1.0, -2.0, -3.0]];
184 let values = singular_values(&matrix);
185 assert!(values[0] > 1.0);
186 assert!(values[1] < 1e-15, "second value {}", values[1]);
187 assert!(values[2] < 1e-15);
188 assert_eq!(condition_number(&values), f64::INFINITY);
189 }
190
191 #[test]
194 fn tiny_diagonal_entries_keep_relative_accuracy() {
195 let scales = [1.0, 1e-4, 1e-8, 1e-12, 1e-16, 1e-20];
196 let mut matrix = [[0.0f64; 6]; 6];
197 for (i, scale) in scales.iter().enumerate() {
198 matrix[i][i] = *scale;
199 }
200 let values = singular_values(&matrix);
201 for (found, expected) in values.iter().zip(scales.iter()) {
202 let relative = (found - expected).abs() / expected;
203 assert!(
204 relative < 1e-15,
205 "expected {expected:e}, got {found:e}, error {relative:.3e}"
206 );
207 }
208 }
209}
210
211#[cfg(test)]
212mod vector_tests {
213 use super::*;
214
215 #[test]
217 fn right_vectors_are_orthonormal() {
218 let matrix = [
219 [3.0, 1.0, -2.0, 0.5],
220 [0.0, 2.5, 1.0, -1.0],
221 [1.0, 0.0, 4.0, 2.0],
222 [-0.5, 1.5, 0.0, 3.0],
223 ];
224 let result = decompose(&matrix);
225 for i in 0..4 {
226 for j in 0..4 {
227 let dot: f64 = (0..4)
228 .map(|axis| result.vectors[axis][i] * result.vectors[axis][j])
229 .sum();
230 let expected = if i == j { 1.0 } else { 0.0 };
231 assert!((dot - expected).abs() < 1e-13, "V columns {i},{j}: {dot}");
232 }
233 }
234 }
235
236 #[test]
238 fn vectors_match_their_values() {
239 let matrix = [[2.0, 0.0, 1.0], [0.0, 3.0, 0.0], [1.0, 0.0, 2.0]];
240 let result = decompose(&matrix);
241 for j in 0..3 {
242 let mut image = [0.0f64; 3];
243 for (i, slot) in image.iter_mut().enumerate() {
244 *slot = (0..3).map(|k| matrix[i][k] * result.vectors[k][j]).sum();
245 }
246 let norm = image.iter().map(|v| v * v).sum::<f64>().sqrt();
247 assert!(
248 (norm - result.values[j]).abs() < 1e-13,
249 "‖A·v{j}‖ = {norm}, but σ{j} = {}",
250 result.values[j]
251 );
252 }
253 }
254
255 #[test]
257 fn null_direction_is_annihilated() {
258 let matrix = [[1.0, 2.0, 3.0], [2.0, 4.0, 6.0], [-1.0, -2.0, -3.0]];
259 let result = decompose(&matrix);
260 let null = [
261 result.vectors[0][2],
262 result.vectors[1][2],
263 result.vectors[2][2],
264 ];
265 for row in &matrix {
266 let value: f64 = (0..3).map(|k| row[k] * null[k]).sum();
267 assert!(value.abs() < 1e-13, "row gives {value}");
268 }
269 }
270}