#![allow(non_snake_case)]
use crate::algebra::*;
impl<T> DenseMatrixSym3<T>
where
T: FloatT,
{
pub(crate) fn eigvals(&mut self) -> [T; 3] {
eigen_hybrid(self, &mut None)
}
pub(crate) fn eigen(&mut self, V: &mut DenseMatrix3<T>) -> [T; 3] {
eigen_hybrid(self, &mut Some(V))
}
}
fn eigen_hybrid<T: FloatT>(
A: &mut DenseMatrixSym3<T>,
V: &mut Option<&mut DenseMatrix3<T>>,
) -> [T; 3] {
let w = eigvals_analytic(A);
if eigvecs_analytic(A, w, V).is_ok() {
w
} else {
eigen_jacobi(A, V)
}
}
fn eigvals_analytic<T: FloatT>(A: &DenseMatrixSym3<T>) -> [T; 3] {
let C_27_OVER_2: T = (27.0 / 2.0).as_T();
let C_27_OVER_4: T = (27.0 / 4.0).as_T();
let ONE_THIRD: T = (1.0 / 3.0).as_T();
let SQRT3_INV: T = T::recip(T::sqrt((3.0).as_T()));
let A11 = A.data[0]; let A12 = A.data[1]; let A22 = A.data[2]; let A13 = A.data[3]; let A23 = A.data[4]; let A33 = A.data[5];
let de = A12 * A23;
let dd = A12 * A12;
let ee = A23 * A23;
let ff = A13 * A13;
let m = A11 + A22 + A33;
let c1 = (A11 * (A22 + A33) + A22 * A33) - (dd + ee + ff);
let c0 = A33 * dd + A11 * ee + A22 * ff - A11 * A22 * A33 - A13 * de * (2.0).as_T();
let p = m * m - c1 * (3.0).as_T();
let q = m * (p - c1 * (1.5).as_T()) - C_27_OVER_2 * c0;
let sqrt_p = T::sqrt(T::abs(p));
let phi = ((c1 * c1) * (p - c1) * (0.25).as_T() + c0 * (q + C_27_OVER_4 * c0)) * (27.0).as_T();
let phi = ONE_THIRD * T::atan2(T::sqrt(T::abs(phi)), q);
let s = SQRT3_INV * sqrt_p * T::sin(phi);
let c = sqrt_p * T::cos(phi);
let w1 = ONE_THIRD * (m - c);
let w2 = w1 + s;
let w0 = w1 + c;
let w1 = w1 - s;
[w1, w2, w0]
}
fn eigvecs_analytic<T: FloatT>(
A: &DenseMatrixSym3<T>,
w: [T; 3],
V: &mut Option<&mut DenseMatrix3<T>>,
) -> Result<(), ()> {
let DOT_TOL: T = T::epsilon().sqrt() * (1f64 / 256f64).as_T();
let sidx = sort3_idx(w);
let wmax = w[sidx[0]].abs();
let wmaxsq = wmax * wmax;
let tol = if wmax < T::one() {
T::epsilon() * (256.).as_T() * wmaxsq
} else {
T::epsilon() * (256.).as_T() * (wmaxsq * wmaxsq)
};
let cond = T::abs(w[sidx[0]] / w[sidx[2]]);
if cond > T::epsilon().sqrt().recip() {
return Err(());
}
let a = [
A.data[0] - w[sidx[0]], A.data[1], A.data[3], ];
let b = [
A.data[1], A.data[2] - w[sidx[0]], A.data[4], ];
let mut v1 = cross3(a, b);
let normv1 = norm3(v1);
if normv1 < tol {
return Err(());
}
let a = [
A.data[0] - w[sidx[1]], A.data[1], A.data[3], ];
let b = [
A.data[1], A.data[2] - w[sidx[1]], A.data[4], ];
let mut v2 = cross3(a, b);
let normv2 = norm3(v2);
if normv2 < tol {
return Err(());
}
scale3(&mut v1, normv1.recip());
scale3(&mut v2, normv2.recip());
if dot3(&v1, &v2).abs() > DOT_TOL {
return Err(());
}
let v3 = cross3(v1, v2);
let mut Av3 = [T::zero(); 3];
A.mul(&mut Av3, &v3);
if dot3(&Av3, &v1).abs() > DOT_TOL {
return Err(());
}
if dot3(&Av3, &v2).abs() > DOT_TOL {
return Err(());
}
if let Some(vecs) = V {
vecs.data[0..3].copy_from_slice(&v1);
vecs.data[3..6].copy_from_slice(&v2);
vecs.data[6..9].copy_from_slice(&v3);
}
Ok(()) }
fn eigen_jacobi<T: FloatT>(
A: &mut DenseMatrixSym3<T>,
V: &mut Option<&mut DenseMatrix3<T>>,
) -> [T; 3] {
if let Some(vecs) = V {
vecs.set_identity();
}
let tol = T::epsilon() * (256.).as_T();
for _ in 1..=25 {
let (idx, p, q, max_off_diag) = find_largest_off_diag(A);
if max_off_diag < tol {
break;
}
let (Apq, App, Aqq) = get_rotation_elements(A, idx);
let (c, s, t) = compute_jacobi_rotation(Apq, App, Aqq);
apply_rotation(A, idx, c, s, t);
if let Some(vecs) = V {
update_eigenvectors(vecs, p, q, c, s);
}
}
if let Some(vecs) = V {
sort3_with_columns(A.data[0], A.data[2], A.data[5], vecs)
} else {
sort3(A.data[0], A.data[2], A.data[5])
}
}
fn find_largest_off_diag<T: FloatT>(A: &DenseMatrixSym3<T>) -> (usize, usize, usize, T) {
let abs12 = A.data[1].abs();
let abs13 = A.data[3].abs();
let abs23 = A.data[4].abs();
let mut idx = 1;
let mut max_off_diag = abs12;
let (mut p, mut q) = (0, 1);
if abs13 > max_off_diag {
(idx, max_off_diag) = (3, abs13);
(p, q) = (0, 2);
}
if abs23 > max_off_diag {
(idx, max_off_diag) = (4, abs23);
(p, q) = (1, 2);
}
(idx, p, q, max_off_diag)
}
#[inline(always)]
fn get_rotation_elements<T: FloatT>(A: &DenseMatrixSym3<T>, idx: usize) -> (T, T, T) {
match idx {
1 => (A.data[1], A.data[0], A.data[2]), 3 => (A.data[3], A.data[0], A.data[5]), 4 => (A.data[4], A.data[2], A.data[5]), _ => unreachable!("Invalid index for Jacobi rotation"),
}
}
pub(crate) fn compute_jacobi_rotation<T: FloatT>(Apq: T, App: T, Aqq: T) -> (T, T, T) {
if Apq.is_zero() {
return (T::one(), T::zero(), T::zero());
}
let diffdiag = Aqq - App;
let absApp = App.abs();
let absAqq = Aqq.abs();
let d = if absApp > absAqq { absApp } else { absAqq };
if d < T::epsilon() || diffdiag.abs() < T::epsilon() * d {
let c = T::FRAC_1_SQRT_2();
let s = T::FRAC_1_SQRT_2();
(c, s, T::one())
} else {
let theta = diffdiag / (Apq * (2.0).as_T());
let thetasq = theta * theta;
let t = {
if thetasq.is_finite() {
theta.signum() / (theta.abs() + T::sqrt(T::one() + thetasq))
} else {
let half: T = (0.5).as_T();
half / theta
}
};
let c = T::recip(T::sqrt(T::one() + t * t));
let s = t * c;
(c, s, t)
}
}
fn apply_rotation<T: FloatT>(A: &mut DenseMatrixSym3<T>, idx: usize, c: T, s: T, t: T) {
let tau = s / (c + T::one());
let tApq = t * A.data[idx];
match idx {
1 => {
let (A13, A23) = (A.data[3], A.data[4]);
A.data[3] = A13 - s * (A23 + tau * A13);
A.data[4] = A23 + s * (A13 - tau * A23);
A.data[0] -= tApq;
A.data[2] += tApq;
}
3 => {
let (A12, A23) = (A.data[1], A.data[4]);
A.data[1] = A12 - s * (A23 + tau * A12);
A.data[4] = A23 + s * (A12 - tau * A23);
A.data[0] -= tApq;
A.data[5] += tApq;
}
4 => {
let (A12, A13) = (A.data[1], A.data[3]);
A.data[1] = A12 - s * (A13 + tau * A12);
A.data[3] = A13 + s * (A12 - tau * A13);
A.data[2] -= tApq;
A.data[5] += tApq;
}
_ => unreachable!("Invalid index for Jacobi rotation"),
}
A.data[idx] = T::zero();
}
pub(crate) fn update_eigenvectors<T: FloatT>(
V: &mut DenseMatrix3<T>,
p: usize,
q: usize,
c: T,
s: T,
) {
for i in 0..3 {
let Vip = V[(i, p)];
let Viq = V[(i, q)];
V[(i, p)] = c * Vip - s * Viq;
V[(i, q)] = s * Vip + c * Viq;
}
}
#[inline(always)]
fn sort3<T: FloatT>(mut a: T, mut b: T, mut c: T) -> [T; 3] {
if a > b {
core::mem::swap(&mut a, &mut b);
}
if a > c {
core::mem::swap(&mut a, &mut c);
}
if b > c {
core::mem::swap(&mut b, &mut c);
}
[a, b, c]
}
#[inline(always)]
fn sort3_with_columns<T: FloatT>(mut a: T, mut b: T, mut c: T, M: &mut DenseMatrix3<T>) -> [T; 3] {
if a > b {
core::mem::swap(&mut a, &mut b);
M.data.swap(0, 3);
M.data.swap(1, 4);
M.data.swap(2, 5);
}
if a > c {
core::mem::swap(&mut a, &mut c);
M.data.swap(0, 6);
M.data.swap(1, 7);
M.data.swap(2, 8);
}
if b > c {
core::mem::swap(&mut b, &mut c);
M.data.swap(3, 6);
M.data.swap(4, 7);
M.data.swap(5, 8);
}
[a, b, c]
}
#[inline(always)]
fn sort3_idx<T: FloatT>(v: [T; 3]) -> [usize; 3] {
let abs0 = v[0].abs();
let abs1 = v[1].abs();
let abs2 = v[2].abs();
if abs0 > abs1 && abs0 > abs2 {
if abs1 > abs2 {
[0, 1, 2]
} else {
[0, 2, 1]
}
} else if abs1 > abs2 {
if abs0 > abs2 {
[1, 0, 2]
} else {
[1, 2, 0]
}
} else if abs0 > abs1 {
[2, 0, 1]
} else {
[2, 1, 0]
}
}
#[inline(always)]
fn scale3<T: FloatT>(v: &mut [T; 3], s: T) {
v[0] *= s;
v[1] *= s;
v[2] *= s;
}
#[inline(always)]
fn cross3<T: FloatT>(v1: [T; 3], v2: [T; 3]) -> [T; 3] {
[
v1[1] * v2[2] - v1[2] * v2[1],
v1[2] * v2[0] - v1[0] * v2[2],
v1[0] * v2[1] - v1[1] * v2[0],
]
}
#[inline(always)]
fn dot3<T: FloatT>(v1: &[T; 3], v2: &[T; 3]) -> T {
v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2]
}
#[inline(always)]
fn norm3<T: FloatT>(v: [T; 3]) -> T {
(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
}
#[cfg(test)]
mod test {
use super::*;
use crate::algebra::DenseMatrixSym3;
#[test]
fn test_jacobi_rotation() {
for idx in [1, 3, 4] {
let (mut A, _) = matrix_example_nice();
let (Apq, App, Aqq) = get_rotation_elements(&A, idx);
let (c, s, t) = compute_jacobi_rotation(Apq, App, Aqq);
apply_rotation(&mut A, idx, c, s, t);
assert!(
A.data[idx].abs() < 1e-14,
"Jacobi rotation failed to zero term {}",
idx
);
}
}
#[test]
fn test_sort3() {
let t = [1.0, 2.0, 3.0];
assert_eq!(sort3(1.0, 2.0, 3.0), t);
assert_eq!(sort3(1.0, 3.0, 2.0), t);
assert_eq!(sort3(2.0, 1.0, 3.0), t);
assert_eq!(sort3(2.0, 3.0, 1.0), t);
assert_eq!(sort3(3.0, 1.0, 2.0), t);
assert_eq!(sort3(3.0, 2.0, 1.0), t);
}
fn matrix_example_nice() -> (DenseMatrixSym3<f64>, [f64; 3]) {
#[rustfmt::skip]
let A = DenseMatrixSym3 {
data: [4.0, 2.0, 3.0, 2.0, 1.0, -3.0],
};
let trueD = [-3.565507919110752, 1.47313296881958, 6.092374950291167];
(A, trueD)
}
fn matrix_example_degen() -> (DenseMatrixSym3<f64>, [f64; 3]) {
#[rustfmt::skip]
let A = DenseMatrixSym3 {
data: [-5.0,-5.0,-5.0,-5.0,-5.0,-5.0],
};
let trueD = [-15.0, 0.0, 0.0];
(A, trueD)
}
fn matrix_example_hard() -> (DenseMatrixSym3<f64>, [f64; 3]) {
let A = DenseMatrixSym3 {
data: [1e5, 1e2, 1e5, 1e2, 1e-5, 1e5],
};
let trueD = [99858.57864876269, 99999.99999, 100141.42136123731];
(A, trueD)
}
pub(super) fn check_eigvals(dtest: [f64; 3], dtrue: [f64; 3]) -> bool {
for (d1, d2) in dtest.iter().zip(dtrue.iter()) {
if (d1 - d2).abs() / f64::max(1., d2.abs()) > 1e-7 {
println!(
"Eigenvalue error: abs({} - {}) = {}",
d1,
d2,
(d1 - d2).abs()
);
return false;
}
}
true
}
fn is_ascending_order<T: FloatT>(s: &[T]) -> bool {
s.windows(2).all(|w| w[0] <= w[1])
}
fn check_eigvecs(A: &DenseMatrixSym3<f64>, V: &DenseMatrix3<f64>, w: &[f64; 3]) -> bool {
assert!(is_ascending_order(w));
check_eigvecs_with_tol(A, V, w, 1e-10)
}
pub(super) fn check_eigvecs_with_tol(
A: &DenseMatrixSym3<f64>,
V: &DenseMatrix3<f64>,
w: &[f64; 3],
tol: f64,
) -> bool {
let mut out = [0.; 3];
for (i, wi) in w.iter().enumerate() {
let mut v = V.col_slice(i).to_vec();
A.mul(out.as_mut_slice(), &v);
v.axpby(1.0, &out, -wi);
if v.norm() > tol {
return false;
}
}
true
}
#[test]
fn test_eigen_nice() {
let mut V = DenseMatrix3::<f64>::zeros();
let (A, trueD) = matrix_example_nice();
let D = eigvals_analytic(&A);
assert!(check_eigvals(D, trueD));
assert!(eigvecs_analytic(&A, D, &mut Some(&mut V)).is_ok());
let (A, trueD) = matrix_example_nice();
let D = eigen_jacobi(&mut A.clone(), &mut Some(&mut V));
assert!(check_eigvals(D, trueD));
assert!(check_eigvecs(&A, &V, &D));
}
#[test]
fn test_eigen_degen() {
let mut V = DenseMatrix3::<f64>::zeros();
let (A, trueD) = matrix_example_degen();
let D = eigvals_analytic(&A);
assert!(check_eigvals(D, trueD));
assert!(eigvecs_analytic(&A, D, &mut Some(&mut V)).is_err());
let D = eigen_hybrid(&mut A.clone(), &mut Some(&mut V));
assert!(check_eigvals(D, trueD));
assert!(check_eigvecs(&A, &V, &D));
let (A, trueD) = matrix_example_degen();
let D = eigen_jacobi(&mut A.clone(), &mut Some(&mut V.clone()));
assert!(check_eigvals(D, trueD));
assert!(check_eigvecs(&A, &V, &D));
}
#[test]
#[should_panic]
fn test_eigvals_hard_analytic() {
let (A, trueD) = matrix_example_hard();
let D = eigvals_analytic(&A);
assert!(check_eigvals(D, trueD));
}
#[test]
fn test_eigvals_hard_jacobi() {
let mut V = DenseMatrix3::<f64>::zeros();
let (A, trueD) = matrix_example_hard();
let D = eigen_jacobi(&mut A.clone(), &mut Some(&mut V));
assert!(check_eigvals(D, trueD));
assert!(check_eigvecs(&A, &V, &D));
}
}
#[cfg(all(test, feature = "bench"))]
mod bench {
use super::test::{check_eigvals, check_eigvecs_with_tol};
use super::*;
use crate::algebra::DenseMatrixSym3;
use itertools::iproduct;
use std::ops::Range;
fn sym3_test_iter(
rng: Range<i32>,
fcn: fn(f64) -> f64,
) -> impl Iterator<Item = DenseMatrixSym3<f64>> {
iproduct!(
rng.clone(),
rng.clone(),
rng.clone(),
rng.clone(),
rng.clone(),
rng.clone()
)
.map(move |(a, b, c, d, e, f)| {
let data = [
fcn(a as f64), fcn(b as f64),
fcn(c as f64),
fcn(d as f64),
fcn(e as f64),
fcn(f as f64),
];
DenseMatrixSym3 { data }
})
}
fn check_eigvecs_relaxed(
A: &DenseMatrixSym3<f64>,
V: &DenseMatrix3<f64>,
w: &[f64; 3],
) -> bool {
check_eigvecs_with_tol(A, V, w, 1e-8)
}
fn bench_eigvals_grid_with_transform(fcn: fn(f64) -> f64, name: &str, check_analytic: bool) {
let rng = -5..6;
let iter = sym3_test_iter(rng, fcn);
let mut eng = EigEngine::<f64>::new(3);
let mut count = 0;
let mut V = DenseMatrix3::zeros();
for As in iter {
count += 1;
let mut Af: Matrix<f64> = As.clone().into();
eng.eigen(&mut Af).unwrap();
let mut trueD = [0.0; 3];
trueD.copy_from_slice(&eng.λ);
let D1 = eigvals_analytic(&As);
let _ = eigvecs_analytic(&As, D1, &mut Some(&mut V));
if check_analytic && !check_eigvals(trueD, D1) {
panic!("Eigenvalue error analytic: A = {:?}", As.data);
}
let D2 = eigen_jacobi(&mut As.clone(), &mut None);
if !check_eigvals(trueD, D2) {
println!("Eigenvalue error jacobi: A = {:?}", As);
println!("Eigenvalue error jacobi: D = {:?}", D2);
panic!();
}
}
println!("Eigvals grid ({}) : {} passed", name, count);
}
fn bench_eigen_grid_with_transform(fcn: fn(f64) -> f64, name: &str) {
let rng = -5..6;
let iter = sym3_test_iter(rng, fcn);
let mut fails = 0;
let mut count = 0;
let mut V = DenseMatrix3::zeros();
for As in iter {
count += 1;
let D1 = eigvals_analytic(&As);
if eigvecs_analytic(&As, D1, &mut Some(&mut V)).is_err() {
fails += 1;
}
let mut Am = As.clone();
let D2 = Am.eigen(&mut V);
check_eigvecs_relaxed(&As, &V, &D2);
}
println!("Eigen grid ({}) : {} of {} failed", name, fails, count);
}
#[test]
fn bench_eigvals_grid() {
bench_eigvals_grid_with_transform(|x| x, "linear", true);
}
#[test]
fn bench_eigvals_exp_grid() {
bench_eigvals_grid_with_transform(|x| (10_f64).powf(x), "10^x", false);
}
#[test]
fn bench_eigen_grid() {
bench_eigen_grid_with_transform(|x| x, "linear");
}
#[test]
fn bench_eigen_exp_grid() {
bench_eigen_grid_with_transform(|x| (10_f64).powf(x), "10^x");
}
}