use crate::error::{LinalgError, LinalgResult};
use scirs2_core::ndarray::{s, Array1, Array2, ArrayView2};
#[derive(Debug, Clone)]
pub struct LanczosSvdConfig {
pub k: usize,
pub max_iter: usize,
pub tol: f64,
}
impl Default for LanczosSvdConfig {
fn default() -> Self {
Self {
k: 10,
max_iter: 50,
tol: 1e-10,
}
}
}
fn vec_norm(v: &[f64]) -> f64 {
v.iter().map(|x| x * x).sum::<f64>().sqrt()
}
fn dot(a: &[f64], b: &[f64]) -> f64 {
a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
}
fn matvec(a: &Array2<f64>, x: &[f64]) -> Vec<f64> {
let m = a.nrows();
let n = a.ncols();
let mut y = vec![0.0f64; m];
for i in 0..m {
let mut s = 0.0f64;
for j in 0..n {
s += a[[i, j]] * x[j];
}
y[i] = s;
}
y
}
fn matvec_t(a: &Array2<f64>, x: &[f64]) -> Vec<f64> {
let m = a.nrows();
let n = a.ncols();
let mut y = vec![0.0f64; n];
for j in 0..n {
let mut s = 0.0f64;
for i in 0..m {
s += a[[i, j]] * x[i];
}
y[j] = s;
}
y
}
fn reorthogonalize(v: &mut Vec<f64>, basis: &[Vec<f64>]) -> f64 {
for b in basis {
let c = dot(v, b);
for (vi, bi) in v.iter_mut().zip(b.iter()) {
*vi -= c * bi;
}
}
vec_norm(v)
}
fn normalize_inplace(v: &mut Vec<f64>) -> f64 {
let n = vec_norm(v);
if n > f64::EPSILON {
for vi in v.iter_mut() {
*vi /= n;
}
}
n
}
fn bidiag_svd(alpha: &[f64], beta: &[f64]) -> (Array2<f64>, Vec<f64>, Array2<f64>) {
let k = alpha.len();
assert!(beta.len() == k.saturating_sub(1) || beta.len() == k);
let mut b = Array2::<f64>::zeros((k, k));
for i in 0..k {
b[[i, i]] = alpha[i];
if i + 1 < k && i < beta.len() {
b[[i, i + 1]] = beta[i];
}
}
let mut t = Array2::<f64>::zeros((k, k));
for i in 0..k {
t[[i, i]] = alpha[i] * alpha[i];
if i > 0 && i - 1 < beta.len() {
t[[i, i]] += beta[i - 1] * beta[i - 1];
}
if i + 1 < k && i < beta.len() {
let off = alpha[i] * beta[i];
t[[i, i + 1]] = off;
t[[i + 1, i]] = off;
}
}
let (eigvals, eigvecs) = symmetric_jacobi(&t);
let mut triplets: Vec<(f64, usize)> = eigvals
.iter()
.enumerate()
.map(|(i, &lam)| (lam.max(0.0).sqrt(), i))
.collect();
triplets.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
let sigma: Vec<f64> = triplets.iter().map(|&(s, _)| s).collect();
let order: Vec<usize> = triplets.iter().map(|&(_, i)| i).collect();
let mut v_b = Array2::<f64>::zeros((k, k));
for (col_new, &col_old) in order.iter().enumerate() {
for row in 0..k {
v_b[[row, col_new]] = eigvecs[[row, col_old]];
}
}
let mut u_b = Array2::<f64>::zeros((k, k));
for (j, &s) in sigma.iter().enumerate() {
if s > f64::EPSILON {
let v_col: Vec<f64> = (0..k).map(|r| v_b[[r, j]]).collect();
for i in 0..k {
let mut sum = b[[i, i]] * v_col[i];
if i > 0 {
sum += b[[i - 1, i]] * v_col[i - 1];
}
if i + 1 < k {
sum += b[[i, i + 1]] * v_col[i + 1];
}
u_b[[i, j]] = sum / s;
}
} else {
u_b[[j, j]] = 1.0;
}
}
(u_b, sigma, v_b)
}
fn symmetric_jacobi(a: &Array2<f64>) -> (Vec<f64>, Array2<f64>) {
let n = a.nrows();
let mut d = a.to_owned();
let mut v = Array2::<f64>::eye(n);
let max_iter = 100 * n * n;
for _ in 0..max_iter {
let mut max_val = 0.0f64;
let mut p = 0usize;
let mut q = 1usize;
for i in 0..n {
for j in (i + 1)..n {
let val = d[[i, j]].abs();
if val > max_val {
max_val = val;
p = i;
q = j;
}
}
}
if max_val < 1e-14 {
break;
}
let theta = (d[[q, q]] - d[[p, p]]) / (2.0 * d[[p, q]]);
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 d_pp = d[[p, p]];
let d_qq = d[[q, q]];
let d_pq = d[[p, q]];
d[[p, p]] = c * c * d_pp - 2.0 * s * c * d_pq + s * s * d_qq;
d[[q, q]] = s * s * d_pp + 2.0 * s * c * d_pq + c * c * d_qq;
d[[p, q]] = 0.0;
d[[q, p]] = 0.0;
for r in 0..n {
if r != p && r != q {
let d_rp = d[[r, p]];
let d_rq = d[[r, q]];
d[[r, p]] = c * d_rp - s * d_rq;
d[[p, r]] = d[[r, p]];
d[[r, q]] = s * d_rp + c * d_rq;
d[[q, r]] = d[[r, q]];
}
}
for r in 0..n {
let v_rp = v[[r, p]];
let v_rq = v[[r, q]];
v[[r, p]] = c * v_rp - s * v_rq;
v[[r, q]] = s * v_rp + c * v_rq;
}
}
let eigenvalues: Vec<f64> = (0..n).map(|i| d[[i, i]]).collect();
(eigenvalues, v)
}
pub fn lanczos_bidiagonalization(
a: &Array2<f64>,
k: usize,
) -> LinalgResult<(Array2<f64>, Array2<f64>, Array2<f64>)> {
let m = a.nrows();
let n = a.ncols();
if m == 0 || n == 0 {
return Err(LinalgError::ValueError(
"lanczos_bidiagonalization: matrix must be non-empty".to_string(),
));
}
let k_eff = k.min(m).min(n);
if k_eff == 0 {
return Err(LinalgError::ValueError(
"lanczos_bidiagonalization: k must be >= 1".to_string(),
));
}
let mut u_vecs: Vec<Vec<f64>> = Vec::with_capacity(k_eff);
let mut v_vecs: Vec<Vec<f64>> = Vec::with_capacity(k_eff);
let mut alpha = Vec::with_capacity(k_eff);
let mut beta = Vec::with_capacity(k_eff);
let mut v_curr: Vec<f64> = (0..n).map(|i| ((i + 1) as f64 * 0.31415926).sin()).collect();
let nrm = normalize_inplace(&mut v_curr);
if nrm < f64::EPSILON {
return Err(LinalgError::ComputationError(
"lanczos_bidiagonalization: initial vector is zero".to_string(),
));
}
let mut u_curr: Vec<f64>;
let mut beta_prev = 0.0f64;
for j in 0..k_eff {
u_curr = matvec(a, &v_curr);
if j > 0 {
let u_prev = &u_vecs[j - 1];
for (ui, &up) in u_curr.iter_mut().zip(u_prev.iter()) {
*ui -= beta_prev * up;
}
}
reorthogonalize(&mut u_curr, &u_vecs);
let alpha_j = normalize_inplace(&mut u_curr);
alpha.push(alpha_j);
u_vecs.push(u_curr.clone());
if j + 1 >= k_eff {
v_vecs.push(v_curr.clone());
break;
}
let mut v_next = matvec_t(a, &u_curr);
for (vi, &vc) in v_next.iter_mut().zip(v_curr.iter()) {
*vi -= alpha_j * vc;
}
reorthogonalize(&mut v_next, &v_vecs);
let beta_j = normalize_inplace(&mut v_next);
beta.push(beta_j);
beta_prev = beta_j;
v_vecs.push(v_curr.clone());
v_curr = v_next;
}
let k_actual = alpha.len();
let mut u_mat = Array2::<f64>::zeros((m, k_actual));
for (j, uv) in u_vecs.iter().enumerate() {
for i in 0..m {
u_mat[[i, j]] = uv[i];
}
}
let v_actual = v_vecs.len().min(k_actual);
let mut v_mat = Array2::<f64>::zeros((n, v_actual));
for (j, vv) in v_vecs.iter().take(v_actual).enumerate() {
for i in 0..n {
v_mat[[i, j]] = vv[i];
}
}
let mut b_mat = Array2::<f64>::zeros((k_actual, k_actual));
for i in 0..k_actual {
b_mat[[i, i]] = alpha[i];
}
for i in 0..beta.len().min(k_actual - 1) {
b_mat[[i, i + 1]] = beta[i];
}
Ok((u_mat, b_mat, v_mat))
}
pub fn distributed_svd_simulate(
a: &Array2<f64>,
k: usize,
) -> LinalgResult<(Array2<f64>, Vec<f64>, Array2<f64>)> {
let m = a.nrows();
let n = a.ncols();
if k == 0 {
return Err(LinalgError::ValueError(
"distributed_svd_simulate: k must be >= 1".to_string(),
));
}
let k_eff = k.min(m).min(n);
let (u_lanczos, b_mat, v_lanczos) = lanczos_bidiagonalization(a, k_eff)?;
let k_actual = u_lanczos.ncols();
let mut alpha_b: Vec<f64> = (0..k_actual).map(|i| b_mat[[i, i]]).collect();
let mut beta_b: Vec<f64> = (0..k_actual.saturating_sub(1))
.map(|i| b_mat[[i, i + 1]])
.collect();
let (u_b, sigma, v_b) = bidiag_svd(&alpha_b, &beta_b);
let k_out = k_eff.min(k_actual).min(sigma.len());
let mut u_k = Array2::<f64>::zeros((m, k_out));
for j in 0..k_out {
for i in 0..m {
let mut s = 0.0f64;
for l in 0..k_actual {
s += u_lanczos[[i, l]] * u_b[[l, j]];
}
u_k[[i, j]] = s;
}
}
let v_cols = v_lanczos.ncols();
let mut v_k = Array2::<f64>::zeros((n, k_out));
for j in 0..k_out {
for i in 0..n {
let mut s = 0.0f64;
for l in 0..v_cols.min(k_actual) {
s += v_lanczos[[i, l]] * v_b[[l, j]];
}
v_k[[i, j]] = s;
}
}
let sigma_k = sigma[..k_out].to_vec();
Ok((u_k, sigma_k, v_k))
}
pub fn thick_restart_lanczos(
a: &Array2<f64>,
k: usize,
tol: f64,
) -> LinalgResult<(Array2<f64>, Vec<f64>, Array2<f64>)> {
let m = a.nrows();
let n = a.ncols();
if k == 0 {
return Err(LinalgError::ValueError(
"thick_restart_lanczos: k must be >= 1".to_string(),
));
}
let k_eff = k.min(m).min(n);
let max_cycles = 10usize;
let lanczos_size = (k_eff + 5).min(m).min(n);
let mut prev_sigma: Option<Vec<f64>> = None;
let mut best_u: Array2<f64> = Array2::<f64>::zeros((m, k_eff));
let mut best_sigma: Vec<f64> = vec![0.0; k_eff];
let mut best_v: Array2<f64> = Array2::<f64>::zeros((n, k_eff));
for _cycle in 0..max_cycles {
let (u_k, sigma_k, v_k) = distributed_svd_simulate(a, lanczos_size)?;
let k_got = sigma_k.len().min(k_eff);
best_u = u_k.slice(s![.., ..k_got]).to_owned();
best_sigma = sigma_k[..k_got].to_vec();
best_v = v_k.slice(s![.., ..k_got]).to_owned();
let converged = if let Some(ref prev) = prev_sigma {
let sigma_1 = best_sigma.first().copied().unwrap_or(1.0).max(1e-14);
prev.iter()
.zip(best_sigma.iter())
.all(|(&s_prev, &s_curr)| (s_prev - s_curr).abs() < tol * sigma_1)
} else {
false
};
if converged {
break;
}
prev_sigma = Some(best_sigma.clone());
}
Ok((best_u, best_sigma, best_v))
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
fn frob_diff(a: &Array2<f64>, b: &Array2<f64>) -> f64 {
a.iter().zip(b.iter()).map(|(x, y)| (x - y) * (x - y)).sum::<f64>().sqrt()
}
fn eye(n: usize) -> Array2<f64> {
Array2::<f64>::eye(n)
}
fn orthogonality_error(u: &Array2<f64>) -> f64 {
let k = u.ncols();
let m = u.nrows();
let mut max_err = 0.0f64;
for i in 0..k {
for j in 0..k {
let mut dot = 0.0f64;
for r in 0..m {
dot += u[[r, i]] * u[[r, j]];
}
let expected = if i == j { 1.0 } else { 0.0 };
max_err = max_err.max((dot - expected).abs());
}
}
max_err
}
#[test]
fn test_lanczos_bidiag_is_bidiagonal() {
let a = Array2::<f64>::from_shape_fn((6, 5), |(i, j)| {
(i as f64 * 1.3 + j as f64 * 0.7 + 0.1).sin()
});
let k = 4;
let (u_mat, b_mat, v_mat) = lanczos_bidiagonalization(&a, k).expect("lanczos failed");
let k_actual = b_mat.nrows();
for i in 0..k_actual {
for j in 0..k_actual {
let val = b_mat[[i, j]];
if i != j && !(i + 1 == j) {
assert!(
val.abs() < 1e-12,
"B[{i},{j}] = {val} should be ≈ 0"
);
}
}
}
}
#[test]
fn test_lanczos_bidiag_u_orthonormal() {
let a = Array2::<f64>::from_shape_fn((8, 6), |(i, j)| {
((i + 1) as f64) / ((j + 2) as f64)
});
let (u_mat, _, _) = lanczos_bidiagonalization(&a, 5).expect("lanczos failed");
let err = orthogonality_error(&u_mat);
assert!(err < 1e-10, "U^T U orthogonality error = {err}");
}
#[test]
fn test_lanczos_bidiag_v_orthonormal() {
let a = Array2::<f64>::from_shape_fn((8, 6), |(i, j)| {
((i + 1) as f64) / ((j + 2) as f64)
});
let (_, _, v_mat) = lanczos_bidiagonalization(&a, 5).expect("lanczos failed");
let err = orthogonality_error(&v_mat);
assert!(err < 1e-10, "V^T V orthogonality error = {err}");
}
#[test]
fn test_distributed_svd_singular_values_match_reference() {
let diag = vec![5.0, 3.0, 2.0, 1.0];
let a = Array2::<f64>::from_shape_fn((4, 4), |(i, j)| if i == j { diag[i] } else { 0.0 });
let (_, sigma, _) = distributed_svd_simulate(&a, 4).expect("svd failed");
let mut sigma_sorted = sigma.clone();
sigma_sorted.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
assert_abs_diff_eq!(sigma_sorted[0], 5.0, epsilon = 1e-6);
assert_abs_diff_eq!(sigma_sorted[1], 3.0, epsilon = 1e-6);
assert_abs_diff_eq!(sigma_sorted[2], 2.0, epsilon = 1e-6);
assert_abs_diff_eq!(sigma_sorted[3], 1.0, epsilon = 1e-6);
}
#[test]
fn test_distributed_svd_rank1_matrix() {
let u0: Vec<f64> = vec![1.0, 0.0, 0.0, 0.0];
let v0: Vec<f64> = vec![0.0, 1.0, 0.0, 0.0];
let sigma0 = 7.0;
let a = Array2::<f64>::from_shape_fn((4, 4), |(i, j)| u0[i] * sigma0 * v0[j]);
let (_, sigma, _) = distributed_svd_simulate(&a, 1).expect("svd failed");
assert!(
(sigma[0] - sigma0).abs() < 1e-5,
"Expected sigma[0] ≈ {sigma0}, got {}",
sigma[0]
);
}
#[test]
fn test_distributed_svd_singular_vectors_orthonormal() {
let a = Array2::<f64>::from_shape_fn((6, 5), |(i, j)| {
(i as f64 + 1.0) * (j as f64 + 1.0) / 10.0
});
let k = 3;
let (u_k, _, v_k) = distributed_svd_simulate(&a, k).expect("svd failed");
let err_u = orthogonality_error(&u_k);
let err_v = orthogonality_error(&v_k);
assert!(err_u < 1e-9, "U_k orthogonality error = {err_u}");
assert!(err_v < 1e-9, "V_k orthogonality error = {err_v}");
}
#[test]
fn test_distributed_svd_reconstruction_error() {
let a = Array2::<f64>::from_shape_fn((5, 4), |(i, j)| {
if i < 2 && j < 2 { (i + 1) as f64 * (j + 2) as f64 } else { 0.0 }
});
let (u_k, sigma_k, v_k) = distributed_svd_simulate(&a, 2).expect("svd failed");
let mut a_rec = Array2::<f64>::zeros((5, 4));
for r in 0..u_k.ncols() {
for i in 0..5 {
for j in 0..4 {
a_rec[[i, j]] += u_k[[i, r]] * sigma_k[r] * v_k[[j, r]];
}
}
}
let err = frob_diff(&a_rec, &a);
assert!(err < 1e-7, "Reconstruction error = {err}");
}
#[test]
fn test_thick_restart_converges() {
let diag = vec![10.0, 7.0, 4.0, 1.0];
let a = Array2::<f64>::from_shape_fn((4, 4), |(i, j)| if i == j { diag[i] } else { 0.0 });
let (_, sigma, _) = thick_restart_lanczos(&a, 3, 1e-8).expect("thick restart failed");
assert!(sigma.len() >= 1);
let max_sv = sigma.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
assert!(max_sv > 8.0, "Expected largest singular value near 10, got {max_sv}");
}
#[test]
fn test_bidiag_svd_diagonal_matrix() {
let alpha = vec![3.0f64, 2.0, 1.0];
let beta: Vec<f64> = vec![];
let (_, sigma, _) = bidiag_svd(&alpha, &beta);
let mut s = sigma.clone();
s.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
assert_abs_diff_eq!(s[0], 3.0, epsilon = 1e-10);
assert_abs_diff_eq!(s[1], 2.0, epsilon = 1e-10);
assert_abs_diff_eq!(s[2], 1.0, epsilon = 1e-10);
}
}