use crate::polynomial::{CubicEqn, RootType};
use crate::primitives::{SymmTensor, Tensor, Vector3, SMALL, VGREAT};
#[must_use]
pub fn eigen_values(t: Tensor) -> Vector3 {
eigen_values_checked(t).0
}
#[must_use]
pub fn eigen_values_checked(t: Tensor) -> (Vector3, bool) {
let b = -t.xx - t.yy - t.zz;
let c = t.xx * t.yy + t.xx * t.zz + t.yy * t.zz - t.xy * t.yx - t.yz * t.zy - t.zx * t.xz;
let d = -t.xx * t.yy * t.zz - t.xy * t.yz * t.zx - t.xz * t.zy * t.yx
+ t.xx * t.yz * t.zy
+ t.yy * t.zx * t.xz
+ t.zz * t.xy * t.yx;
let roots = CubicEqn::new(1.0, b, c, d).roots();
let mut lambda = [0.0_f64; 3];
let mut any_complex = false;
for i in 0..3 {
match roots.root_type(i) {
RootType::Real => lambda[i] = roots.get(i),
RootType::Complex => {
any_complex = true;
lambda[i] = 0.0;
}
RootType::PosInf => lambda[i] = VGREAT,
RootType::NegInf => lambda[i] = -VGREAT,
RootType::Nan => lambda[i] = f64::NAN,
}
}
sort3(&mut lambda);
(Vector3::new(lambda[0], lambda[1], lambda[2]), any_complex)
}
#[must_use]
pub fn eigen_values_symm(t: SymmTensor) -> Vector3 {
let b = -t.xx - t.yy - t.zz;
let c = t.xx * t.yy + t.xx * t.zz + t.yy * t.zz - t.xy * t.xy - t.yz * t.yz - t.xz * t.xz;
let d = -t.xx * t.yy * t.zz - 2.0 * t.xy * t.yz * t.xz
+ t.xx * t.yz * t.yz
+ t.yy * t.xz * t.xz
+ t.zz * t.xy * t.xy;
let roots = CubicEqn::new(1.0, b, c, d).roots();
let mut lambda = [0.0_f64; 3];
for i in 0..3 {
lambda[i] = match roots.root_type(i) {
RootType::Real | RootType::Complex => roots.get(i),
RootType::PosInf => VGREAT,
RootType::NegInf => -VGREAT,
RootType::Nan => f64::NAN,
};
}
sort3(&mut lambda);
Vector3::new(lambda[0], lambda[1], lambda[2])
}
#[must_use]
pub fn eigen_vectors_with(t: Tensor, lambdas: Vector3) -> Tensor {
let ux = eigen_vector_of(
t,
lambdas.x,
Vector3::new(0.0, 1.0, 0.0),
Vector3::new(0.0, 0.0, 1.0),
);
let uy = eigen_vector_of(t, lambdas.y, Vector3::new(0.0, 0.0, 1.0), ux);
let uz = eigen_vector_of(t, lambdas.z, ux, uy);
Tensor::from_rows(ux, uy, uz)
}
#[must_use]
pub fn eigen_vectors(t: Tensor) -> Tensor {
eigen_vectors_with(t, eigen_values(t))
}
#[must_use]
pub fn eigen_vectors_symm_with(t: SymmTensor, lambdas: Vector3) -> Tensor {
orthonormalise(eigen_vectors_with(symm_to_full(t), lambdas))
}
fn orthonormalise(q: Tensor) -> Tensor {
let mut rows = [q.row_x(), q.row_y(), q.row_z()];
for i in 0..3 {
for j in 0..i {
let projection = rows[i].dot(rows[j]);
rows[i] = rows[i] - rows[j] * projection;
}
let m = rows[i].mag();
if m > 1.0e-6 {
rows[i] = rows[i] / m;
} else {
rows[i] = orthogonal_complement(&rows[..i]);
}
}
Tensor::from_rows(rows[0], rows[1], rows[2])
}
fn orthogonal_complement(fixed: &[Vector3]) -> Vector3 {
match fixed.len() {
0 => Vector3::new(1.0, 0.0, 0.0),
1 => {
let v = fixed[0];
let axis = if v.x.abs() <= v.y.abs() && v.x.abs() <= v.z.abs() {
Vector3::new(1.0, 0.0, 0.0)
} else if v.y.abs() <= v.z.abs() {
Vector3::new(0.0, 1.0, 0.0)
} else {
Vector3::new(0.0, 0.0, 1.0)
};
let c = v.cross(axis);
c / c.mag()
}
_ => {
let c = fixed[0].cross(fixed[1]);
c / c.mag()
}
}
}
#[must_use]
pub fn eigen_vectors_symm(t: SymmTensor) -> Tensor {
let lambdas = eigen_values_symm(t);
eigen_vectors_symm_with(t, lambdas)
}
fn symm_to_full(t: SymmTensor) -> Tensor {
Tensor::new(
t.xx, t.xy, t.xz, t.xy, t.yy, t.yz, t.xz, t.yz, t.zz,
)
}
fn eigen_vector_of(t: Tensor, lambda: f64, direction1: Vector3, direction2: Vector3) -> Vector3 {
let a = Tensor::new(
t.xx - lambda,
t.xy,
t.xz,
t.yx,
t.yy - lambda,
t.yz,
t.zx,
t.zy,
t.zz - lambda,
);
let sd0 = a.yy * a.zz - a.yz * a.zy;
let sd1 = a.zz * a.xx - a.zx * a.xz;
let sd2 = a.xx * a.yy - a.xy * a.yx;
let (m0, m1, m2) = (sd0.abs(), sd1.abs(), sd2.abs());
if m0 >= m1 && m0 >= m2 && m0 > SMALL {
let ev = Vector3::new(
1.0,
(a.yz * a.zx - a.zz * a.yx) / sd0,
(a.zy * a.yx - a.yy * a.zx) / sd0,
);
return normalise(ev);
} else if m1 >= m2 && m1 > SMALL {
let ev = Vector3::new(
(a.xz * a.zy - a.zz * a.xy) / sd1,
1.0,
(a.zx * a.xy - a.xx * a.zy) / sd1,
);
return normalise(ev);
} else if m2 > SMALL {
let ev = Vector3::new(
(a.xy * a.yz - a.yy * a.xz) / sd2,
(a.yx * a.xz - a.xx * a.yz) / sd2,
1.0,
);
return normalise(ev);
}
let sd0 = a.yy * direction1.z - a.yz * direction1.y;
let sd1 = a.zz * direction1.x - a.zx * direction1.z;
let sd2 = a.xx * direction1.y - a.xy * direction1.x;
let (m0, m1, m2) = (sd0.abs(), sd1.abs(), sd2.abs());
if m0 >= m1 && m0 >= m2 && m0 > SMALL {
let ev = Vector3::new(
1.0,
(a.yz * direction1.x - direction1.z * a.yx) / sd0,
(direction1.y * a.yx - a.yy * direction1.x) / sd0,
);
return normalise(ev);
} else if m1 >= m2 && m1 > SMALL {
let ev = Vector3::new(
(direction1.z * a.zy - a.zz * direction1.y) / sd1,
1.0,
(a.zx * direction1.y - direction1.x * a.zy) / sd1,
);
return normalise(ev);
} else if m2 > SMALL {
let ev = Vector3::new(
(a.xy * direction1.z - direction1.y * a.xz) / sd2,
(direction1.x * a.xz - a.xx * direction1.z) / sd2,
1.0,
);
return normalise(ev);
}
direction1.cross(direction2)
}
fn normalise(v: Vector3) -> Vector3 {
let m = v.mag();
if m > SMALL {
v / m
} else {
v
}
}
fn sort3(l: &mut [f64; 3]) {
if l[0] > l[1] {
l.swap(0, 1);
}
if l[1] > l[2] {
l.swap(1, 2);
}
if l[0] > l[1] {
l.swap(0, 1);
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn diagonal_eigenvalues_are_the_diagonal_sorted() {
let t = SymmTensor::from_diag(3.0, 1.0, 2.0);
let l = eigen_values_symm(t);
assert_relative_eq!(l.x, 1.0, max_relative = 1e-12);
assert_relative_eq!(l.y, 2.0, max_relative = 1e-12);
assert_relative_eq!(l.z, 3.0, max_relative = 1e-12);
}
#[test]
fn spectrum_reproduces_trace_and_determinant() {
let t = SymmTensor::new(2.0, 0.3, -0.5, 3.0, 0.7, 1.0);
let l = eigen_values_symm(t);
assert_relative_eq!(l.x + l.y + l.z, t.tr(), max_relative = 1e-10);
assert_relative_eq!(l.x * l.y * l.z, t.det(), max_relative = 1e-10);
}
#[test]
fn eigenvectors_satisfy_the_eigenvalue_equation() {
let t = SymmTensor::new(2.0, 0.3, -0.5, 3.0, 0.7, 1.0);
let l = eigen_values_symm(t);
let v = eigen_vectors_symm(t);
for (lambda, row) in [(l.x, v.row_x()), (l.y, v.row_y()), (l.z, v.row_z())] {
let tv = t.mat_vec(row);
let lv = row * lambda;
assert!(
(tv - lv).mag() < 1e-9,
"T·v != λv for λ = {lambda}: residual {}",
(tv - lv).mag()
);
}
}
#[test]
fn symmetric_eigenvectors_are_orthonormal() {
let t = SymmTensor::new(2.0, 0.3, -0.5, 3.0, 0.7, 1.0);
let v = eigen_vectors_symm(t);
let rows = [v.row_x(), v.row_y(), v.row_z()];
for r in rows {
assert!(
(r.mag() - 1.0).abs() < 1e-9,
"row not unit: |v| = {}",
r.mag()
);
}
for i in 0..3 {
for j in (i + 1)..3 {
let d = rows[i].dot(rows[j]);
assert!(d.abs() < 1e-9, "rows {i},{j} not orthogonal: dot = {d}");
}
}
}
#[test]
fn repeated_eigenvalues_still_give_an_orthonormal_basis() {
let t = SymmTensor::from_diag(2.0, 2.0, 5.0);
let l = eigen_values_symm(t);
assert_relative_eq!(l.x, 2.0, max_relative = 1e-12);
assert_relative_eq!(l.y, 2.0, max_relative = 1e-12);
assert_relative_eq!(l.z, 5.0, max_relative = 1e-12);
let v = eigen_vectors_symm(t);
let rows = [v.row_x(), v.row_y(), v.row_z()];
for r in rows {
assert!((r.mag() - 1.0).abs() < 1e-9);
}
for i in 0..3 {
for j in (i + 1)..3 {
assert!(rows[i].dot(rows[j]).abs() < 1e-9);
}
let tv = t.mat_vec(rows[i]);
let lv = rows[i] * [l.x, l.y, l.z][i];
assert!((tv - lv).mag() < 1e-9);
}
}
#[test]
fn triple_eigenvalue_is_handled() {
let t = SymmTensor::from_diag(1.0, 1.0, 1.0);
let l = eigen_values_symm(t);
assert_relative_eq!(l.x, 1.0, max_relative = 1e-12);
assert_relative_eq!(l.z, 1.0, max_relative = 1e-12);
let v = eigen_vectors_symm(t);
let rows = [v.row_x(), v.row_y(), v.row_z()];
for r in rows {
assert!((r.mag() - 1.0).abs() < 1e-9, "|v| = {}", r.mag());
}
for i in 0..3 {
for j in (i + 1)..3 {
assert!(rows[i].dot(rows[j]).abs() < 1e-9);
}
}
}
#[test]
fn a_near_degenerate_spectrum_still_gives_an_orthonormal_basis() {
let a = 1.4_f64;
let c = SymmTensor::new(a * a, 0.0, 0.0, 1.0 * 1.0, 0.0, 1.0 * 1.0);
let l = eigen_values_symm(c);
let v = eigen_vectors_symm(c);
let rows = [v.row_x(), v.row_y(), v.row_z()];
for r in rows {
assert!(
(r.mag() - 1.0).abs() < 1e-9,
"row not unit: |v| = {}",
r.mag()
);
}
for i in 0..3 {
for j in (i + 1)..3 {
let d = rows[i].dot(rows[j]);
assert!(d.abs() < 1e-9, "rows {i},{j} not orthogonal: dot = {d}");
}
}
for (i, r) in rows.iter().enumerate() {
let lambda = [l.x, l.y, l.z][i];
let residual = (c.mat_vec(*r) - *r * lambda).mag();
assert!(
residual < 1e-7,
"row {i}: T*v - lambda*v = {residual:e}, beyond the sqrt(eps) \
bound for a near-degenerate spectrum"
);
}
}
#[test]
fn general_tensor_with_real_spectrum() {
let t = Tensor::new(1.0, 4.0, 5.0, 0.0, 2.0, 6.0, 0.0, 0.0, 3.0);
let (l, complex) = eigen_values_checked(t);
assert!(
!complex,
"upper-triangular real spectrum must not be complex"
);
assert_relative_eq!(l.x, 1.0, max_relative = 1e-10);
assert_relative_eq!(l.y, 2.0, max_relative = 1e-10);
assert_relative_eq!(l.z, 3.0, max_relative = 1e-10);
}
#[test]
fn complex_spectrum_is_reported() {
let t = Tensor::new(0.0, -1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0);
let (_l, complex) = eigen_values_checked(t);
assert!(complex, "a rotation has a complex spectrum and must say so");
}
}