use nalgebra::{Matrix3, Vector3};
pub(crate) fn solve_gep_3x3(system: &Matrix3<f64>, _c1: &Matrix3<f64>) -> Option<Vector3<f64>> {
let a = system;
let tr = a[(0, 0)] + a[(1, 1)] + a[(2, 2)];
let minor_sum = a[(0, 0)] * a[(1, 1)] - a[(0, 1)] * a[(1, 0)] + a[(0, 0)] * a[(2, 2)]
- a[(0, 2)] * a[(2, 0)]
+ a[(1, 1)] * a[(2, 2)]
- a[(1, 2)] * a[(2, 1)];
let det = a.determinant();
let eigenvalues = solve_cubic_real(1.0, -tr, minor_sum, -det);
if let Some(v) = select_constrained_eigenvector(system, &eigenvalues) {
return Some(v);
}
let fallback = schur_real_eigenvalues(system);
select_constrained_eigenvector(system, &fallback)
}
fn select_constrained_eigenvector(
system: &Matrix3<f64>,
eigenvalues: &[f64],
) -> Option<Vector3<f64>> {
let mut best_vec = None;
let mut best_ev = f64::MAX;
for &ev in eigenvalues {
let shifted = system - Matrix3::identity() * ev;
let Some(v) = null_vector_3x3(&shifted) else {
continue;
};
let constraint = 4.0 * v[0] * v[2] - v[1] * v[1];
if constraint > 0.0 {
if ev.abs() < best_ev {
best_ev = ev.abs();
best_vec = Some(v);
}
}
}
best_vec
}
fn schur_real_eigenvalues(system: &Matrix3<f64>) -> Vec<f64> {
let scale = system.norm();
if !scale.is_finite() {
return Vec::new();
}
let imag_tol = 1e-9 * scale.max(1.0);
system
.complex_eigenvalues()
.iter()
.filter(|c| c.im.abs() <= imag_tol)
.map(|c| c.re)
.collect()
}
fn null_vector_3x3(m: &Matrix3<f64>) -> Option<Vector3<f64>> {
let cofactors = [
Vector3::new(
m[(1, 1)] * m[(2, 2)] - m[(1, 2)] * m[(2, 1)],
-(m[(1, 0)] * m[(2, 2)] - m[(1, 2)] * m[(2, 0)]),
m[(1, 0)] * m[(2, 1)] - m[(1, 1)] * m[(2, 0)],
),
Vector3::new(
-(m[(0, 1)] * m[(2, 2)] - m[(0, 2)] * m[(2, 1)]),
m[(0, 0)] * m[(2, 2)] - m[(0, 2)] * m[(2, 0)],
-(m[(0, 0)] * m[(2, 1)] - m[(0, 1)] * m[(2, 0)]),
),
Vector3::new(
m[(0, 1)] * m[(1, 2)] - m[(0, 2)] * m[(1, 1)],
-(m[(0, 0)] * m[(1, 2)] - m[(0, 2)] * m[(1, 0)]),
m[(0, 0)] * m[(1, 1)] - m[(0, 1)] * m[(1, 0)],
),
];
let mut best = &cofactors[0];
let mut best_norm = best.norm_squared();
for c in &cofactors[1..] {
let n = c.norm_squared();
if n > best_norm {
best = c;
best_norm = n;
}
}
if best_norm < 1e-30 {
return None;
}
Some(best / best_norm.sqrt())
}
fn solve_cubic_real(a: f64, b: f64, c: f64, d: f64) -> Vec<f64> {
let a_inv = 1.0 / a;
let b_ = b * a_inv;
let c_ = c * a_inv;
let d_ = d * a_inv;
let p = c_ - b_ * b_ / 3.0;
let q = 2.0 * b_ * b_ * b_ / 27.0 - b_ * c_ / 3.0 + d_;
let disc = -4.0 * p * p * p - 27.0 * q * q;
let shift = -b_ / 3.0;
if disc >= 0.0 {
let r = (-p / 3.0).sqrt();
let cos_arg = if r.abs() < 1e-15 {
0.0
} else {
(-q / (2.0 * r * r * r)).clamp(-1.0, 1.0)
};
let theta = cos_arg.acos();
let two_r = 2.0 * r;
vec![
two_r * (theta / 3.0).cos() + shift,
two_r * ((theta + 2.0 * std::f64::consts::PI) / 3.0).cos() + shift,
two_r * ((theta + 4.0 * std::f64::consts::PI) / 3.0).cos() + shift,
]
} else {
let sqrt_disc = (q * q / 4.0 + p * p * p / 27.0).sqrt();
let u = (-q / 2.0 + sqrt_disc).cbrt();
let v = (-q / 2.0 - sqrt_disc).cbrt();
vec![u + v + shift]
}
}
#[cfg(test)]
mod tests {
use super::*;
fn repeated_eigenvalue_system() -> Matrix3<f64> {
Matrix3::new(3.5, 0.0, 1.5, 0.0, 2.0, 0.0, 1.5, 0.0, 3.5)
}
#[test]
fn solve_gep_survives_repeated_eigenvalue() {
let system = repeated_eigenvalue_system();
let c1 = Matrix3::new(0.0, 0.0, 2.0, 0.0, -1.0, 0.0, 2.0, 0.0, 0.0);
let v = solve_gep_3x3(&system, &c1)
.expect("repeated non-selected eigenvalue must not abort the solve");
let normalized = v / v[0];
assert!((normalized[0] - 1.0).abs() < 1e-12);
assert!(normalized[1].abs() < 1e-12);
assert!((normalized[2] - 1.0).abs() < 1e-12);
let residual = (system * v - v * 5.0).norm();
assert!(residual < 1e-9, "residual {residual}");
}
#[test]
fn schur_fallback_recovers_real_eigenvalues() {
let system = repeated_eigenvalue_system();
let mut evs = schur_real_eigenvalues(&system);
evs.sort_by(f64::total_cmp);
assert_eq!(evs.len(), 3, "all eigenvalues are real: {evs:?}");
assert!((evs[0] - 2.0).abs() < 1e-9);
assert!((evs[1] - 2.0).abs() < 1e-9);
assert!((evs[2] - 5.0).abs() < 1e-9);
}
#[test]
fn schur_fallback_discards_complex_pairs() {
let system = Matrix3::new(1.0, -1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 3.0);
let evs = schur_real_eigenvalues(&system);
assert_eq!(evs.len(), 1, "complex pair must be discarded: {evs:?}");
assert!((evs[0] - 3.0).abs() < 1e-9);
}
}