nalgebra/src/linalg/symmetric_eigen.rs

426 lines
13 KiB
Rust
Raw Normal View History

#[cfg(feature = "serde-serialize-no-std")]
2018-10-22 13:00:10 +08:00
use serde::{Deserialize, Serialize};
use approx::AbsDiffEq;
2020-03-21 19:16:46 +08:00
use num::Zero;
2019-03-23 21:29:07 +08:00
use crate::allocator::Allocator;
2021-04-11 17:00:38 +08:00
use crate::base::{DefaultAllocator, Matrix2, OMatrix, OVector, SquareMatrix, Vector2};
use crate::dimension::{Dim, DimDiff, DimSub, U1};
use crate::storage::Storage;
2020-03-21 19:16:46 +08:00
use simba::scalar::ComplexField;
2019-03-23 21:29:07 +08:00
use crate::linalg::givens::GivensRotation;
use crate::linalg::SymmetricTridiagonal;
/// Eigendecomposition of a symmetric matrix.
#[cfg_attr(feature = "serde-serialize-no-std", derive(Serialize, Deserialize))]
#[cfg_attr(
feature = "serde-serialize-no-std",
2021-04-11 17:00:38 +08:00
serde(bound(serialize = "DefaultAllocator: Allocator<T, D, D> +
Allocator<T::RealField, D>,
OVector<T::RealField, D>: Serialize,
OMatrix<T, D, D>: Serialize"))
)]
#[cfg_attr(
feature = "serde-serialize-no-std",
2021-04-11 17:00:38 +08:00
serde(bound(deserialize = "DefaultAllocator: Allocator<T, D, D> +
Allocator<T::RealField, D>,
OVector<T::RealField, D>: Deserialize<'de>,
OMatrix<T, D, D>: Deserialize<'de>"))
)]
#[derive(Clone, Debug)]
2021-04-11 17:00:38 +08:00
pub struct SymmetricEigen<T: ComplexField, D: Dim>
2020-04-06 00:49:48 +08:00
where
2021-04-11 17:00:38 +08:00
DefaultAllocator: Allocator<T, D, D> + Allocator<T::RealField, D>,
2018-02-02 19:26:35 +08:00
{
/// The eigenvectors of the decomposed matrix.
2021-04-11 17:00:38 +08:00
pub eigenvectors: OMatrix<T, D, D>,
/// The unsorted eigenvalues of the decomposed matrix.
2021-04-11 17:00:38 +08:00
pub eigenvalues: OVector<T::RealField, D>,
}
impl<T: ComplexField, D: Dim> Copy for SymmetricEigen<T, D>
2021-07-17 17:36:14 +08:00
where
DefaultAllocator: Allocator<T, D, D> + Allocator<T::RealField, D>,
OMatrix<T, D, D>: Copy,
OVector<T::RealField, D>: Copy,
2020-03-21 19:16:46 +08:00
{
}
2021-04-11 17:00:38 +08:00
impl<T: ComplexField, D: Dim> SymmetricEigen<T, D>
2020-04-06 00:49:48 +08:00
where
2021-04-11 17:00:38 +08:00
DefaultAllocator: Allocator<T, D, D> + Allocator<T::RealField, D>,
2018-02-02 19:26:35 +08:00
{
/// Computes the eigendecomposition of the given symmetric matrix.
///
/// Only the lower-triangular parts (including its diagonal) of `m` is read.
2021-04-11 17:00:38 +08:00
pub fn new(m: OMatrix<T, D, D>) -> Self
2018-02-02 19:26:35 +08:00
where
D: DimSub<U1>,
2021-04-11 17:00:38 +08:00
DefaultAllocator: Allocator<T, DimDiff<D, U1>> + Allocator<T::RealField, DimDiff<D, U1>>,
2018-02-02 19:26:35 +08:00
{
2021-04-11 17:00:38 +08:00
Self::try_new(m, T::RealField::default_epsilon(), 0).unwrap()
}
/// Computes the eigendecomposition of the given symmetric matrix with user-specified
/// convergence parameters.
///
/// Only the lower-triangular part (including its diagonal) of `m` is read.
///
/// # Arguments
///
/// * `eps` tolerance used to determine when a value converged to 0.
/// * `max_niter` maximum total number of iterations performed by the algorithm. If this
/// number of iteration is exceeded, `None` is returned. If `niter == 0`, then the algorithm
/// continues indefinitely until convergence.
2021-04-11 17:00:38 +08:00
pub fn try_new(m: OMatrix<T, D, D>, eps: T::RealField, max_niter: usize) -> Option<Self>
2018-02-02 19:26:35 +08:00
where
D: DimSub<U1>,
2021-04-11 17:00:38 +08:00
DefaultAllocator: Allocator<T, DimDiff<D, U1>> + Allocator<T::RealField, DimDiff<D, U1>>,
2018-02-02 19:26:35 +08:00
{
Self::do_decompose(m, true, eps, max_niter).map(|(vals, vecs)| SymmetricEigen {
eigenvectors: vecs.unwrap(),
eigenvalues: vals,
})
}
2018-02-02 19:26:35 +08:00
fn do_decompose(
2021-06-18 15:45:37 +08:00
mut matrix: OMatrix<T, D, D>,
2018-02-02 19:26:35 +08:00
eigenvectors: bool,
2021-04-11 17:00:38 +08:00
eps: T::RealField,
2018-02-02 19:26:35 +08:00
max_niter: usize,
2021-04-11 17:00:38 +08:00
) -> Option<(OVector<T::RealField, D>, Option<OMatrix<T, D, D>>)>
2018-02-02 19:26:35 +08:00
where
D: DimSub<U1>,
2021-04-11 17:00:38 +08:00
DefaultAllocator: Allocator<T, DimDiff<D, U1>> + Allocator<T::RealField, DimDiff<D, U1>>,
2018-02-02 19:26:35 +08:00
{
assert!(
2021-06-18 15:45:37 +08:00
matrix.is_square(),
2018-02-02 19:26:35 +08:00
"Unable to compute the eigendecomposition of a non-square matrix."
);
2021-06-18 15:45:37 +08:00
let dim = matrix.nrows();
let m_amax = matrix.camax();
if !m_amax.is_zero() {
matrix.unscale_mut(m_amax.clone());
}
2021-06-18 15:45:37 +08:00
let (mut q_mat, mut diag, mut off_diag);
if eigenvectors {
2021-06-18 15:45:37 +08:00
let res = SymmetricTridiagonal::new(matrix).unpack();
q_mat = Some(res.0);
2018-02-02 19:26:35 +08:00
diag = res.1;
off_diag = res.2;
} else {
2021-06-18 15:45:37 +08:00
let res = SymmetricTridiagonal::new(matrix).unpack_tridiagonal();
q_mat = None;
2018-02-02 19:26:35 +08:00
diag = res.0;
off_diag = res.1;
}
if dim == 1 {
diag.scale_mut(m_amax);
2021-06-18 15:45:37 +08:00
return Some((diag, q_mat));
}
let mut niter = 0;
let (mut start, mut end) =
Self::delimit_subproblem(&diag, &mut off_diag, dim - 1, eps.clone());
while end != start {
let subdim = end - start + 1;
2021-06-18 15:45:37 +08:00
#[allow(clippy::comparison_chain)]
if subdim > 2 {
let m = end - 1;
let n = end;
2021-06-18 15:45:37 +08:00
let mut vec = Vector2::new(
diag[start].clone()
- wilkinson_shift(
diag[m].clone().clone(),
diag[n].clone(),
off_diag[m].clone().clone(),
),
off_diag[start].clone(),
2018-02-02 19:26:35 +08:00
);
2018-02-02 19:26:35 +08:00
for i in start..n {
let j = i + 1;
2021-06-18 15:45:37 +08:00
if let Some((rot, norm)) = GivensRotation::cancel_y(&vec) {
if i > start {
// Not the first iteration.
off_diag[i - 1] = norm;
}
let mii = diag[i].clone();
let mjj = diag[j].clone();
let mij = off_diag[i].clone();
let cc = rot.c() * rot.c();
let ss = rot.s() * rot.s();
let cs = rot.c() * rot.s();
let b = cs.clone() * crate::convert(2.0) * mij.clone();
diag[i] = (cc.clone() * mii.clone() + ss.clone() * mjj.clone()) - b.clone();
diag[j] = (ss.clone() * mii.clone() + cc.clone() * mjj.clone()) + b;
off_diag[i] = cs * (mii - mjj) + mij * (cc - ss);
if i != n - 1 {
vec.x = off_diag[i].clone();
vec.y = -rot.s() * off_diag[i + 1].clone();
off_diag[i + 1] *= rot.c();
}
2021-06-18 15:45:37 +08:00
if let Some(ref mut q) = q_mat {
2021-04-11 17:00:38 +08:00
let rot = GivensRotation::new_unchecked(rot.c(), T::from_real(rot.s()));
rot.inverse().rotate_rows(&mut q.fixed_columns_mut::<2>(i));
}
2018-02-02 19:26:35 +08:00
} else {
break;
}
}
if off_diag[m].clone().norm1()
<= eps.clone() * (diag[m].clone().norm1() + diag[n].clone().norm1())
{
end -= 1;
}
2018-02-02 19:26:35 +08:00
} else if subdim == 2 {
let m = Matrix2::new(
diag[start].clone(),
off_diag[start].clone().conjugate(),
off_diag[start].clone(),
diag[start + 1].clone(),
2018-02-02 19:26:35 +08:00
);
let eigvals = m.eigenvalues().unwrap();
let basis = Vector2::new(
eigvals.x.clone() - diag[start + 1].clone(),
off_diag[start].clone(),
);
diag[start] = eigvals[0].clone();
diag[start + 1] = eigvals[1].clone();
2021-06-18 15:45:37 +08:00
if let Some(ref mut q) = q_mat {
if let Some((rot, _)) =
GivensRotation::try_new(basis.x.clone(), basis.y.clone(), eps.clone())
{
2021-04-11 17:00:38 +08:00
let rot = GivensRotation::new_unchecked(rot.c(), T::from_real(rot.s()));
rot.rotate_rows(&mut q.fixed_columns_mut::<2>(start));
}
}
end -= 1;
}
2018-09-24 12:48:42 +08:00
// Re-delimit the subproblem in case some decoupling occurred.
let sub = Self::delimit_subproblem(&diag, &mut off_diag, end, eps.clone());
start = sub.0;
2018-02-02 19:26:35 +08:00
end = sub.1;
niter += 1;
if niter == max_niter {
return None;
}
}
diag.scale_mut(m_amax);
2021-06-18 15:45:37 +08:00
Some((diag, q_mat))
}
2018-02-02 19:26:35 +08:00
fn delimit_subproblem(
2021-04-11 17:00:38 +08:00
diag: &OVector<T::RealField, D>,
off_diag: &mut OVector<T::RealField, DimDiff<D, U1>>,
2018-02-02 19:26:35 +08:00
end: usize,
2021-04-11 17:00:38 +08:00
eps: T::RealField,
2018-02-02 19:26:35 +08:00
) -> (usize, usize)
where
D: DimSub<U1>,
2021-04-11 17:00:38 +08:00
DefaultAllocator: Allocator<T::RealField, DimDiff<D, U1>>,
2018-02-02 19:26:35 +08:00
{
let mut n = end;
while n > 0 {
let m = n - 1;
if off_diag[m].clone().norm1()
> eps.clone() * (diag[n].clone().norm1() + diag[m].clone().norm1())
{
break;
}
n -= 1;
}
if n == 0 {
return (0, 0);
}
let mut new_start = n - 1;
while new_start > 0 {
let m = new_start - 1;
if off_diag[m].clone().is_zero()
|| off_diag[m].clone().norm1()
<= eps.clone() * (diag[new_start].clone().norm1() + diag[m].clone().norm1())
2018-02-02 19:26:35 +08:00
{
2021-04-11 17:00:38 +08:00
off_diag[m] = T::RealField::zero();
break;
}
new_start -= 1;
}
(new_start, n)
}
/// Rebuild the original matrix.
///
/// This is useful if some of the eigenvalues have been manually modified.
#[must_use]
pub fn recompose(&self) -> OMatrix<T, D, D> {
let mut u_t = self.eigenvectors.clone();
2018-02-02 19:26:35 +08:00
for i in 0..self.eigenvalues.len() {
let val = self.eigenvalues[i].clone();
u_t.column_mut(i).scale_mut(val);
}
u_t.adjoint_mut();
&self.eigenvectors * u_t
}
}
/// Computes the wilkinson shift, i.e., the 2x2 symmetric matrix eigenvalue to its tailing
/// component `tnn`.
///
/// The inputs are interpreted as the 2x2 matrix:
/// tmm tmn
/// tmn tnn
2021-04-11 17:00:38 +08:00
pub fn wilkinson_shift<T: ComplexField>(tmm: T, tnn: T, tmn: T) -> T {
let sq_tmn = tmn.clone() * tmn;
if !sq_tmn.is_zero() {
2018-09-24 12:48:42 +08:00
// We have the guarantee that the denominator won't be zero.
let d = (tmm - tnn.clone()) * crate::convert(0.5);
tnn - sq_tmn.clone() / (d.clone() + d.clone().signum() * (d.clone() * d + sq_tmn).sqrt())
2018-02-02 19:26:35 +08:00
} else {
tnn
}
}
/*
*
* Computations of eigenvalues for symmetric matrices.
*
*/
2021-04-11 17:00:38 +08:00
impl<T: ComplexField, D: DimSub<U1>, S: Storage<T, D, D>> SquareMatrix<T, D, S>
2020-04-06 00:49:48 +08:00
where
2021-04-11 17:00:38 +08:00
DefaultAllocator: Allocator<T, D, D>
+ Allocator<T, DimDiff<D, U1>>
+ Allocator<T::RealField, D>
+ Allocator<T::RealField, DimDiff<D, U1>>,
2018-02-02 19:26:35 +08:00
{
/// Computes the eigenvalues of this symmetric matrix.
///
/// Only the lower-triangular part of the matrix is read.
#[must_use]
2021-04-11 17:00:38 +08:00
pub fn symmetric_eigenvalues(&self) -> OVector<T::RealField, D> {
2020-03-21 19:16:46 +08:00
SymmetricEigen::do_decompose(
self.clone_owned(),
false,
2021-04-11 17:00:38 +08:00
T::RealField::default_epsilon(),
2020-03-21 19:16:46 +08:00
0,
)
.unwrap()
.0
}
}
#[cfg(test)]
mod test {
2019-03-23 21:29:07 +08:00
use crate::base::Matrix2;
fn expected_shift(m: Matrix2<f64>) -> f64 {
let vals = m.eigenvalues().unwrap();
if (vals.x - m.m22).abs() < (vals.y - m.m22).abs() {
vals.x
} else {
vals.y
}
}
#[cfg(feature = "rand")]
#[test]
fn wilkinson_shift_random() {
2018-02-02 19:26:35 +08:00
for _ in 0..1000 {
let m = Matrix2::new_random();
let m = m * m.transpose();
let expected = expected_shift(m);
let computed = super::wilkinson_shift(m.m11, m.m22, m.m12);
assert!(relative_eq!(expected, computed, epsilon = 1.0e-7));
}
}
#[test]
fn wilkinson_shift_zero() {
2018-02-02 19:26:35 +08:00
let m = Matrix2::new(0.0, 0.0, 0.0, 0.0);
assert!(relative_eq!(
expected_shift(m),
super::wilkinson_shift(m.m11, m.m22, m.m12)
));
}
#[test]
fn wilkinson_shift_zero_diagonal() {
2018-02-02 19:26:35 +08:00
let m = Matrix2::new(0.0, 42.0, 42.0, 0.0);
assert!(relative_eq!(
expected_shift(m),
super::wilkinson_shift(m.m11, m.m22, m.m12)
));
}
#[test]
fn wilkinson_shift_zero_off_diagonal() {
2018-02-02 19:26:35 +08:00
let m = Matrix2::new(42.0, 0.0, 0.0, 64.0);
assert!(relative_eq!(
expected_shift(m),
super::wilkinson_shift(m.m11, m.m22, m.m12)
));
}
#[test]
fn wilkinson_shift_zero_trace() {
2018-02-02 19:26:35 +08:00
let m = Matrix2::new(42.0, 20.0, 20.0, -42.0);
assert!(relative_eq!(
expected_shift(m),
super::wilkinson_shift(m.m11, m.m22, m.m12)
));
}
#[test]
fn wilkinson_shift_zero_diag_diff_and_zero_off_diagonal() {
2018-02-02 19:26:35 +08:00
let m = Matrix2::new(42.0, 0.0, 0.0, 42.0);
assert!(relative_eq!(
expected_shift(m),
super::wilkinson_shift(m.m11, m.m22, m.m12)
));
}
#[test]
fn wilkinson_shift_zero_det() {
2018-02-02 19:26:35 +08:00
let m = Matrix2::new(2.0, 4.0, 4.0, 8.0);
assert!(relative_eq!(
expected_shift(m),
super::wilkinson_shift(m.m11, m.m22, m.m12)
));
}
}