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}