use crate::error::Error;
use crate::math::matmul::gemv_par_auto;
use crate::parallel_gates::cheap_map_f64_parallel_threshold;
use ndarray::{Array1, Array2, Axis};
use ndarray_rand::rand::rngs::StdRng;
use ndarray_rand::rand::{Rng, SeedableRng};
use rayon::prelude::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator};
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 = gemv_par_auto(matrix, &v);
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(&gemv_par_auto(matrix, &v));
if !lambda.is_finite() {
return Err(Error::non_finite("power iteration eigenvalue"));
}
Ok((v, lambda))
}
pub(super) 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(super) 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 = gemv_par_auto(matrix, &v);
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 = nalgebra::DMatrix::<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 = nalgebra::linalg::SymmetricEigen::new(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 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 != λ·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],
"λ_0 ({}) must be > λ_1 ({})",
vals[0],
vals[1]
);
assert!(
vals[1] > vals[2],
"λ_1 ({}) must be > λ_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
);
}
}