Skip to main content

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}