use oxiblas_core::scalar::{Field, Real, Scalar};
use oxiblas_matrix::Mat;
const MAX_BISECTION_ITER: usize = 100;
const MAX_RQI_ITER: usize = 8;
const MAX_TREE_DEPTH: usize = 64;
const MAX_SHIFT_ATTEMPTS: usize = 16;
const MIN_RELATIVE_GAP: f64 = 1.0e-3;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MrrrError {
EmptyInput,
DimensionMismatch,
LdlFactorizationFailed,
EigenvalueNotConverged,
EigenvectorComputationFailed,
InvalidIndexRange,
}
impl core::fmt::Display for MrrrError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::EmptyInput => write!(f, "Empty input"),
Self::DimensionMismatch => write!(f, "Off-diagonal must have length n-1"),
Self::LdlFactorizationFailed => write!(f, "LDL factorization failed"),
Self::EigenvalueNotConverged => write!(f, "Eigenvalue computation did not converge"),
Self::EigenvectorComputationFailed => write!(f, "Eigenvector computation failed"),
Self::InvalidIndexRange => write!(f, "Invalid index range"),
}
}
}
impl std::error::Error for MrrrError {}
#[inline]
fn copysign_mag<T: Real>(mag: T, sign_of: T) -> T {
if sign_of >= T::zero() { mag } else { -mag }
}
#[derive(Debug, Clone, Copy)]
struct MrrrParams<T> {
spdiam: T,
pivmin: T,
minrgp: T,
rqtol: T,
degtol: T,
eps: T,
}
#[derive(Debug, Clone)]
struct Rrr<T> {
d: Vec<T>,
l: Vec<T>,
ld: Vec<T>,
lld: Vec<T>,
}
impl<T: Field + Real> Rrr<T> {
fn from_tridiagonal(diagonal: &[T], off_diagonal: &[T], sigma: T, pivmin: T) -> Self {
let n = diagonal.len();
let mut d = vec![T::zero(); n];
let mut l = vec![T::zero(); n.saturating_sub(1)];
let mut ld = vec![T::zero(); n.saturating_sub(1)];
let mut lld = vec![T::zero(); n.saturating_sub(1)];
let mut d0 = diagonal[0] - sigma;
if Scalar::abs(d0) < pivmin {
d0 = copysign_mag(pivmin, d0);
}
d[0] = d0;
for i in 0..n.saturating_sub(1) {
let li = off_diagonal[i] / d[i];
let ldi = li * d[i];
let lldi = li * ldi;
l[i] = li;
ld[i] = ldi;
lld[i] = lldi;
let mut dn = (diagonal[i + 1] - sigma) - lldi;
if Scalar::abs(dn) < pivmin {
dn = copysign_mag(pivmin, dn);
}
d[i + 1] = dn;
}
Rrr { d, l, ld, lld }
}
fn factor_child(&self, tau: T, pivmin: T) -> Self {
let n = self.d.len();
let mut d = vec![T::zero(); n];
let mut l = vec![T::zero(); n.saturating_sub(1)];
let mut ld = vec![T::zero(); n.saturating_sub(1)];
let mut lld = vec![T::zero(); n.saturating_sub(1)];
let mut s = -tau;
for i in 0..n.saturating_sub(1) {
let mut dplus = self.d[i] + s;
if Scalar::abs(dplus) < pivmin {
dplus = copysign_mag(pivmin, dplus);
}
let lplus = self.ld[i] / dplus;
d[i] = dplus;
l[i] = lplus;
ld[i] = lplus * dplus;
lld[i] = lplus * ld[i];
s = self.lld[i] * (s / dplus) - tau;
if !s.is_finite() {
s = -tau;
}
}
let mut dlast = self.d[n - 1] + s;
if Scalar::abs(dlast) < pivmin {
dlast = copysign_mag(pivmin, dlast);
}
d[n - 1] = dlast;
Rrr { d, l, ld, lld }
}
fn neg_count(&self, mu: T, pivmin: T) -> usize {
let n = self.d.len();
let mut neg = 0usize;
let mut s = -mu;
for i in 0..n - 1 {
let mut dplus = self.d[i] + s;
if Scalar::abs(dplus) < pivmin {
dplus = -pivmin;
}
if dplus < T::zero() {
neg += 1;
}
s = self.lld[i] * (s / dplus) - mu;
if !s.is_finite() {
s = -mu;
}
}
let mut dlast = self.d[n - 1] + s;
if Scalar::abs(dlast) < pivmin {
dlast = -pivmin;
}
if dlast < T::zero() {
neg += 1;
}
neg
}
fn twisted_eigenvector(&self, mu: T, pivmin: T) -> (Vec<T>, T, T) {
let n = self.d.len();
let mut s = vec![T::zero(); n];
let mut lplus = vec![T::zero(); n.saturating_sub(1)];
let mut dplus = vec![T::zero(); n];
s[0] = -mu;
for i in 0..n {
let mut dp = self.d[i] + s[i];
if Scalar::abs(dp) < pivmin {
dp = copysign_mag(pivmin, dp);
}
dplus[i] = dp;
if i < n - 1 {
lplus[i] = self.ld[i] / dp;
let mut snext = self.lld[i] * (s[i] / dp) - mu;
if !snext.is_finite() {
snext = -mu;
}
s[i + 1] = snext;
}
}
let mut p = vec![T::zero(); n];
let mut uminus = vec![T::zero(); n.saturating_sub(1)];
p[n - 1] = self.d[n - 1] - mu;
for i in (0..n - 1).rev() {
let mut dminus = self.lld[i] + p[i + 1];
if Scalar::abs(dminus) < pivmin {
dminus = copysign_mag(pivmin, dminus);
}
let tmp = self.d[i] / dminus;
uminus[i] = self.l[i] * tmp;
let mut pi = p[i + 1] * tmp - mu;
if !pi.is_finite() {
pi = self.d[i] - mu;
}
p[i] = pi;
}
let mut r = 0usize;
let mut best = Scalar::abs(s[0] + p[0] + mu);
for i in 1..n {
let g = Scalar::abs(s[i] + p[i] + mu);
if g < best {
best = g;
r = i;
}
}
let mingma = s[r] + p[r] + mu;
let mut z = vec![T::zero(); n];
z[r] = T::one();
for i in (0..r).rev() {
let mut zi = -lplus[i] * z[i + 1];
if !zi.is_finite() {
zi = T::zero();
}
z[i] = zi;
}
for i in r..n - 1 {
let mut zi = -uminus[i] * z[i];
if !zi.is_finite() {
zi = T::zero();
}
z[i + 1] = zi;
}
let ztz: T = z.iter().map(|&x| x * x).sum();
(z, ztz, mingma)
}
}
struct WorkItem<T> {
rrr: Rrr<T>,
sigma: T,
first: usize,
last: usize,
depth: usize,
}
#[derive(Debug, Clone)]
pub struct MrrrEvd<T: Scalar> {
eigenvalues: Vec<T>,
eigenvectors: Option<Mat<T>>,
n: usize,
}
impl<T: Field + Real + bytemuck::Zeroable> MrrrEvd<T> {
pub fn compute(diagonal: &[T], off_diagonal: &[T]) -> Result<Self, MrrrError> {
Self::compute_range(diagonal, off_diagonal, 0, diagonal.len().saturating_sub(1))
}
pub fn eigenvalues_only(diagonal: &[T], off_diagonal: &[T]) -> Result<Self, MrrrError> {
let n = diagonal.len();
if n == 0 {
return Err(MrrrError::EmptyInput);
}
if off_diagonal.len() != n.saturating_sub(1) {
return Err(MrrrError::DimensionMismatch);
}
if n == 1 {
return Ok(Self {
eigenvalues: vec![diagonal[0]],
eigenvectors: None,
n,
});
}
let eigenvalues = compute_all_eigenvalues(diagonal, off_diagonal)?;
Ok(Self {
eigenvalues,
eigenvectors: None,
n,
})
}
pub fn compute_range(
diagonal: &[T],
off_diagonal: &[T],
il: usize,
iu: usize,
) -> Result<Self, MrrrError> {
let n = diagonal.len();
if n == 0 {
return Err(MrrrError::EmptyInput);
}
if off_diagonal.len() != n.saturating_sub(1) {
return Err(MrrrError::DimensionMismatch);
}
if il > iu || iu >= n {
return Err(MrrrError::InvalidIndexRange);
}
if n == 1 {
let mut eigenvectors = Mat::zeros(1, 1);
eigenvectors[(0, 0)] = T::one();
return Ok(Self {
eigenvalues: vec![diagonal[0]],
eigenvectors: Some(eigenvectors),
n,
});
}
let (all_eigenvalues, all_eigenvectors) = mrrr_eigenvectors(diagonal, off_diagonal)?;
let eigenvalues: Vec<T> = all_eigenvalues[il..=iu].to_vec();
let num_eigs = eigenvalues.len();
let mut eigenvectors = Mat::zeros(n, num_eigs);
for (col, src) in (il..=iu).enumerate() {
for row in 0..n {
eigenvectors[(row, col)] = all_eigenvectors[(row, src)];
}
}
Ok(Self {
eigenvalues,
eigenvectors: Some(eigenvectors),
n,
})
}
pub fn eigenvalues(&self) -> &[T] {
&self.eigenvalues
}
pub fn eigenvectors(&self) -> Option<&Mat<T>> {
self.eigenvectors.as_ref()
}
pub fn dim(&self) -> usize {
self.n
}
pub fn num_eigenvalues(&self) -> usize {
self.eigenvalues.len()
}
}
fn mrrr_eigenvectors<T: Field + Real + bytemuck::Zeroable>(
diagonal: &[T],
off_diagonal: &[T],
) -> Result<(Vec<T>, Mat<T>), MrrrError> {
let n = diagonal.len();
if n == 0 {
return Err(MrrrError::EmptyInput);
}
if n == 1 {
let mut vecs = Mat::zeros(1, 1);
vecs[(0, 0)] = T::one();
return Ok((vec![diagonal[0]], vecs));
}
let (glow, ghigh) = gershgorin_bounds(diagonal, off_diagonal);
let spdiam = ghigh - glow;
let eps = <T as Scalar>::epsilon();
let split_tol = eps * spdiam + <T as Scalar>::min_positive();
let mut blocks: Vec<(usize, usize)> = Vec::new();
let mut start = 0usize;
for i in 0..n - 1 {
if Scalar::abs(off_diagonal[i]) <= split_tol {
blocks.push((start, i));
start = i + 1;
}
}
blocks.push((start, n - 1));
let mut pairs: Vec<(T, Vec<T>)> = Vec::with_capacity(n);
for (bs, be) in blocks {
let nb = be - bs + 1;
let dblock = &diagonal[bs..=be];
let oblock: &[T] = if nb > 1 { &off_diagonal[bs..be] } else { &[] };
let (block_eigs, block_vecs) = block_mrrr(dblock, oblock)?;
for (col, &lam) in block_eigs.iter().enumerate() {
let mut full = vec![T::zero(); n];
for row in 0..nb {
full[bs + row] = block_vecs[(row, col)];
}
pairs.push((lam, full));
}
}
pairs.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(core::cmp::Ordering::Equal));
let mut eigenvalues = Vec::with_capacity(n);
let mut eigenvectors = Mat::zeros(n, n);
for (col, (lam, full)) in pairs.into_iter().enumerate() {
eigenvalues.push(lam);
for row in 0..n {
eigenvectors[(row, col)] = full[row];
}
}
Ok((eigenvalues, eigenvectors))
}
fn block_mrrr<T: Field + Real + bytemuck::Zeroable>(
diagonal: &[T],
off_diagonal: &[T],
) -> Result<(Vec<T>, Mat<T>), MrrrError> {
let n = diagonal.len();
if n == 0 {
return Ok((Vec::new(), Mat::zeros(0, 0)));
}
if n == 1 {
let mut vecs = Mat::zeros(1, 1);
vecs[(0, 0)] = T::one();
return Ok((vec![diagonal[0]], vecs));
}
let eps = <T as Scalar>::epsilon();
let two = T::one() + T::one();
let (glow, ghigh) = gershgorin_bounds(diagonal, off_diagonal);
let spdiam = ghigh - glow;
let pivmin = spdiam * eps * eps + <T as Scalar>::min_positive();
let params = MrrrParams {
spdiam,
pivmin,
minrgp: T::from_f64(MIN_RELATIVE_GAP).unwrap_or_else(T::zero),
rqtol: two * eps,
degtol: spdiam * eps * T::from_f64(n as f64).unwrap_or_else(T::one)
+ <T as Scalar>::min_positive(),
eps,
};
let lambda = compute_all_eigenvalues(diagonal, off_diagonal)?;
let (base, sigma0) = build_base_rrr(diagonal, off_diagonal, glow, spdiam, pivmin);
let mut w: Vec<T> = lambda.iter().map(|&l| l - sigma0).collect();
let mut werr: Vec<T> = vec![eps * spdiam + <T as Scalar>::min_positive(); n];
let mut eig_abs = lambda;
let mut vecs = Mat::zeros(n, n);
let mut stack: Vec<WorkItem<T>> = Vec::new();
stack.push(WorkItem {
rrr: base,
sigma: sigma0,
first: 0,
last: n - 1,
depth: 0,
});
while let Some(item) = stack.pop() {
let WorkItem {
rrr,
sigma,
first,
last,
depth,
} = item;
for i in first..=last {
let (mu, err) = refine_relative(&rrr, i, w[i], werr[i], ¶ms);
w[i] = mu;
werr[i] = err;
}
let mut j = first;
while j <= last {
let mut k = j;
while k < last {
let gap = w[k + 1] - w[k];
let scale =
Scalar::abs(w[k]).max(Scalar::abs(w[k + 1])) + params.spdiam * params.eps;
if gap < params.minrgp * scale {
k += 1;
} else {
break;
}
}
if k == j {
let (z, mu) = compute_singleton_eigenvector(&rrr, w[j], werr[j], ¶ms);
w[j] = mu;
eig_abs[j] = sigma + mu;
for row in 0..n {
vecs[(row, j)] = z[row];
}
} else {
let spread = w[k] - w[j];
let child = if depth >= MAX_TREE_DEPTH || spread <= params.degtol {
None
} else {
find_child_rrr(&rrr, &w, &werr, j, k, ¶ms)
};
match child {
Some((child_rrr, tau)) => {
for wm in &mut w[j..=k] {
*wm = *wm - tau;
}
stack.push(WorkItem {
rrr: child_rrr,
sigma: sigma + tau,
first: j,
last: k,
depth: depth + 1,
});
}
None => {
for m in j..=k {
let (z, mu) =
compute_singleton_eigenvector(&rrr, w[m], werr[m], ¶ms);
w[m] = mu;
eig_abs[m] = sigma + mu;
for row in 0..n {
vecs[(row, m)] = z[row];
}
}
local_orthonormalize(&mut vecs, j, k, n);
}
}
}
j = k + 1;
}
}
Ok((eig_abs, vecs))
}
fn build_base_rrr<T: Field + Real>(
diagonal: &[T],
off_diagonal: &[T],
glow: T,
spdiam: T,
pivmin: T,
) -> (Rrr<T>, T) {
let mut sigma0 = glow;
for attempt in 0..40usize {
let rrr = Rrr::from_tridiagonal(diagonal, off_diagonal, sigma0, pivmin);
if rrr.d.iter().all(|&x| x > T::zero()) {
return (rrr, sigma0);
}
let step = spdiam * T::from_f64(0.05 * (attempt as f64 + 1.0)).unwrap_or_else(T::one)
+ <T as Scalar>::min_positive();
sigma0 = sigma0 - step;
}
let rrr = Rrr::from_tridiagonal(diagonal, off_diagonal, sigma0, pivmin);
(rrr, sigma0)
}
fn refine_relative<T: Field + Real>(
rrr: &Rrr<T>,
i: usize,
mu0: T,
werr0: T,
params: &MrrrParams<T>,
) -> (T, T) {
let two = T::one() + T::one();
let floor = params.spdiam * params.eps + <T as Scalar>::min_positive();
let mut lo = mu0 - werr0 - floor;
let mut hi = mu0 + werr0 + floor;
let mut ex = werr0 + floor;
let mut tries = 0usize;
while rrr.neg_count(lo, params.pivmin) > i && tries < 80 {
lo = lo - ex;
ex = ex * two;
tries += 1;
}
let mut ex = werr0 + floor;
tries = 0;
while rrr.neg_count(hi, params.pivmin) <= i && tries < 80 {
hi = hi + ex;
ex = ex * two;
tries += 1;
}
for _ in 0..MAX_BISECTION_ITER {
let mid = (lo + hi) / two;
let width = hi - lo;
if width
<= two * params.eps * (Scalar::abs(mid) + params.spdiam) + <T as Scalar>::min_positive()
{
break;
}
if rrr.neg_count(mid, params.pivmin) <= i {
lo = mid;
} else {
hi = mid;
}
}
let mu = (lo + hi) / two;
let err = ((hi - lo) / two).max(params.spdiam * params.eps);
(mu, err)
}
fn compute_singleton_eigenvector<T: Field + Real>(
rrr: &Rrr<T>,
mu0: T,
werr: T,
params: &MrrrParams<T>,
) -> (Vec<T>, T) {
let floor = params.spdiam * params.eps + <T as Scalar>::min_positive();
let lo = mu0 - werr - floor;
let hi = mu0 + werr + floor;
let mut mu = mu0;
for _ in 0..MAX_RQI_ITER {
let (_z, ztz, mingma) = rrr.twisted_eigenvector(mu, params.pivmin);
if ztz <= T::zero() {
break;
}
let rqcorr = mingma / ztz;
if !rqcorr.is_finite() {
break;
}
if Scalar::abs(rqcorr)
<= params.rqtol * (Scalar::abs(mu) + params.spdiam) + <T as Scalar>::min_positive()
{
break;
}
let mu_new = mu + rqcorr;
if mu_new > lo && mu_new < hi {
mu = mu_new;
} else {
break;
}
}
let (mut z, _ztz, _mingma) = rrr.twisted_eigenvector(mu, params.pivmin);
let norm = vector_norm(&z);
if norm > <T as Scalar>::min_positive() {
for x in &mut z {
*x = *x / norm;
}
} else {
for x in &mut z {
*x = T::zero();
}
if !z.is_empty() {
z[0] = T::one();
}
}
(z, mu)
}
fn find_child_rrr<T: Field + Real>(
rrr: &Rrr<T>,
w: &[T],
werr: &[T],
first: usize,
last: usize,
params: &MrrrParams<T>,
) -> Option<(Rrr<T>, T)> {
let spread = w[last] - w[first];
let mag = Scalar::abs(w[first]).max(Scalar::abs(w[last]));
let mut off = spread + werr[first].max(werr[last]) + params.eps * mag + params.pivmin;
for _ in 0..MAX_SHIFT_ATTEMPTS {
let tau_left = w[first] - off;
if let Some(child) = try_factor_child(rrr, tau_left, params) {
return Some((child, tau_left));
}
let tau_right = w[last] + off;
if let Some(child) = try_factor_child(rrr, tau_right, params) {
return Some((child, tau_right));
}
off = off / (T::one() + T::one());
if off <= params.pivmin {
break;
}
}
None
}
fn try_factor_child<T: Field + Real>(
rrr: &Rrr<T>,
tau: T,
params: &MrrrParams<T>,
) -> Option<Rrr<T>> {
let child = rrr.factor_child(tau, params.pivmin);
let mut maxd = T::zero();
for &d in &child.d {
let ad = Scalar::abs(d);
if !d.is_finite() || ad <= params.pivmin {
return None;
}
maxd = maxd.max(ad);
}
let growth_limit = params.spdiam / params.eps;
if maxd < growth_limit {
Some(child)
} else {
None
}
}
fn local_orthonormalize<T: Field + Real>(vecs: &mut Mat<T>, first: usize, last: usize, n: usize) {
for col in first..=last {
for prev in first..col {
let mut dot = T::zero();
for row in 0..n {
dot = dot + vecs[(row, col)] * vecs[(row, prev)];
}
for row in 0..n {
vecs[(row, col)] = vecs[(row, col)] - dot * vecs[(row, prev)];
}
}
let mut norm_sq = T::zero();
for row in 0..n {
norm_sq = norm_sq + vecs[(row, col)] * vecs[(row, col)];
}
let norm = Real::sqrt(norm_sq);
if norm > <T as Scalar>::min_positive() {
for row in 0..n {
vecs[(row, col)] = vecs[(row, col)] / norm;
}
}
}
}
fn compute_all_eigenvalues<T: Field + Real>(
diagonal: &[T],
off_diagonal: &[T],
) -> Result<Vec<T>, MrrrError> {
let n = diagonal.len();
if n == 0 {
return Ok(Vec::new());
}
let (glow, ghigh) = gershgorin_bounds(diagonal, off_diagonal);
let eps = <T as Scalar>::epsilon();
let two = T::one() + T::one();
let mut eigenvalues = Vec::with_capacity(n);
for target_index in 0..n {
let mut lo = glow;
let mut hi = ghigh;
for _iter in 0..MAX_BISECTION_ITER {
let tol = eps * (Scalar::abs(lo) + Scalar::abs(hi) + T::one());
if hi - lo <= tol {
break;
}
let mid = (lo + hi) / two;
let count = sturm_count(diagonal, off_diagonal, mid);
if count <= target_index {
lo = mid;
} else {
hi = mid;
}
}
eigenvalues.push((lo + hi) / two);
}
Ok(eigenvalues)
}
fn gershgorin_bounds<T: Field + Real>(diagonal: &[T], off_diagonal: &[T]) -> (T, T) {
let n = diagonal.len();
if n == 0 {
return (T::zero(), T::zero());
}
if n == 1 {
return (diagonal[0], diagonal[0]);
}
let mut min = diagonal[0] - Scalar::abs(off_diagonal[0]);
let mut max = diagonal[0] + Scalar::abs(off_diagonal[0]);
for i in 1..(n - 1) {
let radius = Scalar::abs(off_diagonal[i - 1]) + Scalar::abs(off_diagonal[i]);
let low = diagonal[i] - radius;
let high = diagonal[i] + radius;
if low < min {
min = low;
}
if high > max {
max = high;
}
}
let last_low = diagonal[n - 1] - Scalar::abs(off_diagonal[n - 2]);
let last_high = diagonal[n - 1] + Scalar::abs(off_diagonal[n - 2]);
if last_low < min {
min = last_low;
}
if last_high > max {
max = last_high;
}
let margin =
(max - min) * T::from_f64(0.01).unwrap_or_else(T::zero) + <T as Scalar>::min_positive();
(min - margin, max + margin)
}
fn sturm_count<T: Field + Real>(diagonal: &[T], off_diagonal: &[T], x: T) -> usize {
let n = diagonal.len();
if n == 0 {
return 0;
}
let eps = <T as Scalar>::epsilon();
let mut count = 0;
let mut d = diagonal[0] - x;
if d < T::zero() {
count += 1;
} else if d < eps && d > -eps {
d = -eps;
count += 1;
}
for i in 1..n {
let e_sq = off_diagonal[i - 1] * off_diagonal[i - 1];
if Scalar::abs(d) < eps {
d = copysign_mag(eps, d);
}
d = (diagonal[i] - x) - e_sq / d;
if d < T::zero() {
count += 1;
} else if d < eps && d > -eps {
d = -eps;
count += 1;
}
}
count
}
fn vector_norm<T: Field + Real>(v: &[T]) -> T {
let sum: T = v.iter().map(|&x| x * x).sum();
Real::sqrt(sum)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::evd::SymmetricEvd;
fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() < tol
}
fn dense_tridiagonal(diag: &[f64], off: &[f64]) -> Mat<f64> {
let n = diag.len();
let mut a = Mat::zeros(n, n);
for i in 0..n {
a[(i, i)] = diag[i];
}
for i in 0..n.saturating_sub(1) {
a[(i, i + 1)] = off[i];
a[(i + 1, i)] = off[i];
}
a
}
fn max_residual(diag: &[f64], off: &[f64], evd: &MrrrEvd<f64>) -> f64 {
let n = diag.len();
let eigs = evd.eigenvalues();
let vecs = match evd.eigenvectors() {
Some(v) => v,
None => return f64::INFINITY,
};
let mut worst = 0.0f64;
for (j, &lambda) in eigs.iter().enumerate() {
for i in 0..n {
let mut tv = diag[i] * vecs[(i, j)];
if i > 0 {
tv += off[i - 1] * vecs[(i - 1, j)];
}
if i + 1 < n {
tv += off[i] * vecs[(i + 1, j)];
}
worst = worst.max((tv - lambda * vecs[(i, j)]).abs());
}
}
worst
}
fn max_orthogonality_error(evd: &MrrrEvd<f64>) -> f64 {
let vecs = match evd.eigenvectors() {
Some(v) => v,
None => return f64::INFINITY,
};
let n = vecs.nrows();
let m = vecs.ncols();
let mut worst = 0.0f64;
for a in 0..m {
for b in a..m {
let mut dot = 0.0;
for i in 0..n {
dot += vecs[(i, a)] * vecs[(i, b)];
}
let target = if a == b { 1.0 } else { 0.0 };
worst = worst.max((dot - target).abs());
}
}
worst
}
fn max_eigenvalue_error_vs_reference(diag: &[f64], off: &[f64], evd: &MrrrEvd<f64>) -> f64 {
let a = dense_tridiagonal(diag, off);
let reference = SymmetricEvd::compute(a.as_ref()).unwrap();
let mut ref_eigs: Vec<f64> = reference.eigenvalues().to_vec();
ref_eigs.sort_by(|x, y| x.partial_cmp(y).unwrap());
let mut mrrr_eigs: Vec<f64> = evd.eigenvalues().to_vec();
mrrr_eigs.sort_by(|x, y| x.partial_cmp(y).unwrap());
let mut worst = 0.0f64;
for (a, b) in ref_eigs.iter().zip(mrrr_eigs.iter()) {
worst = worst.max((a - b).abs());
}
worst
}
#[test]
fn test_mrrr_2x2() {
let diag = vec![2.0, 2.0];
let off_diag = vec![1.0];
let evd = MrrrEvd::compute(&diag, &off_diag).unwrap();
let eigs = evd.eigenvalues();
assert_eq!(eigs.len(), 2);
assert!(approx_eq(eigs[0], 1.0, 1e-10));
assert!(approx_eq(eigs[1], 3.0, 1e-10));
}
#[test]
fn test_mrrr_diagonal() {
let diag = vec![1.0, 2.0, 3.0];
let off_diag = vec![0.0, 0.0];
let evd = MrrrEvd::compute(&diag, &off_diag).unwrap();
let eigs = evd.eigenvalues();
assert_eq!(eigs.len(), 3);
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));
assert!(max_orthogonality_error(&evd) < 1e-12);
assert!(max_residual(&diag, &off_diag, &evd) < 1e-12);
}
#[test]
fn test_mrrr_eigenvalues_only() {
let diag = vec![4.0, 3.0, 2.0, 1.0];
let off_diag = vec![1.0, 2.0, 1.0];
let evd = MrrrEvd::eigenvalues_only(&diag, &off_diag).unwrap();
assert_eq!(evd.eigenvalues().len(), 4);
assert!(evd.eigenvectors().is_none());
}
#[test]
fn test_mrrr_eigenvectors_orthogonal() {
let diag = vec![2.0, 2.0];
let off_diag = vec![1.0];
let evd = MrrrEvd::compute(&diag, &off_diag).unwrap();
assert!(max_orthogonality_error(&evd) < 1e-10);
}
#[test]
fn test_mrrr_eigenvectors_normalized() {
let diag = vec![2.0, 2.0];
let off_diag = vec![1.0];
let evd = MrrrEvd::compute(&diag, &off_diag).unwrap();
let vecs = evd.eigenvectors().unwrap();
for j in 0..2 {
let mut norm = 0.0;
for i in 0..2 {
norm += vecs[(i, j)] * vecs[(i, j)];
}
assert!(approx_eq(norm, 1.0, 1e-10), "norm[{}] = {}", j, norm);
}
}
#[test]
fn test_mrrr_single_element() {
let diag = vec![5.0];
let off_diag: Vec<f64> = vec![];
let evd = MrrrEvd::compute(&diag, &off_diag).unwrap();
assert_eq!(evd.eigenvalues().len(), 1);
assert!(approx_eq(evd.eigenvalues()[0], 5.0, 1e-10));
}
#[test]
fn test_mrrr_range() {
let diag = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let off_diag = vec![0.0, 0.0, 0.0, 0.0];
let evd = MrrrEvd::compute_range(&diag, &off_diag, 1, 3).unwrap();
let eigs = evd.eigenvalues();
assert_eq!(eigs.len(), 3);
assert!(approx_eq(eigs[0], 2.0, 1e-10));
assert!(approx_eq(eigs[1], 3.0, 1e-10));
assert!(approx_eq(eigs[2], 4.0, 1e-10));
let vecs = evd.eigenvectors().unwrap();
assert_eq!(vecs.nrows(), 5);
assert_eq!(vecs.ncols(), 3);
}
#[test]
fn test_mrrr_eigenvalue_equation() {
let diag = vec![4.0, 3.0, 2.0, 1.0];
let off_diag = vec![1.0, 2.0, 1.0];
let evd = MrrrEvd::compute(&diag, &off_diag).unwrap();
assert!(max_residual(&diag, &off_diag, &evd) < 1e-8);
assert!(max_orthogonality_error(&evd) < 1e-8);
assert!(max_eigenvalue_error_vs_reference(&diag, &off_diag, &evd) < 1e-8);
}
#[test]
fn test_mrrr_negative_eigenvalues() {
let diag = vec![-2.0, -2.0];
let off_diag = vec![1.0];
let evd = MrrrEvd::compute(&diag, &off_diag).unwrap();
let eigs = evd.eigenvalues();
assert!(approx_eq(eigs[0], -3.0, 1e-10));
assert!(approx_eq(eigs[1], -1.0, 1e-10));
assert!(max_residual(&diag, &off_diag, &evd) < 1e-8);
}
#[test]
fn test_mrrr_clustered_eigenvalues() {
let diag = vec![2.0, 2.0, 2.0];
let off_diag = vec![1e-6, 1e-6];
let evd = MrrrEvd::compute(&diag, &off_diag).unwrap();
let eigs = evd.eigenvalues();
for &e in eigs {
assert!(approx_eq(e, 2.0, 1e-5), "eigenvalue = {}", e);
}
assert!(
max_orthogonality_error(&evd) < 1e-8,
"orthogonality error = {}",
max_orthogonality_error(&evd)
);
assert!(max_residual(&diag, &off_diag, &evd) < 1e-6);
}
#[test]
fn test_mrrr_repeated_eigenvalue_via_split() {
let diag = vec![5.0, 5.0, 5.0];
let off_diag = vec![0.0, 0.0];
let evd = MrrrEvd::compute(&diag, &off_diag).unwrap();
let eigs = evd.eigenvalues();
assert_eq!(eigs.len(), 3);
for &e in eigs {
assert!(approx_eq(e, 5.0, 1e-12));
}
assert!(max_orthogonality_error(&evd) < 1e-12);
assert!(max_residual(&diag, &off_diag, &evd) < 1e-12);
}
#[test]
fn test_mrrr_larger_matrix() {
let n = 10;
let diag: Vec<f64> = (1..=n).map(|i| i as f64).collect();
let off_diag: Vec<f64> = vec![0.0; n - 1];
let evd = MrrrEvd::compute(&diag, &off_diag).unwrap();
let eigs = evd.eigenvalues();
assert_eq!(eigs.len(), n);
for (i, &e) in eigs.iter().enumerate() {
assert!(approx_eq(e, (i + 1) as f64, 1e-10));
}
assert!(max_orthogonality_error(&evd) < 1e-12);
}
#[test]
fn test_mrrr_laplacian() {
let n = 12;
let diag = vec![2.0; n];
let off = vec![-1.0; n - 1];
let evd = MrrrEvd::compute(&diag, &off).unwrap();
let eigs = evd.eigenvalues();
assert_eq!(eigs.len(), n);
for (k, &lam) in eigs.iter().enumerate() {
let expected =
2.0 - 2.0 * ((k as f64 + 1.0) * std::f64::consts::PI / (n as f64 + 1.0)).cos();
assert!(
approx_eq(lam, expected, 1e-9),
"k={}, got {}, expected {}",
k,
lam,
expected
);
}
assert!(max_residual(&diag, &off, &evd) < 1e-9);
assert!(max_orthogonality_error(&evd) < 1e-9);
assert!(max_eigenvalue_error_vs_reference(&diag, &off, &evd) < 1e-9);
}
#[test]
fn test_mrrr_general_dense_reference() {
let diag = vec![1.0, -3.0, 4.0, 0.5, 2.5, -1.0, 6.0, 3.0];
let off = vec![0.7, 1.3, -0.9, 2.1, 0.4, -1.7, 0.6];
let evd = MrrrEvd::compute(&diag, &off).unwrap();
assert_eq!(evd.eigenvalues().len(), diag.len());
assert!(
max_residual(&diag, &off, &evd) < 1e-8,
"residual = {}",
max_residual(&diag, &off, &evd)
);
assert!(
max_orthogonality_error(&evd) < 1e-8,
"orthogonality = {}",
max_orthogonality_error(&evd)
);
assert!(
max_eigenvalue_error_vs_reference(&diag, &off, &evd) < 1e-8,
"eig error = {}",
max_eigenvalue_error_vs_reference(&diag, &off, &evd)
);
}
#[test]
fn test_mrrr_two_clusters() {
let diag = vec![1.0, 1.0, 1.0, 8.0, 8.0, 8.0];
let off = vec![1e-5, 1e-5, 3.0, 1e-5, 1e-5];
let evd = MrrrEvd::compute(&diag, &off).unwrap();
assert_eq!(evd.eigenvalues().len(), 6);
assert!(
max_orthogonality_error(&evd) < 1e-7,
"orthogonality = {}",
max_orthogonality_error(&evd)
);
assert!(max_residual(&diag, &off, &evd) < 1e-6);
assert!(max_eigenvalue_error_vs_reference(&diag, &off, &evd) < 1e-6);
}
#[test]
fn test_mrrr_split_blocks() {
let diag = vec![2.0, 1.0, 5.0, 4.0];
let off = vec![0.5, 0.0, 0.5];
let evd = MrrrEvd::compute(&diag, &off).unwrap();
assert_eq!(evd.eigenvalues().len(), 4);
assert!(max_residual(&diag, &off, &evd) < 1e-9);
assert!(max_orthogonality_error(&evd) < 1e-9);
assert!(max_eigenvalue_error_vs_reference(&diag, &off, &evd) < 1e-9);
}
#[test]
fn test_mrrr_large_pseudo_random() {
let n = 40usize;
let mut state: u64 = 0x1234_5678_9abc_def0;
let mut next = || {
state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
((state >> 33) as f64 / (1u64 << 31) as f64) - 1.0
};
let diag: Vec<f64> = (0..n).map(|_| 3.0 * next()).collect();
let off: Vec<f64> = (0..n - 1).map(|_| 1.0 + 0.5 * next()).collect();
let evd = MrrrEvd::compute(&diag, &off).unwrap();
assert_eq!(evd.eigenvalues().len(), n);
assert!(
max_residual(&diag, &off, &evd) < 1e-8,
"residual = {}",
max_residual(&diag, &off, &evd)
);
assert!(
max_orthogonality_error(&evd) < 1e-8,
"orthogonality = {}",
max_orthogonality_error(&evd)
);
assert!(
max_eigenvalue_error_vs_reference(&diag, &off, &evd) < 1e-8,
"eig error = {}",
max_eigenvalue_error_vs_reference(&diag, &off, &evd)
);
}
#[test]
fn test_mrrr_f32() {
let diag = vec![2.0f32, 2.0];
let off_diag = vec![1.0f32];
let evd = MrrrEvd::compute(&diag, &off_diag).unwrap();
let eigs = evd.eigenvalues();
assert_eq!(eigs.len(), 2);
assert!((eigs[0] - 1.0).abs() < 1e-5);
assert!((eigs[1] - 3.0).abs() < 1e-5);
let vecs = evd.eigenvectors().unwrap();
let mut dot = 0.0f32;
for i in 0..2 {
dot += vecs[(i, 0)] * vecs[(i, 1)];
}
assert!(dot.abs() < 1e-4, "dot = {}", dot);
}
#[test]
fn test_rrr_factorization_reconstructs() {
let diag = vec![4.0, 3.0, 2.0];
let off = vec![1.0, 1.0];
let eps = f64::EPSILON;
let (glow, ghigh) = gershgorin_bounds(&diag, &off);
let spdiam = ghigh - glow;
let pivmin = spdiam * eps * eps + f64::MIN_POSITIVE;
let (rrr, sigma) = build_base_rrr(&diag, &off, glow, spdiam, pivmin);
let n = 3;
for i in 0..n {
let mut aii = rrr.d[i];
if i > 0 {
aii += rrr.l[i - 1] * rrr.l[i - 1] * rrr.d[i - 1];
}
assert!((aii - (diag[i] - sigma)).abs() < 1e-10);
}
for i in 0..n - 1 {
assert!((rrr.ld[i] - off[i]).abs() < 1e-10);
}
}
#[test]
fn test_child_representation_shift() {
let diag = vec![4.0, 3.0, 2.0];
let off = vec![1.0, 1.0];
let eps = f64::EPSILON;
let (glow, ghigh) = gershgorin_bounds(&diag, &off);
let spdiam = ghigh - glow;
let pivmin = spdiam * eps * eps + f64::MIN_POSITIVE;
let (rrr, _sigma) = build_base_rrr(&diag, &off, glow, spdiam, pivmin);
let tau = 0.5;
let child = rrr.factor_child(tau, pivmin);
for &probe in &[-1.0, 0.0, 0.3, 1.0, 2.5] {
let cc = child.neg_count(probe, pivmin);
let pc = rrr.neg_count(probe + tau, pivmin);
assert_eq!(cc, pc, "probe = {}", probe);
}
}
#[test]
fn test_error_empty_input() {
let empty: Vec<f64> = vec![];
assert!(matches!(
MrrrEvd::compute(&empty, &empty),
Err(MrrrError::EmptyInput)
));
}
#[test]
fn test_error_dimension_mismatch() {
let diag = vec![1.0, 2.0, 3.0];
let off_diag = vec![1.0]; assert!(matches!(
MrrrEvd::compute(&diag, &off_diag),
Err(MrrrError::DimensionMismatch)
));
}
#[test]
fn test_error_invalid_range() {
let diag = vec![1.0, 2.0, 3.0];
let off_diag = vec![1.0, 1.0];
assert!(matches!(
MrrrEvd::compute_range(&diag, &off_diag, 5, 6),
Err(MrrrError::InvalidIndexRange)
));
}
}