a/math/matrix/properties.rs
1//! Norms, condition numbers, ranks, null spaces and characteristic polynomials.
2
3use super::householder::norm2;
4use super::schur::hessenberg;
5use super::svd::{svd, svd_scaled};
6use super::{Matrix, Scalar};
7use crate::math::complex::Float;
8
9/// The power of two `k` by which the finite elements `data` are scaled, `2^k·A`, as LAPACK's
10/// drivers scale, when the largest of them is so small or so large that products of elements
11/// could underflow or overflow, outside `[√MIN/ε, ε/√MIN]`; 0 inside it, which the largest part
12/// decides alone unless it is near an end. A complex element can have finite parts and an
13/// absolute value that overflows: its largest part, within a factor √2, stands for it then.
14pub(crate) fn range_exponent<T: Scalar>(data: &[T]) -> i32 {
15 let zero = T::Real::ZERO;
16 let small = T::Real::MIN_POSITIVE.sqrt() / T::Real::EPSILON;
17 let big = data
18 .iter()
19 .fold(zero, |s, x| s.max(x.re().abs().max(x.im().abs())));
20 if big >= small && big * T::Real::TWO.sqrt() <= T::Real::ONE / small {
21 return 0;
22 }
23 let max = data.iter().fold(zero, |s, x| s.max(x.abs()));
24 let log2 = |x: T::Real| x.ln().to_f64() / core::f64::consts::LN_2;
25 let top = if max.is_finite() {
26 log2(max)
27 } else {
28 log2(big) + 0.5
29 };
30 if max > zero && max < small {
31 (log2(small) - top).ceil() as i32
32 } else if max > T::Real::ONE / small {
33 (-log2(small) - top).floor() as i32
34 } else {
35 0
36 }
37}
38
39/// The larger of `m` and `x`, or a NaN when either is one: `Float::max` ignores NaNs, which
40/// would let a norm of a matrix holding one be finite.
41fn max_nan<R: Float>(m: R, x: R) -> R {
42 if m.is_nan() || x.is_nan() {
43 R::NAN
44 } else {
45 m.max(x)
46 }
47}
48
49/// A matrix norm, for [`Matrix::norm`] and [`Matrix::cond`].
50#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
51pub enum Norm {
52 /// The largest sum of the absolute values in a column.
53 One,
54 /// The spectral norm, the largest singular value.
55 Two,
56 /// The largest sum of the absolute values in a row.
57 Infinity,
58 /// Frobenius's, the square root of the sum of the squares of the absolute values.
59 Frobenius,
60 /// The largest absolute value of an element; not submultiplicative.
61 Max,
62 /// The nuclear norm, the sum of the singular values.
63 Nuclear,
64}
65
66impl<T: Scalar> Matrix<T> {
67 /// The norm `‖A‖` of the kind given: see [`Norm`]. The 2-norm and the nuclear norm take the
68 /// singular values; the others one pass over the elements, Frobenius's scaled so that the
69 /// squares neither overflow nor underflow.
70 ///
71 /// # Examples
72 ///
73 /// ```
74 /// use a::math::matrix::{Matrix, Norm};
75 ///
76 /// let a: Matrix<f64> = Matrix::from_rows(&[[1.0, -2.0], [-3.0, 4.0]]);
77 /// assert_eq!(a.norm(Norm::One), 6.0);
78 /// assert_eq!(a.norm(Norm::Infinity), 7.0);
79 /// assert_eq!(a.norm(Norm::Max), 4.0);
80 /// assert!((a.norm(Norm::Frobenius) - 30f64.sqrt()).abs() < 1e-15);
81 /// ```
82 pub fn norm(&self, norm: Norm) -> T::Real {
83 let zero = T::Real::ZERO;
84 let (m, n) = self.shape();
85 match norm {
86 Norm::One => (0..n)
87 .map(|j| self.column(j).iter().map(|x| x.abs()).sum::<T::Real>())
88 .fold(zero, max_nan),
89 Norm::Infinity => {
90 let mut rows = vec![zero; m];
91 for j in 0..n {
92 for (r, x) in rows.iter_mut().zip(self.column(j)) {
93 *r += x.abs();
94 }
95 }
96 rows.into_iter().fold(zero, max_nan)
97 }
98 Norm::Frobenius => norm2(&self.data),
99 Norm::Max => self.data.iter().map(|x| x.abs()).fold(zero, max_nan),
100 Norm::Two => svd(self, false).0.first().copied().unwrap_or(zero),
101 Norm::Nuclear => svd(self, false).0.into_iter().sum(),
102 }
103 }
104
105 /// The condition number `κ(A) = ‖A‖·‖A⁻¹‖` in the norm given, which bounds how much a
106 /// relative change of `A` or of `b` can change the solution of `A·x = b`: infinite for a
107 /// singular matrix. In the 2-norm it is the ratio of the largest singular value to the
108 /// smallest, for a matrix of any shape; in the other norms it takes the inverse.
109 ///
110 /// # Panics
111 ///
112 /// Panics if the norm is not [`Norm::Two`] and the matrix is not square.
113 #[track_caller]
114 pub fn cond(&self, norm: Norm) -> T::Real {
115 let zero = T::Real::ZERO;
116 match norm {
117 // From the singular values of the matrix scaled into range, whose ratios are the
118 // same, so that neither overflows where the matrix's own would.
119 Norm::Two => {
120 let s = svd_scaled(self, false).0;
121 match (s.first(), s.last()) {
122 (Some(&max), Some(&min)) if max.is_nan() || min.is_nan() => T::Real::NAN,
123 (Some(&max), Some(&min)) if min > zero => max / min,
124 (Some(_), Some(_)) => T::Real::INFINITY,
125 _ => zero,
126 }
127 }
128 Norm::Nuclear => {
129 assert!(self.is_square(), "the matrix is not square");
130 let s = svd_scaled(self, false).0;
131 if s.last().is_some_and(|&x| x == zero) {
132 return T::Real::INFINITY;
133 }
134 // `(Σσ/σ₁)·(Σσ₁/σ)`, each sum at most `n` times its largest term.
135 let top = s.first().copied().unwrap_or(T::Real::ONE);
136 let sum: T::Real = s.iter().map(|&x| x / top).sum();
137 let inv: T::Real = s.iter().map(|&x| top / x).sum();
138 sum * inv
139 }
140 _ => {
141 assert!(self.is_square(), "the matrix is not square");
142 match self.inverse() {
143 Some(inv) => self.norm(norm) * inv.norm(norm),
144 None => T::Real::INFINITY,
145 }
146 }
147 }
148 }
149
150 /// The rank: the number of singular values above `max(m, n)·ε·σ₁`, `ε` the unit roundoff
151 /// of the type and `σ₁` the largest singular value, as NumPy's `matrix_rank`.
152 pub fn rank(&self) -> usize {
153 let s = svd_scaled(self, false).0;
154 let tol = self.rank_tolerance(&s);
155 s.iter().filter(|&&x| x > tol).count()
156 }
157
158 /// An orthonormal basis of the null space `{x : A·x = 0}`, as the columns of an
159 /// `n × (n - rank)` matrix: the right singular vectors of the singular values at or below
160 /// the tolerance of [`rank`](Matrix::rank).
161 pub fn null_space(&self) -> Matrix<T> {
162 let (m, n) = self.shape();
163 // With fewer rows than columns, rows of zeros make all `n` right singular vectors.
164 let a = if m < n {
165 Matrix::from_fn(n, n, |i, j| if i < m { self[(i, j)] } else { T::zero() })
166 } else {
167 self.clone()
168 };
169 let (s, uv, _) = svd_scaled(&a, true);
170 let v = uv.map(|(_, v)| v).unwrap_or_default();
171 let tol = self.rank_tolerance(&s);
172 let rank = s.iter().filter(|&&x| x > tol).count();
173 Matrix::from_fn(n, n - rank, |i, j| v[(i, rank + j)])
174 }
175
176 /// The tolerance below which a singular value counts as zero.
177 fn rank_tolerance(&self, s: &[T::Real]) -> T::Real {
178 let (m, n) = self.shape();
179 let max = s.first().copied().unwrap_or(T::Real::ZERO);
180 T::Real::from_f64(m.max(n) as f64) * T::Real::EPSILON * max
181 }
182
183 /// The coefficients of the characteristic polynomial `det(x·I - A)`, from `xⁿ` down: `1`,
184 /// then `-tr(A)`, …, `(-1)ⁿ·det(A)`. From the Hessenberg form, by La Budde's recurrence on
185 /// its leading blocks, about `n³` operations.
186 ///
187 /// # Panics
188 ///
189 /// Panics if the matrix is not square.
190 ///
191 /// # Examples
192 ///
193 /// ```
194 /// use a::math::matrix::Matrix;
195 ///
196 /// let a: Matrix<f64> = Matrix::from_rows(&[[2.0, 1.0], [1.0, 2.0]]);
197 /// // (x - 1)(x - 3)
198 /// assert_eq!(a.characteristic_polynomial(), [1.0, -4.0, 3.0]);
199 /// ```
200 #[track_caller]
201 pub fn characteristic_polynomial(&self) -> Vec<T> {
202 assert!(self.is_square(), "the matrix is not square");
203 let n = self.rows();
204 let mut h = self.clone();
205 hessenberg(&mut h, false);
206 let at = |i: usize, j: usize| h.data[i + j * n];
207 // `p[s]`: the polynomial of the leading `s × s` block, from the constant term up.
208 let mut p: Vec<Vec<T>> = vec![vec![T::one()]];
209 for k in 0..n {
210 // `(x - h_kk)·p_k`.
211 let prev = &p[k];
212 let mut next = vec![T::zero(); k + 2];
213 for (i, &c) in prev.iter().enumerate() {
214 next[i + 1] += c;
215 next[i] -= at(k, k) * c;
216 }
217 // `- Σ h_{k-m,k}·(h_{k,k-1}·…·h_{k-m+1,k-m})·p_{k-m}`.
218 let mut product = T::one();
219 for m in 1..=k {
220 product *= at(k - m + 1, k - m);
221 let f = at(k - m, k) * product;
222 if f == T::zero() {
223 continue;
224 }
225 for (i, &c) in p[k - m].iter().enumerate() {
226 next[i] -= f * c;
227 }
228 }
229 p.push(next);
230 }
231 let mut c = p.pop().unwrap_or_default();
232 c.reverse();
233 c
234 }
235}