use crate::error::Error;
use crate::math::matmul::matvec;
use crate::parallel_gates::cheap_map_f64_parallel_threshold;
use gemmkit_ndarray::Parallelism;
use ndarray::{Array1, Array2, Axis};
use ndarray_rand::rand::rngs::StdRng;
use ndarray_rand::rand::{Rng, SeedableRng};
use rayon::prelude::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator};
pub(crate) struct SymmetricEigen {
pub eigenvalues: Array1<f64>,
pub eigenvectors: Array2<f64>,
}
pub(crate) fn symmetric_eigen(a: &Array2<f64>) -> SymmetricEigen {
let n = a.nrows();
debug_assert_eq!(n, a.ncols(), "symmetric_eigen requires a square matrix");
if n == 0 {
return SymmetricEigen {
eigenvalues: Array1::zeros(0),
eigenvectors: Array2::zeros((0, 0)),
};
}
let mut v = vec![0.0_f64; n * n];
for i in 0..n {
for j in 0..n {
v[i * n + j] = a[[i, j]];
}
}
let mut d = vec![0.0_f64; n];
let mut e = vec![0.0_f64; n];
tred2(n, &mut v, &mut d, &mut e);
tql2(n, &mut v, &mut d, &mut e);
let eigenvalues = Array1::from_vec(d);
let eigenvectors = Array2::from_shape_fn((n, n), |(i, j)| v[i * n + j]);
SymmetricEigen {
eigenvalues,
eigenvectors,
}
}
fn tred2(n: usize, v: &mut [f64], d: &mut [f64], e: &mut [f64]) {
for j in 0..n {
d[j] = v[(n - 1) * n + j];
}
for i in (1..n).rev() {
let mut scale = 0.0;
let mut h = 0.0;
for &dk in d.iter().take(i) {
scale += dk.abs();
}
if scale == 0.0 {
e[i] = d[i - 1];
for j in 0..i {
d[j] = v[(i - 1) * n + j];
v[i * n + j] = 0.0;
v[j * n + i] = 0.0;
}
} else {
for dk in d.iter_mut().take(i) {
*dk /= scale;
h += *dk * *dk;
}
let mut f = d[i - 1];
let mut g = h.sqrt();
if f > 0.0 {
g = -g;
}
e[i] = scale * g;
h -= f * g;
d[i - 1] = f - g;
for ej in e.iter_mut().take(i) {
*ej = 0.0;
}
for j in 0..i {
f = d[j];
v[j * n + i] = f;
g = e[j] + v[j * n + j] * f;
for k in (j + 1)..=(i - 1) {
g += v[k * n + j] * d[k];
e[k] += v[k * n + j] * f;
}
e[j] = g;
}
f = 0.0;
for j in 0..i {
e[j] /= h;
f += e[j] * d[j];
}
let hh = f / (h + h);
for j in 0..i {
e[j] -= hh * d[j];
}
for j in 0..i {
f = d[j];
g = e[j];
for k in j..=(i - 1) {
v[k * n + j] -= f * e[k] + g * d[k];
}
d[j] = v[(i - 1) * n + j];
v[i * n + j] = 0.0;
}
}
d[i] = h;
}
for i in 0..(n - 1) {
v[(n - 1) * n + i] = v[i * n + i];
v[i * n + i] = 1.0;
let h = d[i + 1];
if h != 0.0 {
for k in 0..=i {
d[k] = v[k * n + (i + 1)] / h;
}
for j in 0..=i {
let mut g = 0.0;
for k in 0..=i {
g += v[k * n + (i + 1)] * v[k * n + j];
}
for k in 0..=i {
v[k * n + j] -= g * d[k];
}
}
}
for k in 0..=i {
v[k * n + (i + 1)] = 0.0;
}
}
for j in 0..n {
d[j] = v[(n - 1) * n + j];
v[(n - 1) * n + j] = 0.0;
}
v[(n - 1) * n + (n - 1)] = 1.0;
e[0] = 0.0;
}
#[allow(unused_assignments)]
fn tql2(n: usize, v: &mut [f64], d: &mut [f64], e: &mut [f64]) {
for i in 1..n {
e[i - 1] = e[i];
}
e[n - 1] = 0.0;
let mut f = 0.0_f64;
let mut tst1 = 0.0_f64;
let eps = 2.0_f64.powi(-52);
for l in 0..n {
tst1 = tst1.max(d[l].abs() + e[l].abs());
let mut m = l;
while m < n {
if e[m].abs() <= eps * tst1 {
break;
}
m += 1;
}
if m > l {
loop {
let mut g = d[l];
let mut p = (d[l + 1] - g) / (2.0 * e[l]);
let mut r = p.hypot(1.0);
if p < 0.0 {
r = -r;
}
d[l] = e[l] / (p + r);
d[l + 1] = e[l] * (p + r);
let dl1 = d[l + 1];
let mut h = g - d[l];
for di in d.iter_mut().take(n).skip(l + 2) {
*di -= h;
}
f += h;
p = d[m];
let mut c = 1.0;
let mut c2 = c;
let mut c3 = c;
let el1 = e[l + 1];
let mut s = 0.0;
let mut s2 = 0.0;
let mut i = m;
while i > l {
i -= 1;
c3 = c2;
c2 = c;
s2 = s;
g = c * e[i];
h = c * p;
r = p.hypot(e[i]);
e[i + 1] = s * r;
s = e[i] / r;
c = p / r;
p = c * d[i] - s * g;
d[i + 1] = h + s * (c * g + s * d[i]);
for k in 0..n {
h = v[k * n + (i + 1)];
v[k * n + (i + 1)] = s * v[k * n + i] + c * h;
v[k * n + i] = c * v[k * n + i] - s * h;
}
}
p = -s * s2 * c3 * el1 * e[l] / dl1;
e[l] = s * p;
d[l] = c * p;
if e[l].abs() <= eps * tst1 {
break;
}
}
}
d[l] += f;
e[l] = 0.0;
}
for i in 0..(n - 1) {
let mut k = i;
let mut p = d[i];
for (j, &dj) in d.iter().enumerate().take(n).skip(i + 1) {
if dj < p {
k = j;
p = dj;
}
}
if k != i {
d[k] = d[i];
d[i] = p;
for row in 0..n {
v.swap(row * n + i, row * n + k);
}
}
}
}
pub(crate) struct Svd {
pub singular_values: Array1<f64>,
#[cfg_attr(not(feature = "machine_learning"), allow(dead_code))]
pub u: Option<Array2<f64>>,
pub v_t: Option<Array2<f64>>,
}
#[cfg(feature = "machine_learning")]
impl Svd {
pub(crate) fn solve(&self, b: &Array1<f64>, tol: f64) -> Result<Array1<f64>, Error> {
let u = self
.u
.as_ref()
.ok_or_else(|| Error::computation("SVD solve requires the U factor"))?;
let v_t = self
.v_t
.as_ref()
.ok_or_else(|| Error::computation("SVD solve requires the V^T factor"))?;
let m = u.nrows();
let r = self.singular_values.len();
let n = v_t.ncols();
if b.len() != m {
return Err(Error::computation(
"SVD solve: right-hand side length does not match the number of rows",
));
}
let mut c = Array1::<f64>::zeros(r);
for k in 0..r {
let sv = self.singular_values[k];
if sv > tol {
let mut acc = 0.0;
for i in 0..m {
acc += u[[i, k]] * b[i];
}
c[k] = acc / sv;
}
}
let mut x = Array1::<f64>::zeros(n);
for j in 0..n {
let mut acc = 0.0;
for k in 0..r {
acc += v_t[[k, j]] * c[k];
}
x[j] = acc;
}
Ok(x)
}
pub(crate) fn pseudo_inverse(&self, tol: f64) -> Result<Array2<f64>, Error> {
let u = self
.u
.as_ref()
.ok_or_else(|| Error::computation("SVD pseudo_inverse requires the U factor"))?;
let v_t = self
.v_t
.as_ref()
.ok_or_else(|| Error::computation("SVD pseudo_inverse requires the V^T factor"))?;
let m = u.nrows();
let r = self.singular_values.len();
let n = v_t.ncols();
let mut sinv = Array1::<f64>::zeros(r);
for k in 0..r {
let sv = self.singular_values[k];
sinv[k] = if sv > tol { 1.0 / sv } else { 0.0 };
}
let mut pinv = Array2::<f64>::zeros((n, m));
for j in 0..n {
for i in 0..m {
let mut acc = 0.0;
for k in 0..r {
acc += v_t[[k, j]] * sinv[k] * u[[i, k]];
}
pinv[[j, i]] = acc;
}
}
Ok(pinv)
}
}
pub(crate) fn svd(a: &Array2<f64>, compute_u: bool, compute_v: bool) -> Svd {
let m = a.nrows();
let n = a.ncols();
let (s, u_like, v_like) = if m >= n {
jacobi_svd_tall(a)
} else {
let at = a.t().to_owned();
let (s, u2, v2) = jacobi_svd_tall(&at);
(s, v2, u2)
};
let (s_sorted, u_sorted, v_sorted) = sort_svd_descending(s, u_like, v_like);
Svd {
singular_values: s_sorted,
u: if compute_u { Some(u_sorted) } else { None },
v_t: if compute_v {
Some(v_sorted.t().to_owned())
} else {
None
},
}
}
fn jacobi_svd_tall(a: &Array2<f64>) -> (Array1<f64>, Array2<f64>, Array2<f64>) {
let m = a.nrows();
let n = a.ncols();
let mut u = a.to_owned(); let mut v = Array2::<f64>::eye(n);
let eps = f64::EPSILON;
let max_sweeps = 60;
for _sweep in 0..max_sweeps {
let mut changed = false;
for p in 0..n {
for q in (p + 1)..n {
let mut alpha = 0.0; let mut beta = 0.0; let mut gamma = 0.0; for i in 0..m {
let up = u[[i, p]];
let uq = u[[i, q]];
alpha += up * up;
beta += uq * uq;
gamma += up * uq;
}
if gamma == 0.0 || gamma.abs() <= eps * (alpha * beta).sqrt() {
continue;
}
changed = true;
let zeta = (beta - alpha) / (2.0 * gamma);
let sign = if zeta >= 0.0 { 1.0 } else { -1.0 };
let t = sign / (zeta.abs() + (1.0 + zeta * zeta).sqrt());
let c = 1.0 / (1.0 + t * t).sqrt();
let s = c * t;
for i in 0..m {
let up = u[[i, p]];
let uq = u[[i, q]];
u[[i, p]] = c * up - s * uq;
u[[i, q]] = s * up + c * uq;
}
for i in 0..n {
let vp = v[[i, p]];
let vq = v[[i, q]];
v[[i, p]] = c * vp - s * vq;
v[[i, q]] = s * vp + c * vq;
}
}
}
if !changed {
break;
}
}
let mut s = Array1::<f64>::zeros(n);
for j in 0..n {
let mut norm_sq = 0.0;
for i in 0..m {
norm_sq += u[[i, j]] * u[[i, j]];
}
let norm = norm_sq.sqrt();
s[j] = norm;
if norm > 0.0 {
for i in 0..m {
u[[i, j]] /= norm;
}
}
}
(s, u, v)
}
fn sort_svd_descending(
s: Array1<f64>,
u: Array2<f64>,
v: Array2<f64>,
) -> (Array1<f64>, Array2<f64>, Array2<f64>) {
let r = s.len();
let mut order: Vec<usize> = (0..r).collect();
order.sort_by(|&i, &j| s[j].partial_cmp(&s[i]).unwrap_or(std::cmp::Ordering::Equal));
let s_sorted = Array1::from_shape_fn(r, |k| s[order[k]]);
let u_sorted = Array2::from_shape_fn((u.nrows(), r), |(i, k)| u[[i, order[k]]]);
let v_sorted = Array2::from_shape_fn((v.nrows(), r), |(i, k)| v[[i, order[k]]]);
(s_sorted, u_sorted, v_sorted)
}
pub(crate) fn qr_q(a: &Array2<f64>) -> Array2<f64> {
let m = a.nrows();
let k = a.ncols();
let mut q = a.to_owned();
for j in 0..k {
for _pass in 0..2 {
for i in 0..j {
let mut proj = 0.0;
for row in 0..m {
proj += q[[row, i]] * q[[row, j]];
}
for row in 0..m {
q[[row, j]] -= proj * q[[row, i]];
}
}
}
let mut norm_sq = 0.0;
for row in 0..m {
norm_sq += q[[row, j]] * q[[row, j]];
}
let norm = norm_sq.sqrt();
if norm > f64::EPSILON {
for row in 0..m {
q[[row, j]] /= norm;
}
} else {
for row in 0..m {
q[[row, j]] = 0.0;
}
}
}
q
}
fn random_unit_vector(n: usize, rng: &mut StdRng) -> Array1<f64> {
let mut v = Array1::<f64>::from_shape_fn(n, |_| rng.random_range(-1.0..1.0));
let norm = v.dot(&v).sqrt();
if norm <= f64::EPSILON {
v.fill(1.0 / (n as f64).sqrt());
} else {
v /= norm;
}
v
}
fn dominant_eigenpair(
matrix: &Array2<f64>,
rng: &mut StdRng,
max_iter: usize,
tol: f64,
) -> Result<(Array1<f64>, f64), Error> {
let n = matrix.ncols();
let mut v = random_unit_vector(n, rng);
let mut prev_lambda = 0.0;
for _ in 0..max_iter {
let w = matvec(matrix, &v, Parallelism::Rayon(0));
let lambda = v.dot(&w);
if !lambda.is_finite() {
return Err(Error::non_finite("power iteration eigenvalue"));
}
let w_norm = w.dot(&w).sqrt();
if w_norm <= f64::EPSILON || !w_norm.is_finite() {
return Err(Error::not_converged("Power iteration failed to converge"));
}
if (lambda - prev_lambda).abs() < tol {
return Ok((v, lambda));
}
prev_lambda = lambda;
v = &w / w_norm;
}
let lambda = v.dot(&matvec(matrix, &v, Parallelism::Rayon(0)));
if !lambda.is_finite() {
return Err(Error::non_finite("power iteration eigenvalue"));
}
Ok((v, lambda))
}
pub(crate) fn top_eigenpairs_power_iteration(
mut matrix: Array2<f64>,
k: usize,
seed: u64,
max_iter: usize,
tol: f64,
) -> Result<(Vec<f64>, Vec<Array1<f64>>), Error> {
let mut rng = StdRng::seed_from_u64(seed);
let mut eigenvalues = Vec::with_capacity(k);
let mut eigenvectors = Vec::with_capacity(k);
for _ in 0..k {
let (vector, value) = dominant_eigenpair(&matrix, &mut rng, max_iter, tol)?;
deflate_rank_one(&mut matrix, &vector, value);
eigenvalues.push(value);
eigenvectors.push(vector);
}
Ok((eigenvalues, eigenvectors))
}
fn deflate_rank_one(matrix: &mut Array2<f64>, v: &Array1<f64>, value: f64) {
let n = matrix.nrows();
if n.saturating_mul(n) >= cheap_map_f64_parallel_threshold() {
matrix
.axis_iter_mut(Axis(0))
.into_par_iter()
.enumerate()
.for_each(|(i, mut row)| {
row.scaled_add(-value * v[i], v);
});
} else {
for (i, mut row) in matrix.axis_iter_mut(Axis(0)).enumerate() {
row.scaled_add(-value * v[i], v);
}
}
}
pub(crate) fn top_eigenpairs_lanczos(
matrix: &Array2<f64>,
k: usize,
seed: u64,
) -> Result<(Vec<f64>, Vec<Array1<f64>>), Error> {
let n = matrix.ncols();
let m = (2 * k + 20).min(n);
let mut rng = StdRng::seed_from_u64(seed);
let mut lanczos_vectors: Vec<Array1<f64>> = Vec::with_capacity(m);
let mut alphas: Vec<f64> = Vec::with_capacity(m);
let mut betas: Vec<f64> = Vec::with_capacity(m);
let mut v = random_unit_vector(n, &mut rng);
let mut v_prev: Option<Array1<f64>> = None;
let mut beta_prev = 0.0;
for _ in 0..m {
let mut w = matvec(matrix, &v, Parallelism::Rayon(0));
let alpha = v.dot(&w);
w.scaled_add(-alpha, &v);
if let Some(ref vp) = v_prev {
w.scaled_add(-beta_prev, vp);
}
for _ in 0..2 {
for u in lanczos_vectors.iter() {
let proj = w.dot(u);
w.scaled_add(-proj, u);
}
let proj_v = w.dot(&v);
w.scaled_add(-proj_v, &v);
}
lanczos_vectors.push(v.clone());
alphas.push(alpha);
let beta = w.dot(&w).sqrt();
if beta <= 1e-12 || !beta.is_finite() {
break;
}
betas.push(beta);
v_prev = Some(v);
beta_prev = beta;
v = w / beta;
}
let dim = alphas.len();
if dim == 0 {
return Err(Error::not_converged(
"Lanczos iteration produced an empty subspace",
));
}
let mut tri = Array2::<f64>::zeros((dim, dim));
for i in 0..dim {
tri[[i, i]] = alphas[i];
if i + 1 < dim {
tri[[i, i + 1]] = betas[i];
tri[[i + 1, i]] = betas[i];
}
}
let eigen = symmetric_eigen(&tri);
let mut order: Vec<usize> = (0..dim).collect();
order.sort_by(|&a, &b| {
eigen.eigenvalues[b]
.partial_cmp(&eigen.eigenvalues[a])
.unwrap_or(std::cmp::Ordering::Equal)
});
let take = k.min(dim);
let mut eigenvalues = Vec::with_capacity(take);
let mut eigenvectors = Vec::with_capacity(take);
for &idx in order.iter().take(take) {
eigenvalues.push(eigen.eigenvalues[idx]);
let mut ritz = Array1::<f64>::zeros(n);
for (j, lv) in lanczos_vectors.iter().enumerate() {
ritz.scaled_add(eigen.eigenvectors[[j, idx]], lv);
}
let norm = ritz.dot(&ritz).sqrt();
if norm > f64::EPSILON {
ritz /= norm;
}
eigenvectors.push(ritz);
}
Ok((eigenvalues, eigenvectors))
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
use ndarray::array;
fn na_dmatrix(a: &Array2<f64>) -> nalgebra::DMatrix<f64> {
let (r, c) = (a.nrows(), a.ncols());
nalgebra::DMatrix::from_row_slice(r, c, a.as_slice().unwrap())
}
fn assert_svd_reconstructs(a: &Array2<f64>, tol: f64) {
let s = svd(a, true, true);
let u = s.u.as_ref().unwrap();
let v_t = s.v_t.as_ref().unwrap();
let r = s.singular_values.len();
let (m, n) = (a.nrows(), a.ncols());
for i in 0..m {
for j in 0..n {
let mut acc = 0.0;
for k in 0..r {
acc += u[[i, k]] * s.singular_values[k] * v_t[[k, j]];
}
assert_abs_diff_eq!(acc, a[[i, j]], epsilon = tol);
}
}
}
#[test]
fn symmetric_eigen_eigenvalues_match_nalgebra() {
let a = array![
[4.0, 1.0, 0.0, 0.5],
[1.0, 3.0, 0.5, 0.0],
[0.0, 0.5, 2.0, 1.0],
[0.5, 0.0, 1.0, 1.0],
];
let mine = symmetric_eigen(&a);
let mut mine_vals: Vec<f64> = mine.eigenvalues.to_vec();
mine_vals.sort_by(|x, y| x.partial_cmp(y).unwrap());
let na = nalgebra::linalg::SymmetricEigen::new(na_dmatrix(&a));
let mut na_vals: Vec<f64> = na.eigenvalues.iter().copied().collect();
na_vals.sort_by(|x, y| x.partial_cmp(y).unwrap());
for (m, n) in mine_vals.iter().zip(na_vals.iter()) {
assert_abs_diff_eq!(m, n, epsilon = 1e-10);
}
}
#[test]
fn symmetric_eigen_pairs_are_valid_and_orthonormal() {
let a = array![[2.0, -1.0, 0.0], [-1.0, 2.0, -1.0], [0.0, -1.0, 2.0]];
let eig = symmetric_eigen(&a);
let n = a.nrows();
for j in 0..n {
let v = eig.eigenvectors.column(j);
let av = a.dot(&v);
for i in 0..n {
assert_abs_diff_eq!(av[i], eig.eigenvalues[j] * v[i], epsilon = 1e-10);
}
}
for i in 0..n {
for j in 0..n {
let dot: f64 = eig.eigenvectors.column(i).dot(&eig.eigenvectors.column(j));
let expected = if i == j { 1.0 } else { 0.0 };
assert_abs_diff_eq!(dot, expected, epsilon = 1e-10);
}
}
}
#[test]
fn symmetric_eigen_one_by_one() {
let a = array![[3.5]];
let eig = symmetric_eigen(&a);
assert_abs_diff_eq!(eig.eigenvalues[0], 3.5, epsilon = 1e-12);
assert_abs_diff_eq!(eig.eigenvectors[[0, 0]].abs(), 1.0, epsilon = 1e-12);
}
#[test]
fn symmetric_eigen_diagonal() {
let a = array![[5.0, 0.0, 0.0], [0.0, -2.0, 0.0], [0.0, 0.0, 1.0]];
let eig = symmetric_eigen(&a);
let mut vals = eig.eigenvalues.to_vec();
vals.sort_by(|x, y| x.partial_cmp(y).unwrap());
assert_abs_diff_eq!(vals[0], -2.0, epsilon = 1e-12);
assert_abs_diff_eq!(vals[1], 1.0, epsilon = 1e-12);
assert_abs_diff_eq!(vals[2], 5.0, epsilon = 1e-12);
}
#[test]
fn svd_tall_matches_nalgebra() {
let a = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0], [7.0, 8.0]];
let mine = svd(&a, true, true);
let na = nalgebra::linalg::SVD::new(na_dmatrix(&a), true, true);
let mut na_sv: Vec<f64> = na.singular_values.iter().copied().collect();
na_sv.sort_by(|x, y| y.partial_cmp(x).unwrap());
for (m, n) in mine.singular_values.iter().zip(na_sv.iter()) {
assert_abs_diff_eq!(m, n, epsilon = 1e-10);
}
assert_svd_reconstructs(&a, 1e-10);
}
#[test]
fn svd_wide_reconstructs() {
let a = array![[1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0]];
assert_svd_reconstructs(&a, 1e-10);
let s = svd(&a, false, true);
assert_eq!(s.singular_values.len(), 2);
}
#[test]
fn svd_square_reconstructs() {
let a = array![[2.0, 0.0, 1.0], [0.0, 3.0, 0.0], [1.0, 0.0, 2.0]];
assert_svd_reconstructs(&a, 1e-10);
}
#[cfg(feature = "machine_learning")]
#[test]
fn svd_solve_least_squares() {
let a = array![[1.0, 1.0], [1.0, 2.0], [1.0, 3.0], [1.0, 4.0]];
let b = array![6.0, 5.0, 7.0, 10.0];
let mine = svd(&a, true, true).solve(&b, 1e-12).unwrap();
let na = nalgebra::linalg::SVD::new(na_dmatrix(&a), true, true);
let nb = nalgebra::DVector::from_row_slice(b.as_slice().unwrap());
let na_sol = na.solve(&nb, 1e-12).unwrap();
for i in 0..mine.len() {
assert_abs_diff_eq!(mine[i], na_sol[i], epsilon = 1e-9);
}
}
#[cfg(feature = "machine_learning")]
#[test]
fn svd_pseudo_inverse_identity() {
let a = array![[1.0, 2.0], [3.0, 4.0], [5.0, 7.0]];
let pinv = svd(&a, true, true).pseudo_inverse(1e-12).unwrap();
let ap = a.dot(&pinv); let apa = ap.dot(&a); for i in 0..a.nrows() {
for j in 0..a.ncols() {
assert_abs_diff_eq!(apa[[i, j]], a[[i, j]], epsilon = 1e-9);
}
}
}
#[cfg(feature = "machine_learning")]
#[test]
fn svd_pseudo_inverse_rank_deficient() {
let a = array![[1.0, 1.0], [2.0, 2.0], [3.0, 3.0]];
let s = svd(&a, true, true);
let tol = 1e-12 * s.singular_values[0].max(1e-12);
let pinv = s.pseudo_inverse(tol.max(1e-12)).unwrap();
let ap = a.dot(&pinv);
let apa = ap.dot(&a);
for i in 0..a.nrows() {
for j in 0..a.ncols() {
assert_abs_diff_eq!(apa[[i, j]], a[[i, j]], epsilon = 1e-8);
}
}
}
#[test]
fn qr_q_orthonormal_and_spanning() {
let a = array![
[1.0, 2.0, 0.0],
[0.0, 1.0, 1.0],
[1.0, 0.0, 1.0],
[2.0, 1.0, 3.0],
];
let q = qr_q(&a);
let k = a.ncols();
for i in 0..k {
for j in 0..k {
let dot: f64 = q.column(i).dot(&q.column(j));
let expected = if i == j { 1.0 } else { 0.0 };
assert_abs_diff_eq!(dot, expected, epsilon = 1e-10);
}
}
let qt_a = q.t().dot(&a);
let proj = q.dot(&qt_a);
for i in 0..a.nrows() {
for j in 0..a.ncols() {
assert_abs_diff_eq!(proj[[i, j]], a[[i, j]], epsilon = 1e-10);
}
}
}
}
#[cfg(test)]
mod iterative_tests {
use super::*;
use ndarray::array;
fn symmetric_test_matrix() -> Array2<f64> {
array![
[4.0, 1.0, 0.0, 0.5],
[1.0, 3.0, 0.5, 0.0],
[0.0, 0.5, 2.0, 1.0],
[0.5, 0.0, 1.0, 1.0],
]
}
fn reference_eigenvalues_desc(a: &Array2<f64>) -> Vec<f64> {
let n = a.nrows();
let m = nalgebra::DMatrix::from_row_slice(n, n, a.as_slice().unwrap());
let eig = nalgebra::linalg::SymmetricEigen::new(m);
let mut vals: Vec<f64> = eig.eigenvalues.iter().copied().collect();
vals.sort_by(|x, y| y.partial_cmp(x).unwrap());
vals
}
fn assert_eigenpair(a: &Array2<f64>, value: f64, vector: &Array1<f64>, tol: f64) {
let av = a.dot(vector);
let lv = vector * value;
for (x, y) in av.iter().zip(lv.iter()) {
assert!((x - y).abs() < tol, "A*v != lambda*v: {} vs {}", x, y);
}
assert!(
(vector.dot(vector).sqrt() - 1.0).abs() < tol,
"eigenvector is not unit norm"
);
}
#[test]
fn power_iteration_matches_dense_reference() {
let a = symmetric_test_matrix();
let reference = reference_eigenvalues_desc(&a);
let (vals, vecs) = top_eigenpairs_power_iteration(a.clone(), 2, 0, 2000, 1e-10).unwrap();
assert_eq!(vals.len(), 2);
for i in 0..2 {
assert!(
(vals[i] - reference[i]).abs() < 1e-4,
"eigenvalue {} mismatch: {} vs {}",
i,
vals[i],
reference[i]
);
assert_eigenpair(&a, vals[i], &vecs[i], 1e-4);
}
}
#[test]
fn lanczos_matches_dense_reference() {
let a = symmetric_test_matrix();
let reference = reference_eigenvalues_desc(&a);
let (vals, vecs) = top_eigenpairs_lanczos(&a, 3, 0).unwrap();
assert_eq!(vals.len(), 3);
for i in 0..3 {
assert!(
(vals[i] - reference[i]).abs() < 1e-8,
"eigenvalue {} mismatch: {} vs {}",
i,
vals[i],
reference[i]
);
assert_eigenpair(&a, vals[i], &vecs[i], 1e-8);
}
}
#[test]
fn power_iteration_k_zero_returns_empty() {
let a = symmetric_test_matrix();
let (vals, vecs) = top_eigenpairs_power_iteration(a, 0, 0, 2000, 1e-10).unwrap();
assert_eq!(vals.len(), 0, "eigenvalues should be empty for k=0");
assert_eq!(vecs.len(), 0, "eigenvectors should be empty for k=0");
}
#[test]
fn power_iteration_eigenvalues_descending_order() {
let a = symmetric_test_matrix();
let (vals, _) = top_eigenpairs_power_iteration(a, 3, 0, 2000, 1e-10).unwrap();
assert_eq!(vals.len(), 3);
assert!(
vals[0] > vals[1],
"lambda_0 ({}) must be > lambda_1 ({})",
vals[0],
vals[1]
);
assert!(
vals[1] > vals[2],
"lambda_1 ({}) must be > lambda_2 ({})",
vals[1],
vals[2]
);
}
#[test]
fn power_iteration_eigenvectors_mutually_orthogonal() {
let a = symmetric_test_matrix();
let (_, vecs) = top_eigenpairs_power_iteration(a, 3, 0, 2000, 1e-10).unwrap();
assert_eq!(vecs.len(), 3);
for i in 0..3 {
for j in (i + 1)..3 {
let dot = vecs[i].dot(&vecs[j]).abs();
assert!(dot < 1e-5, "v_{} . v_{} = {} (expected < 1e-5)", i, j, dot);
}
}
}
#[test]
fn lanczos_k_zero_returns_empty() {
let a = symmetric_test_matrix();
let (vals, vecs) = top_eigenpairs_lanczos(&a, 0, 0).unwrap();
assert_eq!(vals.len(), 0, "eigenvalues should be empty for k=0");
assert_eq!(vecs.len(), 0, "eigenvectors should be empty for k=0");
}
#[test]
fn lanczos_rank_one_invariant_subspace_early_exit() {
let a: Array2<f64> = ndarray::array![[1.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0],];
let (vals, vecs) = top_eigenpairs_lanczos(&a, 5, 7).unwrap();
assert!(
vals.len() <= 2,
"expected at most 2 eigenpairs from a rank-1 matrix, got {}",
vals.len()
);
assert_eq!(
vals.len(),
vecs.len(),
"eigenvalues and eigenvectors must have equal length"
);
let n_large = vals.iter().filter(|&&v| v.abs() >= 1e-3).count();
assert!(
n_large <= 1,
"at most 1 non-trivial eigenvalue expected for rank-1 matrix, found {}",
n_large
);
}
}