#![allow(clippy::excessive_precision)]
use crate::math::array::Array;
use crate::math::matrix::Matrix;
use crate::types::{Real, Size};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EigenVectorCalculation {
WithEigenVector,
WithoutEigenVector,
OnlyFirstRowEigenVector,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ShiftStrategy {
NoShift,
Overrelaxation,
CloseEigenValue,
}
#[derive(Clone, Debug)]
pub struct TqrEigenDecomposition {
iterations: Size,
d: Array,
ev: Matrix,
}
impl TqrEigenDecomposition {
pub fn new(
diag: &Array,
sub: &Array,
calc: EigenVectorCalculation,
strategy: ShiftStrategy,
) -> Self {
let n = diag.size();
assert!(n > 0, "null diagonal given");
assert!(n == sub.size() + 1, "wrong dimensions");
let mut d = diag.clone();
let rows = match calc {
EigenVectorCalculation::WithEigenVector => n,
EigenVectorCalculation::WithoutEigenVector => 0,
EigenVectorCalculation::OnlyFirstRowEigenVector => 1,
};
let mut ev = Matrix::with_size(rows, n);
for i in 0..rows {
ev[(i, i)] = 1.0;
}
let mut e = Array::with_size(n);
for i in 1..n {
e[i] = sub[i - 1];
}
let mut iterations: Size = 0;
for k in (1..n).rev() {
while !off_diag_is_zero(k, &d, &e) {
let mut l = k;
loop {
l -= 1;
if l == 0 || off_diag_is_zero(l, &d, &e) {
break;
}
}
iterations += 1;
let mut q = d[l];
if strategy != ShiftStrategy::NoShift {
let t1 = (0.25 * (d[k] * d[k] + d[k - 1] * d[k - 1]) - 0.5 * d[k - 1] * d[k]
+ e[k] * e[k])
.sqrt();
let t2 = 0.5 * (d[k] + d[k - 1]);
let lambda = if (t2 + t1 - d[k]).abs() < (t2 - t1 - d[k]).abs() {
t2 + t1
} else {
t2 - t1
};
if strategy == ShiftStrategy::CloseEigenValue {
q -= lambda;
} else {
q -= if k == n - 1 { 1.25 } else { 1.0 } * lambda;
}
}
let mut sine = 1.0;
let mut cosine = 1.0;
let mut u = 0.0;
let mut recover_underflow = false;
let mut i = l + 1;
while i <= k && !recover_underflow {
let h = cosine * e[i];
let p = sine * e[i];
e[i - 1] = (p * p + q * q).sqrt();
if e[i - 1] != 0.0 {
sine = p / e[i - 1];
cosine = q / e[i - 1];
let g = d[i - 1] - u;
let t = (d[i] - g) * sine + 2.0 * cosine * h;
u = sine * t;
d[i - 1] = g + u;
q = cosine * t - h;
for j in 0..rows {
let tmp = ev[(j, i - 1)];
ev[(j, i - 1)] = sine * ev[(j, i)] + cosine * tmp;
ev[(j, i)] = cosine * ev[(j, i)] - sine * tmp;
}
} else {
d[i - 1] -= u;
e[l] = 0.0;
recover_underflow = true;
}
i += 1;
}
if !recover_underflow {
d[k] -= u;
e[k] = q;
e[l] = 0.0;
}
}
}
let mut temp: Vec<(Real, Vec<Real>)> = (0..n)
.map(|i| {
let eigen_vector: Vec<Real> = (0..rows).map(|j| ev[(j, i)]).collect();
(d[i], eigen_vector)
})
.collect();
temp.sort_by(|a, b| b.partial_cmp(a).expect("eigenvalues are not NaN"));
for (i, (value, vector)) in temp.iter().enumerate() {
d[i] = *value;
let sign = if rows > 0 && vector[0] < 0.0 {
-1.0
} else {
1.0
};
for (j, component) in vector.iter().enumerate() {
ev[(j, i)] = sign * component;
}
}
TqrEigenDecomposition { iterations, d, ev }
}
pub fn eigenvalues(&self) -> &Array {
&self.d
}
pub fn eigenvectors(&self) -> &Matrix {
&self.ev
}
pub fn iterations(&self) -> Size {
self.iterations
}
}
#[allow(clippy::float_cmp)]
fn off_diag_is_zero(k: Size, d: &Array, e: &Array) -> bool {
d[k - 1].abs() + d[k].abs() == d[k - 1].abs() + d[k].abs() + e[k].abs()
}
#[cfg(test)]
mod tests {
use super::*;
const TOLERANCE: Real = 1.0e-10;
#[test]
fn eigenvalue_decomposition() {
let diag: Array = [11.0, 7.0, 6.0, 2.0, 0.0].into_iter().collect();
let sub = Array::filled(4, 1.0);
let expected = [
11.2467832217139119,
7.4854967362908535,
5.5251516080277518,
2.1811760273123308,
-0.4386075933448487,
];
let tqre = TqrEigenDecomposition::new(
&diag,
&sub,
EigenVectorCalculation::WithoutEigenVector,
ShiftStrategy::CloseEigenValue,
);
for (i, &ev) in expected.iter().enumerate() {
let calculated = tqre.eigenvalues()[i];
assert!(
(ev - calculated).abs() <= TOLERANCE,
"wrong eigenvalue: calculated {calculated}, expected {ev}"
);
}
}
#[test]
fn zero_off_diagonal_eigenvalues() {
let diag: Array = [12.0, 9.0, 6.0, 3.0, 0.0].into_iter().collect();
let mut sub = Array::filled(4, 1.0);
sub[0] = 0.0;
sub[2] = 0.0;
let tqre1 = TqrEigenDecomposition::new(
&diag,
&sub,
EigenVectorCalculation::WithEigenVector,
ShiftStrategy::CloseEigenValue,
);
sub[0] = 1e-14;
sub[2] = 1e-14;
let tqre2 = TqrEigenDecomposition::new(
&diag,
&sub,
EigenVectorCalculation::WithEigenVector,
ShiftStrategy::CloseEigenValue,
);
for i in 0..diag.size() {
let expected = tqre2.eigenvalues()[i];
let calculated = tqre1.eigenvalues()[i];
assert!(
(expected - calculated).abs() <= TOLERANCE,
"wrong eigenvalue: calculated {calculated}, expected {expected}"
);
}
}
#[test]
fn eigenvector_decomposition() {
let diag = Array::filled(2, 1.0);
let sub = Array::filled(1, 1.0);
let tqre = TqrEigenDecomposition::new(
&diag,
&sub,
EigenVectorCalculation::WithEigenVector,
ShiftStrategy::CloseEigenValue,
);
let ev = tqre.eigenvectors();
let product = ev[(0, 0)] * ev[(0, 1)] * ev[(1, 0)] * ev[(1, 1)];
assert!(
(0.25 + product).abs() <= TOLERANCE,
"wrong eigenvector: product {product}, expected -0.25"
);
}
}