#[inline]
fn dot(a: &[f64], b: &[f64]) -> f64 {
a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
}
#[inline]
fn norm2(v: &[f64]) -> f64 {
dot(v, v).sqrt()
}
#[inline]
fn axpy(alpha: f64, x: &[f64], y: &mut [f64]) {
for (yi, &xi) in y.iter_mut().zip(x.iter()) {
*yi += alpha * xi;
}
}
#[inline]
fn scal(s: f64, x: &mut [f64]) {
x.iter_mut().for_each(|v| *v *= s);
}
pub fn jacobi_eigen_sym(
a: &mut Vec<Vec<f64>>,
n: usize,
max_sweeps: usize,
tol: f64,
) -> (Vec<f64>, Vec<Vec<f64>>) {
let mut q: Vec<Vec<f64>> = (0..n)
.map(|i| {
let mut row = vec![0.0f64; n];
row[i] = 1.0;
row
})
.collect();
for _sweep in 0..max_sweeps {
let off_norm_sq: f64 = (0..n)
.flat_map(|i| (i + 1..n).map(move |j| (i, j)))
.map(|(i, j)| a[i][j] * a[i][j] * 2.0)
.sum();
if off_norm_sq.sqrt() < tol {
break;
}
for p in 0..n {
for q_idx in (p + 1)..n {
let a_pq = a[p][q_idx];
if a_pq.abs() < 1e-300 {
continue;
}
let a_pp = a[p][p];
let a_qq = a[q_idx][q_idx];
let theta = 0.5 * (a_qq - a_pp) / a_pq;
let t = if theta >= 0.0 {
1.0 / (theta + (1.0 + theta * theta).sqrt())
} else {
1.0 / (theta - (1.0 + theta * theta).sqrt())
};
let c = 1.0 / (1.0 + t * t).sqrt();
let s = t * c;
let tau = s / (1.0 + c);
a[p][p] -= t * a_pq;
a[q_idx][q_idx] += t * a_pq;
a[p][q_idx] = 0.0;
a[q_idx][p] = 0.0;
for r in 0..n {
if r == p || r == q_idx {
continue;
}
let a_rp = a[r][p];
let a_rq = a[r][q_idx];
let new_rp = a_rp - s * (a_rq + tau * a_rp);
let new_rq = a_rq + s * (a_rp - tau * a_rq);
a[r][p] = new_rp;
a[p][r] = new_rp;
a[r][q_idx] = new_rq;
a[q_idx][r] = new_rq;
}
for r in 0..n {
let q_rp = q[r][p];
let q_rq = q[r][q_idx];
q[r][p] = c * q_rp - s * q_rq;
q[r][q_idx] = s * q_rp + c * q_rq;
}
}
}
}
let mut eigenvalues: Vec<f64> = (0..n).map(|i| a[i][i]).collect();
let mut eigenvectors: Vec<Vec<f64>> = (0..n)
.map(|i| (0..n).map(|r| q[r][i]).collect())
.collect();
sort_eigenpairs(&mut eigenvalues, &mut eigenvectors);
(eigenvalues, eigenvectors)
}
pub fn tridiagonal_eigen_dc(
diag: &[f64],
offdiag: &[f64],
) -> (Vec<f64>, Vec<Vec<f64>>) {
let n = diag.len();
if n == 0 {
return (Vec::new(), Vec::new());
}
if n == 1 {
return (vec![diag[0]], vec![vec![1.0]]);
}
dc_solve(diag, offdiag, n)
}
fn dc_solve(diag: &[f64], offdiag: &[f64], n: usize) -> (Vec<f64>, Vec<Vec<f64>>) {
if n <= 4 {
let mut a: Vec<Vec<f64>> = vec![vec![0.0f64; n]; n];
for i in 0..n {
a[i][i] = diag[i];
if i + 1 < n {
a[i][i + 1] = offdiag[i];
a[i + 1][i] = offdiag[i];
}
}
return jacobi_eigen_sym(&mut a, n, 100, 1e-14);
}
let mid = n / 2;
let rho = offdiag[mid - 1];
let mut d1: Vec<f64> = diag[..mid].to_vec();
d1[mid - 1] -= rho.abs();
let of1 = &offdiag[..mid - 1];
let mut d2: Vec<f64> = diag[mid..].to_vec();
d2[0] -= rho.abs();
let of2 = &offdiag[mid..];
let (mut ev1, mut evec1) = dc_solve(&d1, of1, mid);
let (mut ev2, mut evec2) = dc_solve(&d2, of2, n - mid);
sort_eigenpairs(&mut ev1, &mut evec1);
sort_eigenpairs(&mut ev2, &mut evec2);
let sign_rho = if rho >= 0.0 { 1.0 } else { -1.0 };
let d_merged: Vec<f64> = ev1.iter().chain(ev2.iter()).copied().collect();
let mut z: Vec<f64> = {
let mut v = Vec::with_capacity(n);
for k in 0..mid {
v.push(sign_rho * evec1[k][mid - 1]);
}
for k in 0..n - mid {
v.push(evec2[k][0]);
}
v
};
let z_norm = norm2(&z);
let rho_eff = rho.abs() * z_norm * z_norm;
let inv_z_norm = if z_norm > 1e-300 { 1.0 / z_norm } else { 1.0 };
scal(inv_z_norm, &mut z);
let mut idx: Vec<usize> = (0..n).collect();
idx.sort_unstable_by(|&i, &j| d_merged[i].partial_cmp(&d_merged[j]).unwrap_or(std::cmp::Ordering::Equal));
let d_sorted: Vec<f64> = idx.iter().map(|&i| d_merged[i]).collect();
let z_sorted: Vec<f64> = idx.iter().map(|&i| z[i]).collect();
let mut merged_evals: Vec<f64> = Vec::with_capacity(n);
for k in 0..n {
let lo = if k == 0 {
d_sorted[0] - rho_eff.abs() - 1.0
} else {
d_sorted[k - 1]
};
let hi = d_sorted[k] + rho_eff.abs() + 1.0;
let lam = secular_root(&d_sorted, &z_sorted, rho_eff, lo, hi);
merged_evals.push(lam);
}
let mut merged_evecs: Vec<Vec<f64>> = Vec::with_capacity(n);
for &lam in &merged_evals {
let mut v: Vec<f64> = (0..n)
.map(|k| {
let denom = d_sorted[k] - lam;
if denom.abs() < 1e-300 {
z_sorted[k].signum() * 1e14
} else {
z_sorted[k] / denom
}
})
.collect();
let vnorm = norm2(&v);
if vnorm > 1e-300 {
scal(1.0 / vnorm, &mut v);
}
let mut v_unperm = vec![0.0f64; n];
for (sorted_pos, &orig_pos) in idx.iter().enumerate() {
v_unperm[orig_pos] = v[sorted_pos];
}
let mut global_evec = vec![0.0f64; n];
let v1 = &v_unperm[..mid];
for row in 0..mid {
let mut acc = 0.0f64;
for (col, &vk) in v1.iter().enumerate() {
acc += evec1[col][row] * vk;
}
global_evec[row] = acc;
}
let v2 = &v_unperm[mid..];
for row in 0..n - mid {
let mut acc = 0.0f64;
for (col, &vk) in v2.iter().enumerate() {
acc += evec2[col][row] * vk;
}
global_evec[mid + row] = acc;
}
let gnorm = norm2(&global_evec);
if gnorm > 1e-300 {
scal(1.0 / gnorm, &mut global_evec);
}
merged_evecs.push(global_evec);
}
sort_eigenpairs(&mut merged_evals, &mut merged_evecs);
(merged_evals, merged_evecs)
}
fn secular_root(d: &[f64], z: &[f64], rho: f64, lo: f64, hi: f64) -> f64 {
let n = d.len();
let secular = |lam: f64| -> f64 {
1.0 + rho * (0..n).map(|k| z[k] * z[k] / (d[k] - lam)).sum::<f64>()
};
let mut a = lo + 1e-12 * (hi - lo);
let mut b = hi - 1e-12 * (hi - lo);
let fa = secular(a);
let fb = secular(b);
if fa * fb > 0.0 {
return (a + b) * 0.5;
}
for _ in 0..80 {
let mid = (a + b) * 0.5;
if b - a < 1e-13 * (1.0 + mid.abs()) {
return mid;
}
let fm = secular(mid);
if fa * fm <= 0.0 {
b = mid;
} else {
a = mid;
}
}
(a + b) * 0.5
}
fn sort_eigenpairs(evals: &mut Vec<f64>, evecs: &mut Vec<Vec<f64>>) {
let n = evals.len();
let mut idx: Vec<usize> = (0..n).collect();
idx.sort_unstable_by(|&i, &j| evals[i].partial_cmp(&evals[j]).unwrap_or(std::cmp::Ordering::Equal));
let ev_sorted: Vec<f64> = idx.iter().map(|&i| evals[i]).collect();
let evec_sorted: Vec<Vec<f64>> = idx.iter().map(|&i| evecs[i].clone()).collect();
*evals = ev_sorted;
*evecs = evec_sorted;
}
pub struct GolubKahanBidiag {
pub u: Vec<Vec<f64>>,
pub v: Vec<Vec<f64>>,
pub alpha: Vec<f64>,
pub beta: Vec<f64>,
}
pub fn golub_kahan_bidiag<F, G>(
mat_vec: F,
mat_transpose_vec: G,
m: usize,
n: usize,
k: usize,
) -> GolubKahanBidiag
where
F: Fn(&[f64]) -> Vec<f64>,
G: Fn(&[f64]) -> Vec<f64>,
{
let k = k.min(m.min(n));
let mut u: Vec<Vec<f64>> = Vec::with_capacity(k + 1);
let mut v: Vec<Vec<f64>> = Vec::with_capacity(k);
let mut alpha = Vec::with_capacity(k);
let mut beta_vec = Vec::with_capacity(k);
let mut u0 = vec![0.0f64; m];
u0[0] = 1.0;
u.push(u0.clone());
let mut beta_prev = 1.0f64;
for j in 0..k {
let u_j = &u[j];
let mut r = mat_transpose_vec(u_j);
if j > 0 {
axpy(-beta_prev, &v[j - 1], &mut r);
}
for vk in &v {
let d = dot(&r, vk);
axpy(-d, vk, &mut r);
}
let alpha_j = norm2(&r);
alpha.push(alpha_j);
let vj = if alpha_j > 1e-300 {
let inv = 1.0 / alpha_j;
r.iter().map(|x| x * inv).collect::<Vec<_>>()
} else {
let mut e = vec![0.0f64; n];
for i in 0..n {
e[i] = 1.0;
let _d = dot(&e, &e);
for vk in &v {
let dd = dot(&e, vk);
axpy(-dd, vk, &mut e);
}
let nn = norm2(&e);
if nn > 1e-10 {
scal(1.0 / nn, &mut e);
break;
}
e[i] = 0.0;
}
e
};
v.push(vj.clone());
let mut p = mat_vec(&vj);
axpy(-alpha_j, u_j, &mut p);
for uk in &u {
let d = dot(&p, uk);
axpy(-d, uk, &mut p);
}
let beta_j = norm2(&p);
beta_vec.push(beta_j);
beta_prev = beta_j;
let u_next = if beta_j > 1e-300 {
let inv = 1.0 / beta_j;
p.iter().map(|x| x * inv).collect::<Vec<_>>()
} else {
let mut e = vec![0.0f64; m];
'outer: for i in 0..m {
e = vec![0.0f64; m];
e[i] = 1.0;
for uk in &u {
let d = dot(&e, uk);
axpy(-d, uk, &mut e);
}
let nn = norm2(&e);
if nn > 1e-10 {
scal(1.0 / nn, &mut e);
break 'outer;
}
}
e
};
u.push(u_next);
}
GolubKahanBidiag { u, v, alpha, beta: beta_vec }
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EigenwhichTarget {
LargestAlgebraic,
SmallestAlgebraic,
LargestMagnitude,
SmallestMagnitude,
}
impl EigenwhichTarget {
fn is_better(&self, a: f64, b: f64) -> bool {
match self {
Self::LargestAlgebraic => a > b,
Self::SmallestAlgebraic => a < b,
Self::LargestMagnitude => a.abs() > b.abs(),
Self::SmallestMagnitude => a.abs() < b.abs(),
}
}
}
pub struct ThickRestartLanczos {
pub eigenvalues: Vec<f64>,
pub eigenvectors: Vec<Vec<f64>>,
pub n_converged: usize,
}
pub fn thick_restart_lanczos<F>(
mat_vec: F,
n: usize,
k: usize,
krylov: usize,
max_restarts: usize,
tol: f64,
which: EigenwhichTarget,
) -> ThickRestartLanczos
where
F: Fn(&[f64]) -> Vec<f64>,
{
let k = k.min(n);
let krylov = krylov.min(n).max(k + 1);
let mut v: Vec<Vec<f64>> = Vec::with_capacity(krylov + 1);
let mut alpha: Vec<f64> = Vec::with_capacity(krylov);
let mut beta: Vec<f64> = Vec::with_capacity(krylov);
let mut v0 = vec![0.0f64; n];
for i in 0..n {
v0[i] = ((i as f64 + 1.0).sqrt().fract() + 0.1) * if i % 2 == 0 { 1.0 } else { -1.0 };
}
let v0_norm = norm2(&v0);
if v0_norm > 1e-300 {
scal(1.0 / v0_norm, &mut v0);
}
v.push(v0);
let mut n_converged = 0usize;
let mut ritz_vals: Vec<f64> = Vec::new();
let mut ritz_vecs: Vec<Vec<f64>> = Vec::new();
for _restart in 0..=max_restarts {
let start = if _restart == 0 { 0 } else { k };
for j in start..krylov {
if j >= v.len() {
break;
}
let vj = v[j].clone();
let mut w = mat_vec(&vj);
if j > 0 && j <= beta.len() {
axpy(-beta[j - 1], &v[j - 1], &mut w);
}
let alpha_j = dot(&w, &vj);
if j < alpha.len() {
alpha[j] = alpha_j;
} else {
alpha.push(alpha_j);
}
axpy(-alpha_j, &vj, &mut w);
for vk in &v[..j + 1] {
let d = dot(&w, vk);
axpy(-d, vk, &mut w);
}
for vk in &v[..j + 1] {
let d = dot(&w, vk);
axpy(-d, vk, &mut w);
}
let beta_j = norm2(&w);
if j < beta.len() {
beta[j] = beta_j;
} else {
beta.push(beta_j);
}
if beta_j < 1e-12 {
let mut e = vec![0.0f64; n];
for i in 0..n {
e[i] = 1.0;
for vk in v.iter().take(j + 1) {
let d = dot(&e, vk);
axpy(-d, vk, &mut e);
}
let nn = norm2(&e);
if nn > 1e-10 {
scal(1.0 / nn, &mut e);
break;
}
e[i] = 0.0;
}
if j + 1 < v.len() {
v[j + 1] = e;
} else {
v.push(e);
}
break;
} else {
let inv_beta = 1.0 / beta_j;
let v_next: Vec<f64> = w.iter().map(|x| x * inv_beta).collect();
if j + 1 < v.len() {
v[j + 1] = v_next;
} else {
v.push(v_next);
}
}
}
let current_m = alpha.len().min(krylov);
if current_m == 0 {
break;
}
let t_alpha = alpha[..current_m].to_vec();
let t_beta = if beta.len() >= current_m {
beta[..current_m - 1].to_vec()
} else {
beta.clone()
};
let (t_evals, t_evecs) = tridiagonal_eigen_dc(&t_alpha, &t_beta);
let mut order: Vec<usize> = (0..t_evals.len()).collect();
order.sort_unstable_by(|&a, &b| {
let ea = t_evals[a];
let eb = t_evals[b];
if which.is_better(ea, eb) {
std::cmp::Ordering::Less
} else {
std::cmp::Ordering::Greater
}
});
let num_keep = k.min(order.len());
let kept_idx: Vec<usize> = order[..num_keep].to_vec();
let mut new_ritz_vals: Vec<f64> = Vec::with_capacity(num_keep);
let mut new_ritz_vecs: Vec<Vec<f64>> = Vec::with_capacity(num_keep);
let mut residuals: Vec<f64> = Vec::with_capacity(num_keep);
for &ti in &kept_idx {
let ritz_val = t_evals[ti];
let mut x = vec![0.0f64; n];
let y = &t_evecs[ti];
for (j, &y_j) in y.iter().enumerate() {
if j < v.len() {
axpy(y_j, &v[j], &mut x);
}
}
let xnorm = norm2(&x);
if xnorm > 1e-300 {
scal(1.0 / xnorm, &mut x);
}
let ax = mat_vec(&x);
let mut res_vec = ax.clone();
axpy(-ritz_val, &x, &mut res_vec);
let res_norm = norm2(&res_vec);
new_ritz_vals.push(ritz_val);
new_ritz_vecs.push(x);
residuals.push(res_norm);
}
n_converged = residuals.iter().filter(|&&r| r < tol).count();
ritz_vals = new_ritz_vals;
ritz_vecs = new_ritz_vecs;
if n_converged >= k {
break;
}
v.clear();
alpha.clear();
beta.clear();
for (i, rvec) in ritz_vecs.iter().enumerate().take(num_keep) {
v.push(rvec.clone());
alpha.push(ritz_vals[i]);
}
if let Some(&last_beta) = residuals.last() {
if num_keep > 0 {
beta.resize(num_keep - 1, 0.0);
beta.push(last_beta);
}
}
if let Some(last_ritz) = ritz_vecs.last() {
let ax = mat_vec(last_ritz);
let mut w = ax;
axpy(-ritz_vals[ritz_vals.len() - 1], last_ritz, &mut w);
for vk in &v {
let d = dot(&w, vk);
axpy(-d, vk, &mut w);
}
let wn = norm2(&w);
if wn > 1e-12 {
scal(1.0 / wn, &mut w);
v.push(w);
} else {
let mut e = vec![0.0f64; n];
for i in 0..n {
e[i] = 1.0;
for vk in &v {
let d = dot(&e, vk);
axpy(-d, vk, &mut e);
}
let nn = norm2(&e);
if nn > 1e-10 {
scal(1.0 / nn, &mut e);
break;
}
e[i] = 0.0;
}
v.push(e);
}
}
}
ThickRestartLanczos {
eigenvalues: ritz_vals,
eigenvectors: ritz_vecs,
n_converged,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_matrix_4x4() -> Vec<Vec<f64>> {
let n = 4;
let mut a = vec![vec![0.0f64; n]; n];
for i in 0..n {
a[i][i] = 4.0;
if i + 1 < n {
a[i][i + 1] = -1.0;
a[i + 1][i] = -1.0;
}
}
a
}
#[test]
fn test_jacobi_eigen_sym_4x4() {
let mut a = test_matrix_4x4();
let n = 4;
let (evals, evecs) = jacobi_eigen_sym(&mut a, n, 100, 1e-12);
assert_eq!(evals.len(), n);
assert_eq!(evecs.len(), n);
for &ev in &evals {
assert!(ev > 0.0, "eigenvalue should be positive, got {ev}");
}
for i in 0..n {
for j in 0..n {
let d = dot(&evecs[i], &evecs[j]);
if i == j {
assert!((d - 1.0).abs() < 1e-10, "evec[{i}] not normalized: {d}");
} else {
assert!(d.abs() < 1e-10, "evec[{i}] and evec[{j}] not orthogonal: {d}");
}
}
}
}
#[test]
fn test_jacobi_1x1() {
let mut a = vec![vec![7.0f64]];
let (evals, evecs) = jacobi_eigen_sym(&mut a, 1, 10, 1e-12);
assert_eq!(evals.len(), 1);
assert!((evals[0] - 7.0).abs() < 1e-12);
assert!((evecs[0][0].abs() - 1.0).abs() < 1e-12);
}
#[test]
fn test_tridiagonal_eigen_dc_4x4() {
let diag = vec![4.0, 4.0, 4.0, 4.0];
let off = vec![-1.0, -1.0, -1.0];
let (evals, evecs) = tridiagonal_eigen_dc(&diag, &off);
assert_eq!(evals.len(), 4);
for (i, &ev) in evals.iter().enumerate() {
assert!(ev > 0.0, "evals[{i}]={ev}");
}
for i in 0..evals.len() - 1 {
assert!(evals[i] <= evals[i + 1] + 1e-10, "not sorted");
}
let n = 4;
for i in 0..n {
for j in 0..n {
let d = dot(&evecs[i], &evecs[j]);
if i == j {
assert!((d - 1.0).abs() < 1e-8, "evec[{i}] norm^2={d}");
} else {
assert!(d.abs() < 1e-8, "evec[{i}].evec[{j}]={d}");
}
}
}
}
#[test]
fn test_tridiagonal_eigen_dc_1() {
let (evals, evecs) = tridiagonal_eigen_dc(&[3.0], &[]);
assert_eq!(evals.len(), 1);
assert!((evals[0] - 3.0).abs() < 1e-12);
assert_eq!(evecs.len(), 1);
}
#[test]
fn test_golub_kahan_bidiag() {
let mat_vals: Vec<Vec<f64>> = vec![
vec![2.0, 0.0, 0.0],
vec![0.0, 3.0, 0.0],
vec![0.0, 0.0, 4.0],
vec![0.0, 0.0, 0.0],
];
let m = 4;
let n = 3;
let k = 3;
let mv = |x: &[f64]| -> Vec<f64> {
let mut y = vec![0.0f64; m];
for i in 0..m {
for j in 0..n {
y[i] += mat_vals[i][j] * x[j];
}
}
y
};
let mtv = |y: &[f64]| -> Vec<f64> {
let mut x = vec![0.0f64; n];
for i in 0..m {
for j in 0..n {
x[j] += mat_vals[i][j] * y[i];
}
}
x
};
let result = golub_kahan_bidiag(mv, mtv, m, n, k);
assert_eq!(result.alpha.len(), k);
assert_eq!(result.beta.len(), k);
assert_eq!(result.v.len(), k);
assert_eq!(result.u.len(), k + 1);
for &a in &result.alpha {
assert!(a >= 0.0, "alpha={a}");
}
}
#[test]
fn test_thick_restart_lanczos_finds_eigenvalues() {
let n = 5usize;
let diags = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let mat_vec = move |x: &[f64]| -> Vec<f64> {
x.iter().zip(diags.iter()).map(|(xi, di)| xi * di).collect()
};
let result = thick_restart_lanczos(mat_vec, n, 3, 5, 20, 1e-10, EigenwhichTarget::LargestAlgebraic);
assert_eq!(result.eigenvalues.len(), 3);
assert_eq!(result.eigenvectors.len(), 3);
let mut found_evals = result.eigenvalues.clone();
found_evals.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
assert!(
(found_evals[0] - 5.0).abs() < 0.5,
"largest eigenvalue {}, expected ~5.0",
found_evals[0]
);
}
#[test]
fn test_eigenwhich_target() {
assert!(EigenwhichTarget::LargestAlgebraic.is_better(5.0, 3.0));
assert!(EigenwhichTarget::SmallestAlgebraic.is_better(-1.0, 2.0));
assert!(EigenwhichTarget::LargestMagnitude.is_better(-5.0, 3.0));
assert!(EigenwhichTarget::SmallestMagnitude.is_better(0.1, 5.0));
}
}