Skip to main content

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}