use oxiblas_core::scalar::{Field, Real, Scalar};
use oxiblas_matrix::{Mat, MatRef};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SymmetricEvdDcError {
EmptyMatrix,
NotSquare,
NotConverged,
SecularEquationFailed,
}
impl core::fmt::Display for SymmetricEvdDcError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::EmptyMatrix => write!(f, "Matrix is empty"),
Self::NotSquare => write!(f, "Matrix is not square"),
Self::NotConverged => write!(f, "Algorithm did not converge"),
Self::SecularEquationFailed => write!(f, "Secular equation solver failed"),
}
}
}
impl std::error::Error for SymmetricEvdDcError {}
#[derive(Debug, Clone)]
pub struct SymmetricEvdDc<T: Scalar> {
eigenvalues: Vec<T>,
eigenvectors: Mat<T>,
n: usize,
}
const SMLSIZ: usize = 25;
const MAX_SECULAR_ITER: usize = 100;
impl<T: Field + Real + bytemuck::Zeroable> SymmetricEvdDc<T> {
pub fn compute(a: MatRef<'_, T>) -> Result<Self, SymmetricEvdDcError> {
let n = a.nrows();
if n == 0 {
return Err(SymmetricEvdDcError::EmptyMatrix);
}
if n != a.ncols() {
return Err(SymmetricEvdDcError::NotSquare);
}
if n == 1 {
let eigenvalues = vec![a[(0, 0)]];
let mut eigenvectors = Mat::zeros(1, 1);
eigenvectors[(0, 0)] = T::one();
return Ok(Self {
eigenvalues,
eigenvectors,
n,
});
}
let mut work = Mat::zeros(n, n);
for i in 0..n {
for j in i..n {
let val = a[(i, j)];
work[(i, j)] = val;
work[(j, i)] = val;
}
}
let mut q = Mat::eye(n);
let (diag, off_diag) = tridiagonalize(&mut work, &mut q, n);
let (eigenvalues, z_mat) = tridiag_dc(diag, off_diag, SMLSIZ)?;
let mut v = Mat::zeros(n, n);
for jc in 0..n {
for r in 0..n {
let mut acc = T::zero();
for kk in 0..n {
acc = acc + q[(r, kk)] * z_mat[(kk, jc)];
}
v[(r, jc)] = acc;
}
}
Ok(Self {
eigenvalues,
eigenvectors: v,
n,
})
}
pub fn eigenvalues(&self) -> &[T] {
&self.eigenvalues
}
pub fn eigenvectors(&self) -> MatRef<'_, T> {
self.eigenvectors.as_ref()
}
pub fn dim(&self) -> usize {
self.n
}
pub fn reconstruct(&self) -> Mat<T> {
let n = self.n;
let mut a = Mat::zeros(n, n);
for k in 0..n {
let lambda = self.eigenvalues[k];
for i in 0..n {
for j in 0..n {
a[(i, j)] =
a[(i, j)] + lambda * self.eigenvectors[(i, k)] * self.eigenvectors[(j, k)];
}
}
}
a
}
}
fn tridiagonalize<T: Field + Real>(a: &mut Mat<T>, v: &mut Mat<T>, n: usize) -> (Vec<T>, Vec<T>) {
let mut diag = vec![T::zero(); n];
let mut off_diag = vec![T::zero(); n.saturating_sub(1)];
for k in 0..(n.saturating_sub(2)) {
let mut norm_sq = T::zero();
for i in (k + 1)..n {
norm_sq = norm_sq + a[(i, k)] * a[(i, k)];
}
let norm = Real::sqrt(norm_sq);
if norm > T::zero() {
let x_k1 = a[(k + 1, k)];
let beta = if x_k1 >= T::zero() { -norm } else { norm };
let tau = (beta - x_k1) / beta;
let scale = T::one() / (x_k1 - beta);
for i in (k + 2)..n {
a[(i, k)] = a[(i, k)] * scale;
}
let mut p = vec![T::zero(); n];
for i in (k + 1)..n {
for j in (k + 1)..n {
let v_j = if j == k + 1 { T::one() } else { a[(j, k)] };
p[i] = p[i] + a[(i, j)] * v_j;
}
p[i] = tau * p[i];
}
let mut ptv = T::zero();
for i in (k + 1)..n {
let v_i = if i == k + 1 { T::one() } else { a[(i, k)] };
ptv = ptv + p[i] * v_i;
}
let half_tau = tau / (T::one() + T::one());
let mut w = vec![T::zero(); n];
for i in (k + 1)..n {
let v_i = if i == k + 1 { T::one() } else { a[(i, k)] };
w[i] = p[i] - half_tau * ptv * v_i;
}
for i in (k + 1)..n {
let v_i = if i == k + 1 { T::one() } else { a[(i, k)] };
for j in (k + 1)..n {
let v_j = if j == k + 1 { T::one() } else { a[(j, k)] };
a[(i, j)] = a[(i, j)] - v_i * w[j] - w[i] * v_j;
}
}
for i in 0..n {
let mut vv = T::zero();
for j in (k + 1)..n {
let v_j = if j == k + 1 { T::one() } else { a[(j, k)] };
vv = vv + v[(i, j)] * v_j;
}
let tau_vv = tau * vv;
for j in (k + 1)..n {
let v_j = if j == k + 1 { T::one() } else { a[(j, k)] };
v[(i, j)] = v[(i, j)] - tau_vv * v_j;
}
}
off_diag[k] = beta;
}
}
for i in 0..n {
diag[i] = a[(i, i)];
}
if n >= 2 {
off_diag[n - 2] = a[(n - 1, n - 2)];
}
(diag, off_diag)
}
fn tridiag_dc<T: Field + Real + bytemuck::Zeroable>(
diag: Vec<T>,
off_diag: Vec<T>,
smlsiz: usize,
) -> Result<(Vec<T>, Mat<T>), SymmetricEvdDcError> {
let n = diag.len();
if n == 0 {
return Ok((Vec::new(), Mat::zeros(0, 0)));
}
if n == 1 {
let mut z = Mat::zeros(1, 1);
z[(0, 0)] = T::one();
return Ok((diag, z));
}
if n <= smlsiz {
let mut z = Mat::eye(n);
let eig = qr_algorithm(diag, off_diag, &mut z, n)?;
return Ok((eig, z));
}
let m = n / 2;
let beta = off_diag[m - 1];
let abs_beta = Scalar::abs(beta);
let mut diag1: Vec<T> = diag[0..m].to_vec();
let mut diag2: Vec<T> = diag[m..n].to_vec();
diag1[m - 1] = diag1[m - 1] - abs_beta;
diag2[0] = diag2[0] - abs_beta;
let off1: Vec<T> = if m > 1 {
off_diag[0..(m - 1)].to_vec()
} else {
Vec::new()
};
let off2: Vec<T> = if n - m > 1 {
off_diag[m..(n - 1)].to_vec()
} else {
Vec::new()
};
let (eig1, q1) = tridiag_dc(diag1, off1, smlsiz)?;
let (eig2, q2) = tridiag_dc(diag2, off2, smlsiz)?;
merge_rank_one(&eig1, &q1, &eig2, &q2, beta, m, n)
}
fn merge_rank_one<T: Field + Real + bytemuck::Zeroable>(
eig1: &[T],
q1: &Mat<T>,
eig2: &[T],
q2: &Mat<T>,
beta: T,
m: usize,
n: usize,
) -> Result<(Vec<T>, Mat<T>), SymmetricEvdDcError> {
let mut d = vec![T::zero(); n];
let mut z = vec![T::zero(); n];
let mut qmat: Mat<T> = Mat::zeros(n, n);
for a in 0..m {
d[a] = eig1[a];
z[a] = q1[(m - 1, a)];
for r in 0..m {
qmat[(r, a)] = q1[(r, a)];
}
}
let sgn = if beta < T::zero() {
-T::one()
} else {
T::one()
};
for a in 0..(n - m) {
d[m + a] = eig2[a];
z[m + a] = sgn * q2[(0, a)];
for r in 0..(n - m) {
qmat[(m + r, m + a)] = q2[(r, a)];
}
}
let mut rho = Scalar::abs(beta);
let mut znorm_sq = T::zero();
for &za in z.iter() {
znorm_sq = znorm_sq + za * za;
}
if !(rho > T::zero()) || !(znorm_sq > T::zero()) {
let mut eigenvalues = d;
let mut vmat = qmat;
sort_eigenvalues(&mut eigenvalues, &mut vmat, n);
return Ok((eigenvalues, vmat));
}
let znorm = Real::sqrt(znorm_sq);
for za in z.iter_mut() {
*za = *za / znorm;
}
rho = rho * znorm_sq;
let mut perm: Vec<usize> = (0..n).collect();
perm.sort_by(|&x, &y| d[x].partial_cmp(&d[y]).unwrap_or(std::cmp::Ordering::Equal));
let d_sorted: Vec<T> = perm.iter().map(|&x| d[x]).collect();
let z_sorted: Vec<T> = perm.iter().map(|&x| z[x]).collect();
let mut q_sorted: Mat<T> = Mat::zeros(n, n);
for (newc, &oldc) in perm.iter().enumerate() {
for r in 0..n {
q_sorted[(r, newc)] = qmat[(r, oldc)];
}
}
let mut d = d_sorted;
let mut z = z_sorted;
let mut qmat = q_sorted;
let eps = <T as Scalar>::epsilon();
let mut dmax = T::zero();
let mut zmax = T::zero();
for (&da, &za) in d.iter().zip(z.iter()) {
let ad = Scalar::abs(da);
if ad > dmax {
dmax = ad;
}
let az = Scalar::abs(za);
if az > zmax {
zmax = az;
}
}
let eight = T::from_f64(8.0).unwrap_or(T::one());
let tol = eight * eps * if dmax > zmax { dmax } else { zmax };
let mut active: Vec<usize> = Vec::new();
let mut deflated: Vec<usize> = Vec::new();
let mut pj: Option<usize> = None;
for s in 0..n {
if rho * Scalar::abs(z[s]) <= tol {
deflated.push(s);
continue;
}
match pj {
None => pj = Some(s),
Some(p) => {
let zp = z[p];
let zc = z[s];
let tau = Real::hypot(zc, zp);
let c = zc / tau;
let sn = zp / tau;
let t = d[s] - d[p];
if Scalar::abs(t * c * sn) <= tol {
for r in 0..n {
let qa = qmat[(r, p)];
let qb = qmat[(r, s)];
qmat[(r, p)] = c * qa - sn * qb;
qmat[(r, s)] = sn * qa + c * qb;
}
let dp_new = d[p] * c * c + d[s] * sn * sn;
let ds_new = d[p] * sn * sn + d[s] * c * c;
d[p] = dp_new;
d[s] = ds_new;
z[p] = T::zero();
z[s] = tau;
deflated.push(p);
pj = Some(s);
} else {
active.push(p);
pj = Some(s);
}
}
}
}
if let Some(p) = pj {
active.push(p);
}
active.sort_by(|&x, &y| d[x].partial_cmp(&d[y]).unwrap_or(std::cmp::Ordering::Equal));
let k = active.len();
let mut eigenvalues = vec![T::zero(); n];
let mut vmat: Mat<T> = Mat::zeros(n, n);
if k == 0 {
for (idx, &col) in deflated.iter().enumerate() {
eigenvalues[idx] = d[col];
for r in 0..n {
vmat[(r, idx)] = qmat[(r, col)];
}
}
sort_eigenvalues(&mut eigenvalues, &mut vmat, n);
return Ok((eigenvalues, vmat));
}
let dhat: Vec<T> = active.iter().map(|&a| d[a]).collect();
let zhat: Vec<T> = active.iter().map(|&a| z[a]).collect();
let zeta: Vec<T> = zhat.iter().map(|&zz| rho * zz * zz).collect();
let mut lambda = vec![T::zero(); k];
let mut delta_mat: Vec<Vec<T>> = Vec::with_capacity(k);
for l in 0..k {
let (lam, del) = solve_secular_i(&dhat, &zeta, l, k)?;
lambda[l] = lam;
delta_mat.push(del);
}
let mut zt = vec![T::zero(); k];
for a in 0..k {
let mut prod = -delta_mat[a][a]; for l in 0..k {
if l == a {
continue;
}
let num = -delta_mat[l][a]; let den = dhat[l] - dhat[a]; prod = prod * (num / den);
}
let val = prod / rho;
let mag = if val > T::zero() {
Real::sqrt(val)
} else {
T::zero()
};
zt[a] = if zhat[a] >= T::zero() { mag } else { -mag };
}
for l in 0..k {
let mut u = vec![T::zero(); k];
let mut nrm_sq = T::zero();
for a in 0..k {
let val = zt[a] / delta_mat[l][a];
u[a] = val;
nrm_sq = nrm_sq + val * val;
}
let nrm = Real::sqrt(nrm_sq);
let inv = if nrm > T::zero() {
T::one() / nrm
} else {
T::one()
};
for r in 0..n {
let mut acc = T::zero();
for (a, &ua) in u.iter().enumerate() {
acc = acc + ua * qmat[(r, active[a])];
}
vmat[(r, l)] = acc * inv;
}
eigenvalues[l] = lambda[l];
}
for (idx, &col) in deflated.iter().enumerate() {
let slot = k + idx;
eigenvalues[slot] = d[col];
for r in 0..n {
vmat[(r, slot)] = qmat[(r, col)];
}
}
sort_eigenvalues(&mut eigenvalues, &mut vmat, n);
Ok((eigenvalues, vmat))
}
fn solve_secular_i<T: Field + Real>(
d: &[T],
zeta: &[T],
i: usize,
k: usize,
) -> Result<(T, Vec<T>), SymmetricEvdDcError> {
let eps = <T as Scalar>::epsilon();
let two = T::one() + T::one();
let eight = T::from_f64(8.0).unwrap_or(T::one());
let mut sum_zeta = T::zero();
for &zj in zeta.iter() {
sum_zeta = sum_zeta + zj;
}
let (orig, mut elo, mut ehi) = if i + 1 < k {
let gap = d[i + 1] - d[i];
let mid = d[i] + gap / two;
let mut fmid = T::one();
for (&dj, &zj) in d.iter().zip(zeta.iter()) {
fmid = fmid + zj / (dj - mid);
}
if fmid >= T::zero() {
(i, T::zero(), gap)
} else {
(i + 1, -gap, T::zero())
}
} else {
(k - 1, T::zero(), sum_zeta)
};
let d_orig = d[orig];
let mut eta = (elo + ehi) / two;
let mut delta = vec![T::zero(); k];
for _iter in 0..MAX_SECULAR_ITER {
for (deltaj, &dj) in delta.iter_mut().zip(d.iter()) {
*deltaj = (dj - d_orig) - eta;
}
let mut psi = T::zero();
let mut dpsi = T::zero();
let mut phi = T::zero();
let mut dphi = T::zero();
let mut err = T::one();
for (&zj, &dj) in zeta.iter().zip(delta.iter()).take(i + 1) {
let t = zj / dj;
psi = psi + t;
dpsi = dpsi + t / dj;
err = err + Scalar::abs(t);
}
for (&zj, &dj) in zeta.iter().zip(delta.iter()).skip(i + 1) {
let t = zj / dj;
phi = phi + t;
dphi = dphi + t / dj;
err = err + Scalar::abs(t);
}
let f = T::one() + psi + phi;
if f >= T::zero() {
ehi = eta;
} else {
elo = eta;
}
if !(Scalar::abs(f) > eight * eps * err) {
break;
}
let step_opt = if i + 1 < k {
let dl = delta[i];
let dr = delta[i + 1];
let b1 = dpsi * dl * dl;
let a1 = psi - dpsi * dl;
let b2 = dphi * dr * dr;
let a2 = phi - dphi * dr;
let c0 = T::one() + a1 + a2;
let bq = c0 * (dl + dr) + b1 + b2;
let cq = c0 * dl * dr + b1 * dr + b2 * dl;
solve_quadratic_in_range(c0, bq, cq, dl, dr)
} else {
let dl = delta[k - 1];
let b1 = dpsi * dl * dl;
let a1 = psi - dpsi * dl;
let c0 = T::one() + a1 + phi;
if c0 != T::zero() {
let s = dl + b1 / c0;
if s > dl { Some(s) } else { None }
} else {
None
}
};
let eta_next = match step_opt {
Some(s) => {
let cand = eta + s;
if cand > elo && cand < ehi {
cand
} else {
(elo + ehi) / two
}
}
None => (elo + ehi) / two,
};
if eta_next == eta {
let bis = (elo + ehi) / two;
if bis == eta {
break;
}
eta = bis;
} else {
eta = eta_next;
}
}
for (deltaj, &dj) in delta.iter_mut().zip(d.iter()) {
*deltaj = (dj - d_orig) - eta;
}
let lambda = d_orig + eta;
if !lambda.is_finite() {
return Err(SymmetricEvdDcError::SecularEquationFailed);
}
Ok((lambda, delta))
}
fn solve_quadratic_in_range<T: Field + Real>(a: T, b: T, c: T, lo: T, hi: T) -> Option<T> {
let two = T::one() + T::one();
let four = two * two;
if a == T::zero() {
if b == T::zero() {
return None;
}
let s = c / b;
if s > lo && s < hi {
return Some(s);
}
return None;
}
let disc = b * b - four * a * c;
if disc < T::zero() {
return None;
}
let sq = Real::sqrt(disc);
let r1 = (b - sq) / (two * a);
let r2 = (b + sq) / (two * a);
if r1 > lo && r1 < hi {
Some(r1)
} else if r2 > lo && r2 < hi {
Some(r2)
} else {
None
}
}
fn qr_algorithm<T: Field + Real>(
mut diag: Vec<T>,
mut off_diag: Vec<T>,
v: &mut Mat<T>,
n: usize,
) -> Result<Vec<T>, SymmetricEvdDcError> {
const MAX_ITERATIONS: usize = 100;
if n <= 1 {
return Ok(diag);
}
let eps = <T as Scalar>::epsilon() * T::from_f64(100.0).unwrap_or(T::one());
let mut m = n - 1;
let mut iter = 0;
while m > 0 && iter < MAX_ITERATIONS * n {
iter += 1;
let mut l = m;
while l > 0 {
let test = Scalar::abs(diag[l - 1]) + Scalar::abs(diag[l]);
if Scalar::abs(off_diag[l - 1]) <= eps * test {
off_diag[l - 1] = T::zero();
break;
}
l -= 1;
}
if l == m {
m -= 1;
continue;
}
let d = (diag[m - 1] - diag[m]) / (T::one() + T::one());
let e = off_diag[m - 1];
let mu = diag[m] - e * e / (d + Real::signum(d) * Real::hypot(d, e));
let mut x = diag[l] - mu;
let mut z = off_diag[l];
for k in l..m {
let (c, s) = givens_rotation(x, z);
if k > l {
off_diag[k - 1] = c * x - s * z;
}
let d1 = diag[k];
let d2 = diag[k + 1];
let e = off_diag[k];
diag[k] = c * c * d1 + s * s * d2 - (c + c) * s * e;
diag[k + 1] = s * s * d1 + c * c * d2 + (c + c) * s * e;
off_diag[k] = c * s * (d1 - d2) + (c * c - s * s) * e;
if k < m - 1 {
x = off_diag[k];
z = -s * off_diag[k + 1];
off_diag[k + 1] = c * off_diag[k + 1];
}
for i in 0..n {
let t1 = v[(i, k)];
let t2 = v[(i, k + 1)];
v[(i, k)] = c * t1 - s * t2;
v[(i, k + 1)] = s * t1 + c * t2;
}
}
}
if iter >= MAX_ITERATIONS * n {
return Err(SymmetricEvdDcError::NotConverged);
}
sort_eigenvalues(&mut diag, v, n);
Ok(diag)
}
fn givens_rotation<T: Field + Real>(a: T, b: T) -> (T, T) {
if b == T::zero() {
(T::one(), T::zero())
} else if Scalar::abs(b) > Scalar::abs(a) {
let t = -a / b;
let s = T::one() / Real::sqrt(T::one() + t * t);
(s * t, s)
} else {
let t = -b / a;
let c = T::one() / Real::sqrt(T::one() + t * t);
(c, c * t)
}
}
fn sort_eigenvalues<T: Field + Real>(eigenvalues: &mut [T], v: &mut Mat<T>, n: usize) {
for i in 1..n {
let key = eigenvalues[i];
let mut j = i;
while j > 0 && eigenvalues[j - 1] > key {
eigenvalues[j] = eigenvalues[j - 1];
for row in 0..n {
let tmp = v[(row, j)];
v[(row, j)] = v[(row, j - 1)];
v[(row, j - 1)] = tmp;
}
j -= 1;
}
eigenvalues[j] = key;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() < tol
}
struct Prng {
state: u64,
}
impl Prng {
fn new(seed: u64) -> Self {
Self { state: seed }
}
fn next_f64(&mut self) -> f64 {
self.state = self.state.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = self.state;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^= z >> 31;
(z as f64 / u64::MAX as f64) * 2.0 - 1.0
}
}
fn random_symmetric(n: usize, seed: u64) -> Mat<f64> {
let mut prng = Prng::new(seed);
let mut a: Mat<f64> = Mat::zeros(n, n);
for i in 0..n {
for j in i..n {
let v = prng.next_f64();
a[(i, j)] = v;
a[(j, i)] = v;
}
}
a
}
fn symmetric_with_spectrum(eigs: &[f64], seed: u64) -> Mat<f64> {
let n = eigs.len();
let mut prng = Prng::new(seed);
let mut w = vec![0.0f64; n];
let mut norm_sq = 0.0;
for wi in w.iter_mut() {
*wi = prng.next_f64();
norm_sq += *wi * *wi;
}
let norm = norm_sq.sqrt();
if norm > 0.0 {
for wi in w.iter_mut() {
*wi /= norm;
}
}
let mut h: Mat<f64> = Mat::zeros(n, n);
for i in 0..n {
for j in 0..n {
let delta = if i == j { 1.0 } else { 0.0 };
h[(i, j)] = delta - 2.0 * w[i] * w[j];
}
}
let mut a: Mat<f64> = Mat::zeros(n, n);
for i in 0..n {
for j in 0..n {
let mut acc = 0.0;
for k in 0..n {
acc += h[(i, k)] * eigs[k] * h[(j, k)];
}
a[(i, j)] = acc;
}
}
a
}
fn orthogonality_error(v: MatRef<'_, f64>, n: usize) -> f64 {
let mut err = 0.0f64;
for i in 0..n {
for j in 0..n {
let mut dot = 0.0;
for k in 0..n {
dot += v[(k, i)] * v[(k, j)];
}
let expected = if i == j { 1.0 } else { 0.0 };
err = err.max((dot - expected).abs());
}
}
err
}
fn residual_error(a: &Mat<f64>, evd: &SymmetricEvdDc<f64>, n: usize) -> f64 {
let v = evd.eigenvectors();
let eigs = evd.eigenvalues();
let mut err = 0.0f64;
for col in 0..n {
let lambda = eigs[col];
for row in 0..n {
let mut av = 0.0;
for k in 0..n {
av += a[(row, k)] * v[(k, col)];
}
err = err.max((av - lambda * v[(row, col)]).abs());
}
}
err
}
#[test]
fn test_evd_dc_2x2() {
let a = Mat::from_rows(&[&[2.0f64, 1.0], &[1.0, 2.0]]);
let evd = SymmetricEvdDc::compute(a.as_ref()).unwrap();
let eigs = evd.eigenvalues();
assert!(approx_eq(eigs[0], 1.0, 1e-10));
assert!(approx_eq(eigs[1], 3.0, 1e-10));
}
#[test]
fn test_evd_dc_3x3() {
let a = Mat::from_rows(&[&[4.0f64, 1.0, 1.0], &[1.0, 3.0, 2.0], &[1.0, 2.0, 3.0]]);
let evd = SymmetricEvdDc::compute(a.as_ref()).unwrap();
let reconstructed = evd.reconstruct();
for i in 0..3 {
for j in 0..3 {
assert!(
approx_eq(reconstructed[(i, j)], a[(i, j)], 1e-8),
"reconstructed[{},{}] = {}, a = {}",
i,
j,
reconstructed[(i, j)],
a[(i, j)]
);
}
}
}
#[test]
fn test_evd_dc_diagonal() {
let a = Mat::from_rows(&[&[3.0f64, 0.0, 0.0], &[0.0, 1.0, 0.0], &[0.0, 0.0, 2.0]]);
let evd = SymmetricEvdDc::compute(a.as_ref()).unwrap();
let eigs = evd.eigenvalues();
assert!(approx_eq(eigs[0], 1.0, 1e-10));
assert!(approx_eq(eigs[1], 2.0, 1e-10));
assert!(approx_eq(eigs[2], 3.0, 1e-10));
}
#[test]
fn test_evd_dc_identity() {
let eye = Mat::from_rows(&[&[1.0f64, 0.0, 0.0], &[0.0, 1.0, 0.0], &[0.0, 0.0, 1.0]]);
let evd = SymmetricEvdDc::compute(eye.as_ref()).unwrap();
let eigs = evd.eigenvalues();
for &e in eigs {
assert!(approx_eq(e, 1.0, 1e-10));
}
}
#[test]
fn test_evd_dc_single() {
let a = Mat::from_rows(&[&[5.0f64]]);
let evd = SymmetricEvdDc::compute(a.as_ref()).unwrap();
let eigs = evd.eigenvalues();
assert_eq!(eigs.len(), 1);
assert!(approx_eq(eigs[0], 5.0, 1e-10));
}
#[test]
fn test_evd_dc_negative_eigenvalues() {
let a = Mat::from_rows(&[&[-2.0f64, 1.0], &[1.0, -2.0]]);
let evd = SymmetricEvdDc::compute(a.as_ref()).unwrap();
let eigs = evd.eigenvalues();
assert!(approx_eq(eigs[0], -3.0, 1e-10));
assert!(approx_eq(eigs[1], -1.0, 1e-10));
}
#[test]
fn test_evd_dc_f32() {
let a = Mat::from_rows(&[&[2.0f32, 1.0], &[1.0, 2.0]]);
let evd = SymmetricEvdDc::compute(a.as_ref()).unwrap();
let eigs = evd.eigenvalues();
assert!((eigs[0] - 1.0).abs() < 1e-5);
assert!((eigs[1] - 3.0).abs() < 1e-5);
}
#[test]
fn test_evd_dc_repeated_eigenvalues() {
let a = Mat::from_rows(&[&[3.0f64, 0.0, 0.0], &[0.0, 3.0, 0.0], &[0.0, 0.0, 1.0]]);
let evd = SymmetricEvdDc::compute(a.as_ref()).unwrap();
let eigs = evd.eigenvalues();
assert!(approx_eq(eigs[0], 1.0, 1e-10));
assert!(approx_eq(eigs[1], 3.0, 1e-10));
assert!(approx_eq(eigs[2], 3.0, 1e-10));
}
#[test]
fn test_evd_dc_orthogonality() {
let a = Mat::from_rows(&[&[4.0f64, 2.0, 1.0], &[2.0, 5.0, 3.0], &[1.0, 3.0, 6.0]]);
let evd = SymmetricEvdDc::compute(a.as_ref()).unwrap();
let v = evd.eigenvectors();
for i in 0..3 {
for j in 0..3 {
let mut sum = 0.0;
for k in 0..3 {
sum += v[(k, i)] * v[(k, j)];
}
let expected = if i == j { 1.0 } else { 0.0 };
assert!(
approx_eq(sum, expected, 1e-8),
"V^T*V[{},{}] = {}, expected {}",
i,
j,
sum,
expected
);
}
}
}
#[test]
fn test_evd_dc_larger_matrix() {
let n = 10;
let mut a: Mat<f64> = Mat::zeros(n, n);
for i in 0..n {
a[(i, i)] = (i + 1) as f64;
}
let evd = SymmetricEvdDc::compute(a.as_ref()).unwrap();
let eigs = evd.eigenvalues();
for i in 0..n {
assert!(
approx_eq(eigs[i], (i + 1) as f64, 1e-10),
"eigenvalue {} = {}, expected {}",
i,
eigs[i],
i + 1
);
}
let reconstructed = evd.reconstruct();
for i in 0..n {
for j in 0..n {
assert!(
approx_eq(reconstructed[(i, j)], a[(i, j)], 1e-8),
"mismatch at ({},{}): {} vs {}",
i,
j,
reconstructed[(i, j)],
a[(i, j)]
);
}
}
let v = evd.eigenvectors();
for i in 0..n {
for j in 0..n {
let mut sum = 0.0;
for k in 0..n {
sum += v[(k, i)] * v[(k, j)];
}
let expected = if i == j { 1.0 } else { 0.0 };
assert!(
approx_eq(sum, expected, 1e-8),
"V^T*V[{},{}] = {}, expected {}",
i,
j,
sum,
expected
);
}
}
}
#[test]
fn test_evd_dc_vs_qr() {
use super::super::symmetric::SymmetricEvd;
let a = Mat::from_rows(&[
&[4.0f64, 2.0, 1.0, 0.5],
&[2.0, 5.0, 3.0, 1.0],
&[1.0, 3.0, 6.0, 2.0],
&[0.5, 1.0, 2.0, 4.0],
]);
let evd_qr = SymmetricEvd::compute(a.as_ref()).unwrap();
let evd_dc = SymmetricEvdDc::compute(a.as_ref()).unwrap();
let eigs_qr = evd_qr.eigenvalues();
let eigs_dc = evd_dc.eigenvalues();
for i in 0..4 {
assert!(
approx_eq(eigs_qr[i], eigs_dc[i], 1e-8),
"eigenvalue {} mismatch: QR={}, DC={}",
i,
eigs_qr[i],
eigs_dc[i]
);
}
}
#[test]
fn test_evd_dc_reference_across_sizes() {
use super::super::symmetric::SymmetricEvd;
for (idx, &n) in [3usize, 5, 10, 50, 150, 300].iter().enumerate() {
let a = random_symmetric(n, 0x1234_5678 + idx as u64 * 97);
let dc = SymmetricEvdDc::compute(a.as_ref()).unwrap();
let qr = SymmetricEvd::compute(a.as_ref()).unwrap();
let dc_eigs = dc.eigenvalues();
let qr_eigs = qr.eigenvalues();
let scale = qr_eigs.iter().fold(1.0f64, |acc, &e| acc.max(e.abs()));
let eig_tol = 1e-7 * scale * (n as f64).sqrt();
for i in 0..n {
assert!(
(dc_eigs[i] - qr_eigs[i]).abs() <= eig_tol,
"n={}: eigenvalue {} mismatch DC={} QR={} (tol {})",
n,
i,
dc_eigs[i],
qr_eigs[i],
eig_tol
);
}
let ortho = orthogonality_error(dc.eigenvectors(), n);
assert!(
ortho <= 1e-9 * (n as f64),
"n={}: orthogonality error {} too large",
n,
ortho
);
let resid = residual_error(&a, &dc, n);
assert!(
resid <= 1e-7 * scale * (n as f64),
"n={}: residual {} too large",
n,
resid
);
}
}
#[test]
fn test_tridiag_dc_forced_merge_small() {
for &n in &[2usize, 3, 5, 10] {
let mut prng = Prng::new(0xABCD_00FF + n as u64);
let diag: Vec<f64> = (0..n).map(|_| prng.next_f64() * 4.0).collect();
let off: Vec<f64> = (0..n - 1).map(|_| prng.next_f64() * 2.0 + 0.5).collect();
let mut z_ref = Mat::eye(n);
let ref_eigs = qr_algorithm(diag.clone(), off.clone(), &mut z_ref, n).unwrap();
let (dc_eigs, dc_vec) = tridiag_dc(diag.clone(), off.clone(), 1).unwrap();
for i in 0..n {
assert!(
(dc_eigs[i] - ref_eigs[i]).abs() <= 1e-9 * (1.0 + ref_eigs[i].abs()),
"n={}: forced-merge eigenvalue {} mismatch DC={} REF={}",
n,
i,
dc_eigs[i],
ref_eigs[i]
);
}
let ortho = orthogonality_error(dc_vec.as_ref(), n);
assert!(
ortho <= 1e-10 * (n as f64),
"n={}: forced-merge orthogonality error {} too large",
n,
ortho
);
}
}
#[test]
fn test_evd_dc_deflation_repeated_spectrum() {
let mut eigs = vec![
1.0, 1.0, 1.0, 1.0, 2.5, 2.5, 2.5, -3.0, -3.0, -3.0, -3.0, -3.0, 7.0, 7.0, 0.0, 0.0,
0.0, 5.5, 5.5, 9.0,
];
while eigs.len() < 40 {
let v = eigs.len() as f64 * 0.31 - 6.0;
eigs.push(v);
eigs.push(v); }
eigs.truncate(40);
let n = eigs.len();
let a = symmetric_with_spectrum(&eigs, 0x5151_2727);
let dc = SymmetricEvdDc::compute(a.as_ref()).unwrap();
let dc_eigs = dc.eigenvalues();
let mut sorted = eigs.clone();
sorted.sort_by(|x, y| x.partial_cmp(y).unwrap());
let scale = sorted.iter().fold(1.0f64, |acc, &e| acc.max(e.abs()));
for i in 0..n {
assert!(
(dc_eigs[i] - sorted[i]).abs() <= 1e-7 * scale * (n as f64).sqrt(),
"eigenvalue {} mismatch DC={} expected={}",
i,
dc_eigs[i],
sorted[i]
);
}
let ortho = orthogonality_error(dc.eigenvectors(), n);
assert!(
ortho <= 1e-9 * (n as f64),
"deflation orthogonality error {} too large",
ortho
);
let resid = residual_error(&a, &dc, n);
assert!(
resid <= 1e-7 * scale * (n as f64),
"deflation residual {} too large",
resid
);
}
#[test]
fn test_evd_dc_f32_larger() {
let n = 60usize;
let mut prng = Prng::new(0x2468_ACE0);
let mut a: Mat<f32> = Mat::zeros(n, n);
for i in 0..n {
for j in i..n {
let v = (prng.next_f64() as f32) * 2.0;
a[(i, j)] = v;
a[(j, i)] = v;
}
}
let dc = SymmetricEvdDc::compute(a.as_ref()).unwrap();
let eigs = dc.eigenvalues();
for i in 0..n - 1 {
assert!(eigs[i] <= eigs[i + 1] + 1e-3);
}
let v = dc.eigenvectors();
let mut ortho = 0.0f32;
for i in 0..n {
for j in 0..n {
let mut dot = 0.0f32;
for k in 0..n {
dot += v[(k, i)] * v[(k, j)];
}
let expected = if i == j { 1.0 } else { 0.0 };
ortho = ortho.max((dot - expected).abs());
}
}
assert!(
ortho <= 1e-3 * n as f32,
"f32 orthogonality {} too large",
ortho
);
}
}