Skip to main content

qualia_core_db/solvers/linear_algebra/
spectral.rs

1//! Matrix-spectral routines that bridge linear algebra and polynomial algebra:
2//! the characteristic polynomial and general (non-symmetric) eigenvalues.
3//!
4//! `characteristic_polynomial` uses Faddeev–LeVerrier; `eigenvalues_general` factors it
5//! with the engine's [`crate::solvers::polynomial::polynomial_roots`]. For symmetric
6//! matrices prefer [`super::eigen::symmetric_eigen`] (eigenvectors + better conditioning).
7
8use crate::solvers::polynomial::{polynomial_roots, Complex};
9use crate::solvers::SolversError;
10
11/// Characteristic polynomial of a row-major `n×n` matrix via the Faddeev–LeVerrier
12/// algorithm. Returns DESCENDING coefficients `[1, c₁, …, cₙ]` of
13/// `p(λ) = λⁿ + c₁λⁿ⁻¹ + … + cₙ` (so `det(A) = (-1)ⁿ·cₙ`). Exact for integer matrices;
14/// for large/ill-conditioned matrices prefer an iterative eigensolver.
15pub fn characteristic_polynomial(n: usize, data: &[f64]) -> Result<Vec<f64>, SolversError> {
16    if n == 0 || data.len() != n * n {
17        return Err(SolversError::InvalidDimension);
18    }
19    // Each Faddeev–LeVerrier step needs the dense product `A·M` (`n×n · n×n`). Route it
20    // through the engine's one GEMM (`super::gemm::matmul`), which itself picks the
21    // best path on this machine — offloading to the forge dispatcher above
22    // `GEMM_GPU_THRESHOLD` on an accelerator, and otherwise running its exact f64 CPU
23    // floor (same increasing-`k` accumulation, so byte-identical off-accelerator). The
24    // whole algorithm is `n` such products (O(n⁴)), so a large characteristic-polynomial
25    // is exactly the case the accelerator earns its keep.
26    let mul = |x: &[f64], y: &[f64]| -> Result<Vec<f64>, SolversError> {
27        let mut out = vec![0.0_f64; n * n];
28        super::gemm::matmul(n, n, n, x, y, &mut out)?;
29        Ok(out)
30    };
31    let trace = |x: &[f64]| -> f64 { (0..n).map(|i| x[i * n + i]).sum() };
32
33    let mut coeffs = vec![0.0_f64; n + 1];
34    coeffs[0] = 1.0;
35    // M starts as the identity.
36    let mut m = vec![0.0_f64; n * n];
37    for i in 0..n {
38        m[i * n + i] = 1.0;
39    }
40    for k in 1..=n {
41        let am = mul(data, &m)?;
42        let ck = -trace(&am) / (k as f64);
43        coeffs[k] = ck;
44        // M ← A·M + ck·I  (not needed after the final iteration).
45        m = am;
46        for i in 0..n {
47            m[i * n + i] += ck;
48        }
49    }
50    Ok(coeffs)
51}
52
53/// Eigenvalues of a GENERAL (not necessarily symmetric) row-major `n×n` matrix, as
54/// complex numbers. Computes the characteristic polynomial (Faddeev–LeVerrier) and finds
55/// its roots. Returns all `n` eigenvalues; real ones have `im ≈ 0`.
56pub fn eigenvalues_general(n: usize, data: &[f64]) -> Result<Vec<Complex>, SolversError> {
57    let charpoly = characteristic_polynomial(n, data)?;
58    polynomial_roots(&charpoly)
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64
65    #[test]
66    fn charpoly_of_2x2() {
67        // [[2,0],[0,3]] → (λ−2)(λ−3) = λ² − 5λ + 6 → [1, −5, 6]
68        let c = characteristic_polynomial(2, &[2.0, 0.0, 0.0, 3.0]).unwrap();
69        assert!((c[0] - 1.0).abs() < 1e-12);
70        assert!((c[1] + 5.0).abs() < 1e-9);
71        assert!((c[2] - 6.0).abs() < 1e-9);
72    }
73
74    #[test]
75    fn general_eigenvalues_real() {
76        // [[2,0],[0,3]] → eigenvalues {2,3}.
77        let ev = eigenvalues_general(2, &[2.0, 0.0, 0.0, 3.0]).unwrap();
78        assert_eq!(ev.len(), 2);
79        let mut reals: Vec<f64> = ev.iter().map(|z| z.re).collect();
80        reals.sort_by(|a, b| a.partial_cmp(b).unwrap());
81        assert!((reals[0] - 2.0).abs() < 1e-6 && (reals[1] - 3.0).abs() < 1e-6);
82    }
83
84    #[test]
85    fn general_eigenvalues_complex_rotation() {
86        // 90° rotation [[0,-1],[1,0]] → eigenvalues ±i.
87        let ev = eigenvalues_general(2, &[0.0, -1.0, 1.0, 0.0]).unwrap();
88        assert_eq!(ev.len(), 2);
89        assert!(ev.iter().any(|z| (z.im.abs() - 1.0).abs() < 1e-6));
90    }
91
92    #[test]
93    fn rejects_bad_dims() {
94        assert!(matches!(
95            characteristic_polynomial(2, &[1.0, 2.0, 3.0]),
96            Err(SolversError::InvalidDimension)
97        ));
98    }
99}