Skip to main content

a/math/matrix/
eigen.rs

1//! Eigenvalues and eigenvectors: of any square matrix from its Schur form, and of a Hermitian
2//! one by a reduction to a real tridiagonal matrix and divide and conquer.
3
4use super::hermitian::hermitian;
5use super::schur::{eigenvectors, schur};
6use super::{Matrix, Scalar};
7use crate::math::complex::{Complex, Float};
8
9/// The eigendecomposition `A·V = V·Λ` of a square matrix: its eigenvalues, the diagonal of `Λ`,
10/// and its eigenvectors, the columns of `V`, complex in general, each of length 1. When `A` has
11/// a full set of independent eigenvectors, `A = V·Λ·V⁻¹`. From [`Matrix::eigen`].
12///
13/// # Examples
14///
15/// ```
16/// use a::math::complex::Complex;
17/// use a::math::matrix::Matrix;
18///
19/// let a: Matrix<f64> = Matrix::from_rows(&[[2.0, 1.0], [1.0, 2.0]]);
20/// let e = a.eigen();
21/// let (v, w) = (e.vectors(), e.values());
22/// // Each column is an eigenvector: A·v = λ·v.
23/// let a = a.map(|&x| Complex::from(x));
24/// for (j, &w) in w.iter().enumerate() {
25///     let v = Matrix::from_columns(&[v.column(j)]);
26///     let d = &(&a * &v) - &(&v * w);
27///     assert!(d.as_slice().iter().all(|x| x.abs() < 1e-14));
28/// }
29/// ```
30#[derive(Clone, Debug)]
31pub struct Eigen<R> {
32    /// The eigenvalues.
33    values: Vec<Complex<R>>,
34    /// The eigenvectors, as columns.
35    vectors: Matrix<Complex<R>>,
36}
37
38impl<R: Float> Eigen<R> {
39    /// The eigenvalues, in the order of the columns of [`vectors`](Eigen::vectors).
40    pub fn values(&self) -> &[Complex<R>] {
41        &self.values
42    }
43
44    /// The eigenvectors, each a column of length 1 in the 2-norm.
45    pub fn vectors(&self) -> &Matrix<Complex<R>> {
46        &self.vectors
47    }
48}
49
50/// The eigendecomposition `A = V·Λ·V*` of a Hermitian (for real elements, symmetric) matrix:
51/// real eigenvalues in ascending order, and orthonormal eigenvectors, the columns of `V`, of the
52/// matrix's own type. From [`Matrix::hermitian_eigen`].
53///
54/// # Examples
55///
56/// ```
57/// use a::math::matrix::Matrix;
58///
59/// let a: Matrix<f64> = Matrix::from_rows(&[[2.0, 1.0], [1.0, 2.0]]);
60/// let e = a.hermitian_eigen();
61/// assert!((e.values()[0] - 1.0).abs() < 1e-15 && (e.values()[1] - 3.0).abs() < 1e-15);
62/// let v = e.vectors();
63/// let back = &(v * &Matrix::from_diagonal(e.values())) * &v.transpose();
64/// assert!((&back - &a).as_slice().iter().all(|x| x.abs() < 1e-14));
65/// ```
66#[derive(Clone, Debug)]
67pub struct HermitianEigen<T: Scalar> {
68    /// The eigenvalues, ascending.
69    values: Vec<T::Real>,
70    /// The eigenvectors, as columns.
71    vectors: Matrix<T>,
72}
73
74impl<T: Scalar> HermitianEigen<T> {
75    /// The eigenvalues, real, in ascending order.
76    pub fn values(&self) -> &[T::Real] {
77        &self.values
78    }
79
80    /// The eigenvectors, orthonormal columns in the order of the eigenvalues.
81    pub fn vectors(&self) -> &Matrix<T> {
82        &self.vectors
83    }
84}
85
86impl<T: Scalar> Matrix<T> {
87    /// The eigenvalues and eigenvectors: see [`Eigen`]. From the Schur form `A = Q·T·Q*`, the
88    /// eigenvectors of `T` by back substitution, in real arithmetic for real elements, and `Q`
89    /// times them. A matrix with a NaN or an infinity gives NaNs.
90    ///
91    /// # Panics
92    ///
93    /// Panics if the matrix is not square.
94    #[track_caller]
95    pub fn eigen(&self) -> Eigen<T::Real> {
96        assert!(self.is_square(), "the matrix is not square");
97        let n = self.rows();
98        let (t, q, values) = schur(self, true, true);
99        let vectors = eigenvectors(&t.data, &q.unwrap_or_default().data, n);
100        Eigen {
101            values,
102            vectors: Matrix::from_column_major(n, n, vectors),
103        }
104    }
105
106    /// The eigenvalues and orthonormal eigenvectors of a Hermitian (for real elements,
107    /// symmetric) matrix: see [`HermitianEigen`]. A blocked reduction to a real symmetric
108    /// tridiagonal matrix by Householder reflections, its eigensystem by Cuppen's divide and
109    /// conquer, then the reflections applied to it in blocks; up to about a hundred rows (80 to
110    /// 160 by the type), where that takes longer, the reduction one column at a time, its
111    /// reflections multiplied out into `Q`, and the implicit QL algorithm's rotations applied
112    /// to `Q`. Only the lower triangle is read, the upper one taken to be its conjugate. A
113    /// matrix with a NaN or an infinity gives NaNs.
114    ///
115    /// # Panics
116    ///
117    /// Panics if the matrix is not square.
118    #[track_caller]
119    pub fn hermitian_eigen(&self) -> HermitianEigen<T> {
120        assert!(self.is_square(), "the matrix is not square");
121        let (values, vectors) = hermitian(self, true);
122        HermitianEigen {
123            values,
124            vectors: vectors.unwrap_or_default(),
125        }
126    }
127
128    /// The eigenvalues of a Hermitian (for real elements, symmetric) matrix, real and
129    /// ascending, without the eigenvectors: the same reduction, then the QL and QR algorithm
130    /// without square roots, from about a hundred rows less than half the time of
131    /// [`hermitian_eigen`](Matrix::hermitian_eigen). Only the lower triangle is read.
132    ///
133    /// # Panics
134    ///
135    /// Panics if the matrix is not square.
136    #[track_caller]
137    pub fn hermitian_eigenvalues(&self) -> Vec<T::Real> {
138        assert!(self.is_square(), "the matrix is not square");
139        hermitian(self, false).0
140    }
141}
142
143/// Turns the real Schur form `t`, held as complex numbers, into a complex upper triangular one,
144/// a unitary rotation for each 2 × 2 block, applied to `q` too, as MATLAB's `rsf2csf`.
145pub(crate) fn real_to_complex_schur<C: Scalar>(t: &mut [C], q: &mut [C], n: usize) {
146    let at = |i: usize, j: usize| i + j * n;
147    let zero = C::Real::ZERO;
148    for m in (1..n).rev() {
149        let sub = t[at(m, m - 1)];
150        if sub == C::zero() {
151            continue;
152        }
153        // The block's eigenvalue with a positive imaginary part, less `t[m, m]`, from the block
154        // divided by its largest part: the rotation depends only on the ratio of `mu` to `c`,
155        // and the products of the block's own elements could overflow or underflow, as those of
156        // `[[0, -1e200], [1e200, 0]]` do.
157        let (a, b, c, d) = (t[at(m - 1, m - 1)], t[at(m - 1, m)], sub, t[at(m, m)]);
158        let part = |x: C| x.re().abs().max(x.im().abs());
159        let s = part(a).max(part(b)).max(part(c)).max(part(d));
160        let down = |x: C| C::from_parts(x.re() / s, x.im() / s);
161        let (a, b, c, d) = (down(a), down(b), down(c), down(d));
162        let half = (a - d).scale(C::Real::HALF);
163        let disc = (half * half + b * c).sqrt();
164        let mu = half + disc;
165        let r = mu.abs().hypot(c.abs());
166        if r == zero {
167            continue;
168        }
169        let cs = mu.scale(C::Real::ONE / r);
170        let sn = c.scale(C::Real::ONE / r);
171        // `G = [[cs*, sn], [-sn, cs]]` from the left on rows `m - 1, m`, `G*` from the right on
172        // columns `m - 1, m`.
173        for j in m - 1..n {
174            let (x, y) = (t[at(m - 1, j)], t[at(m, j)]);
175            t[at(m - 1, j)] = cs.conj() * x + sn * y;
176            t[at(m, j)] = -sn.conj() * x + cs * y;
177        }
178        for i in 0..=m {
179            let (x, y) = (t[at(i, m - 1)], t[at(i, m)]);
180            t[at(i, m - 1)] = x * cs + y * sn.conj();
181            t[at(i, m)] = -(x * sn) + y * cs.conj();
182        }
183        for i in 0..n {
184            let (x, y) = (q[at(i, m - 1)], q[at(i, m)]);
185            q[at(i, m - 1)] = x * cs + y * sn.conj();
186            q[at(i, m)] = -(x * sn) + y * cs.conj();
187        }
188        t[at(m, m - 1)] = C::zero();
189    }
190}