use crate::error::{SparseError, SparseResult};
fn transpose(a: &[f64], m: usize, n: usize) -> Vec<f64> {
let mut at = vec![0.0_f64; m * n];
for i in 0..m {
for j in 0..n {
at[j * m + i] = a[i * n + j];
}
}
at
}
fn matmul(a: &[f64], b: &[f64], m: usize, k: usize, n: usize) -> Vec<f64> {
let mut c = vec![0.0_f64; m * n];
for i in 0..m {
for p in 0..k {
let a_ip = a[i * k + p];
if a_ip == 0.0 {
continue;
}
for j in 0..n {
c[i * n + j] += a_ip * b[p * n + j];
}
}
}
c
}
fn dot(a: &[f64], b: &[f64]) -> f64 {
a.iter().zip(b.iter()).map(|(&x, &y)| x * y).sum()
}
fn norm2(v: &[f64]) -> f64 {
dot(v, v).sqrt()
}
fn rank1_subtract(m_mat: &mut [f64], u: &[f64], v: &[f64], rows: usize, cols: usize) {
for i in 0..rows {
for j in 0..cols {
m_mat[i * cols + j] -= u[i] * v[j];
}
}
}
fn jacobi_svd_thin(a: &[f64], m: usize, n: usize) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
let at = transpose(a, m, n);
let mut b = matmul(&at, a, n, m, n);
let mut v = vec![0.0_f64; n * n];
for i in 0..n {
v[i * n + i] = 1.0;
}
const MAX_ITER: usize = 100;
for _iter in 0..MAX_ITER {
let mut converged = true;
for p in 0..n {
for q in (p + 1)..n {
let b_pq = b[p * n + q];
let b_pp = b[p * n + p];
let b_qq = b[q * n + q];
let off_sq = b_pq * b_pq;
if off_sq < 1e-28 * (b_pp * b_qq).abs().max(1e-100) {
continue;
}
converged = false;
let theta = 0.5 * (b_qq - b_pp) / b_pq;
let t = if theta >= 0.0 {
1.0 / (theta + (theta * theta + 1.0).sqrt())
} else {
1.0 / (theta - (theta * theta + 1.0).sqrt())
};
let c = 1.0 / (1.0 + t * t).sqrt();
let s = t * c;
let mut bp = vec![0.0_f64; n];
let mut bq = vec![0.0_f64; n];
for r in 0..n {
bp[r] = c * b[r * n + p] - s * b[r * n + q];
bq[r] = s * b[r * n + p] + c * b[r * n + q];
}
for r in 0..n {
b[r * n + p] = bp[r];
b[r * n + q] = bq[r];
}
let mut bpr = vec![0.0_f64; n];
let mut bqr = vec![0.0_f64; n];
for r in 0..n {
bpr[r] = c * b[p * n + r] - s * b[q * n + r];
bqr[r] = s * b[p * n + r] + c * b[q * n + r];
}
for r in 0..n {
b[p * n + r] = bpr[r];
b[q * n + r] = bqr[r];
}
let mut vp = vec![0.0_f64; n];
let mut vq = vec![0.0_f64; n];
for r in 0..n {
vp[r] = c * v[r * n + p] - s * v[r * n + q];
vq[r] = s * v[r * n + p] + c * v[r * n + q];
}
for r in 0..n {
v[r * n + p] = vp[r];
v[r * n + q] = vq[r];
}
}
}
if converged {
break;
}
}
let mut sigma: Vec<f64> = (0..n).map(|i| b[i * n + i].max(0.0).sqrt()).collect();
let mut order: Vec<usize> = (0..n).collect();
order.sort_by(|&a, &b| {
sigma[b]
.partial_cmp(&sigma[a])
.unwrap_or(std::cmp::Ordering::Equal)
});
let sigma_sorted: Vec<f64> = order.iter().map(|&i| sigma[i]).collect();
let v_sorted: Vec<f64> = {
let mut vs = vec![0.0_f64; n * n];
for (new_j, &old_j) in order.iter().enumerate() {
for r in 0..n {
vs[r * n + new_j] = v[r * n + old_j];
}
}
vs
};
sigma = sigma_sorted;
let av = matmul(a, &v_sorted, m, n, n);
let mut u = vec![0.0_f64; m * n];
for j in 0..n {
let s = sigma[j];
if s > 0.0 {
for i in 0..m {
u[i * n + j] = av[i * n + j] / s;
}
}
}
let vt = transpose(&v_sorted, n, n);
(u, sigma, vt)
}
pub fn truncated_svd(
a: &[f64],
m: usize,
n: usize,
tol: f64,
) -> SparseResult<(Vec<f64>, Vec<f64>, Vec<f64>, usize)> {
if m == 0 || n == 0 {
return Err(SparseError::ValueError(
"truncated_svd: dimensions must be > 0".to_string(),
));
}
if tol < 0.0 {
return Err(SparseError::ValueError(
"truncated_svd: tolerance must be >= 0".to_string(),
));
}
if a.len() != m * n {
return Err(SparseError::ValueError(format!(
"truncated_svd: a.len()={} != m*n={}",
a.len(),
m * n
)));
}
let k = m.min(n);
let (u_full, sigma, vt_full) = if m >= n {
jacobi_svd_thin(a, m, n)
} else {
let at = transpose(a, m, n);
let (u_t, s_t, vt_t) = jacobi_svd_thin(&at, n, m);
let u_a = transpose(&vt_t, m, m); let vt_a = transpose(&u_t, n, n); (u_a, s_t, vt_a)
};
let sigma_sq_total: f64 = sigma.iter().map(|&s| s * s).sum();
let mut rank = k;
if tol > 0.0 && sigma_sq_total > 0.0 {
let mut tail_sq = 0.0_f64;
for r in (0..k).rev() {
let new_tail = tail_sq + sigma[r] * sigma[r];
if (new_tail / sigma_sq_total).sqrt() <= tol {
tail_sq = new_tail;
rank = r;
} else {
break;
}
}
rank = rank.max(1);
}
let u_r: Vec<f64> = (0..m)
.flat_map(|i| (0..rank).map(move |j| u_full[i * k + j]))
.collect();
let s_r: Vec<f64> = sigma[..rank].to_vec();
let vt_r: Vec<f64> = (0..rank)
.flat_map(|i| (0..n).map(move |j| vt_full[i * n + j]))
.collect();
Ok((u_r, s_r, vt_r, rank))
}
#[derive(Debug, Clone)]
pub struct AcaResult {
pub u: Vec<f64>,
pub v: Vec<f64>,
pub rank: usize,
pub residual_norm: f64,
}
pub fn aca<F>(
m: usize,
n: usize,
entry: F,
tol: f64,
max_rank: usize,
) -> SparseResult<AcaResult>
where
F: Fn(usize, usize) -> f64,
{
if m == 0 || n == 0 {
return Err(SparseError::ValueError(
"aca: dimensions must be > 0".to_string(),
));
}
if tol < 0.0 {
return Err(SparseError::ValueError(
"aca: tolerance must be >= 0".to_string(),
));
}
let max_rank = max_rank.min(m.min(n));
let mut u_cols: Vec<Vec<f64>> = Vec::new(); let mut v_cols: Vec<Vec<f64>> = Vec::new();
let mut used_rows = vec![false; m];
let mut used_cols = vec![false; n];
let mut approx_norm_sq = 0.0_f64;
let mut pivot_row = 0usize;
used_rows[pivot_row] = true;
for _k in 0..max_rank {
let mut r_row = vec![0.0_f64; n];
for j in 0..n {
let mut val = entry(pivot_row, j);
for (uk, vk) in u_cols.iter().zip(v_cols.iter()) {
val -= uk[pivot_row] * vk[j];
}
r_row[j] = val;
}
let pivot_col_opt = (0..n)
.filter(|&j| !used_cols[j])
.max_by(|&a, &b| {
r_row[a]
.abs()
.partial_cmp(&r_row[b].abs())
.unwrap_or(std::cmp::Ordering::Equal)
});
let pivot_col = match pivot_col_opt {
Some(c) => c,
None => break,
};
let pivot_val = r_row[pivot_col];
if pivot_val.abs() < 1e-14 {
break; }
let mut r_col = vec![0.0_f64; m];
for i in 0..m {
let mut val = entry(i, pivot_col);
for (uk, vk) in u_cols.iter().zip(v_cols.iter()) {
val -= uk[i] * vk[pivot_col];
}
r_col[i] = val;
}
let u_new: Vec<f64> = r_col.iter().map(|&x| x / pivot_val).collect();
let v_new = r_row.clone();
let norm_u = norm2(&u_new);
let norm_v = norm2(&v_new);
let update_norm_sq = norm_u * norm_u * norm_v * norm_v;
let mut cross = 0.0_f64;
for (uj, vj) in u_cols.iter().zip(v_cols.iter()) {
cross += dot(uj, &u_new) * dot(vj, &v_new);
}
approx_norm_sq += 2.0 * cross + update_norm_sq;
u_cols.push(u_new);
v_cols.push(v_new);
used_cols[pivot_col] = true;
if approx_norm_sq > 0.0 && update_norm_sq <= tol * tol * approx_norm_sq {
break;
}
let next_row_opt = (0..m).filter(|&i| !used_rows[i]).max_by(|&a, &b| {
let va = u_cols.last().map(|uk| uk[a].abs()).unwrap_or(0.0);
let vb = u_cols.last().map(|uk| uk[b].abs()).unwrap_or(0.0);
va.partial_cmp(&vb).unwrap_or(std::cmp::Ordering::Equal)
});
match next_row_opt {
Some(nr) => {
used_rows[nr] = true;
pivot_row = nr;
}
None => break,
}
}
let rank = u_cols.len();
if rank == 0 {
return Ok(AcaResult {
u: vec![0.0_f64; m],
v: vec![0.0_f64; n],
rank: 1,
residual_norm: 0.0,
});
}
let mut u_flat = vec![0.0_f64; m * rank];
let mut v_flat = vec![0.0_f64; n * rank];
for k in 0..rank {
for i in 0..m {
u_flat[i * rank + k] = u_cols[k][i];
}
for j in 0..n {
v_flat[j * rank + k] = v_cols[k][j];
}
}
let residual_norm = (approx_norm_sq.max(0.0)).sqrt();
Ok(AcaResult {
u: u_flat,
v: v_flat,
rank,
residual_norm,
})
}
pub fn aca_plus<F>(
m: usize,
n: usize,
entry: F,
tol: f64,
max_rank: usize,
) -> SparseResult<AcaResult>
where
F: Fn(usize, usize) -> f64,
{
if m == 0 || n == 0 {
return Err(SparseError::ValueError(
"aca_plus: dimensions must be > 0".to_string(),
));
}
if tol < 0.0 {
return Err(SparseError::ValueError(
"aca_plus: tolerance must be >= 0".to_string(),
));
}
let max_rank = max_rank.min(m.min(n));
let mut r: Vec<f64> = (0..m * n).map(|idx| entry(idx / n, idx % n)).collect();
let mut u_cols: Vec<Vec<f64>> = Vec::new();
let mut v_cols: Vec<Vec<f64>> = Vec::new();
let mut approx_norm_sq = 0.0_f64;
for _k in 0..max_rank {
let (pivot_idx, &pivot_val) = r
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| {
a.abs()
.partial_cmp(&b.abs())
.unwrap_or(std::cmp::Ordering::Equal)
})
.unwrap_or((0, &0.0_f64));
if pivot_val.abs() < 1e-14 {
break;
}
let pi = pivot_idx / n;
let pj = pivot_idx % n;
let r_row: Vec<f64> = (0..n).map(|j| r[pi * n + j]).collect();
let r_col: Vec<f64> = (0..m).map(|i| r[i * n + pj]).collect();
let u_new: Vec<f64> = r_col.iter().map(|&x| x / pivot_val).collect();
let v_new = r_row;
let norm_u = norm2(&u_new);
let norm_v = norm2(&v_new);
let update_norm_sq = norm_u * norm_u * norm_v * norm_v;
let mut cross = 0.0_f64;
for (uj, vj) in u_cols.iter().zip(v_cols.iter()) {
cross += dot(uj, &u_new) * dot(vj, &v_new);
}
approx_norm_sq += 2.0 * cross + update_norm_sq;
rank1_subtract(&mut r, &u_new, &v_new, m, n);
u_cols.push(u_new);
v_cols.push(v_new);
if approx_norm_sq > 0.0 && update_norm_sq <= tol * tol * approx_norm_sq {
break;
}
}
let rank = u_cols.len();
if rank == 0 {
return Ok(AcaResult {
u: vec![0.0_f64; m],
v: vec![0.0_f64; n],
rank: 1,
residual_norm: 0.0,
});
}
let mut u_flat = vec![0.0_f64; m * rank];
let mut v_flat = vec![0.0_f64; n * rank];
for k in 0..rank {
for i in 0..m {
u_flat[i * rank + k] = u_cols[k][i];
}
for j in 0..n {
v_flat[j * rank + k] = v_cols[k][j];
}
}
let residual_norm = approx_norm_sq.max(0.0).sqrt();
Ok(AcaResult {
u: u_flat,
v: v_flat,
rank,
residual_norm,
})
}
pub fn hmatrix_truncate(
u: &[f64],
v: &[f64],
m: usize,
n: usize,
r: usize,
tol: f64,
) -> SparseResult<(Vec<f64>, Vec<f64>, usize)> {
if r == 0 {
return Err(SparseError::ValueError(
"hmatrix_truncate: rank r must be > 0".to_string(),
));
}
let vt = transpose(v, n, r); let dense = matmul(u, &vt, m, r, n);
let (u_svd, sigma, vt_svd, new_rank) = truncated_svd(&dense, m, n, tol)?;
let mut u_new = vec![0.0_f64; m * new_rank];
let mut v_new = vec![0.0_f64; n * new_rank];
for k in 0..new_rank {
let sq = sigma[k].sqrt();
for i in 0..m {
u_new[i * new_rank + k] = u_svd[i * new_rank + k] * sq;
}
for j in 0..n {
v_new[j * new_rank + k] = vt_svd[k * n + j] * sq;
}
}
Ok((u_new, v_new, new_rank))
}
pub use self::{matmul, norm2, transpose};
#[cfg(test)]
mod tests {
use super::*;
fn frobenius_diff(a: &[f64], b: &[f64]) -> f64 {
a.iter()
.zip(b.iter())
.map(|(&x, &y)| (x - y) * (x - y))
.sum::<f64>()
.sqrt()
}
fn reconstruct_lr(u: &[f64], v: &[f64], m: usize, n: usize, r: usize) -> Vec<f64> {
let vt = transpose(v, n, r);
matmul(u, &vt, m, r, n)
}
fn rank2_matrix(m: usize, n: usize) -> Vec<f64> {
let mut a = vec![0.0_f64; m * n];
for i in 0..m {
for j in 0..n {
let u1 = (i + 1) as f64;
let v1 = (j + 1) as f64;
let u2 = ((i as f64 + 0.5) * std::f64::consts::PI).sin();
let v2 = ((j as f64 + 0.5) * std::f64::consts::PI).cos();
a[i * n + j] = u1 * v1 + u2 * v2;
}
}
a
}
#[test]
fn test_truncated_svd_rank2() {
let m = 8;
let n = 6;
let a = rank2_matrix(m, n);
let a_norm = norm2(&a);
let (u, sigma, vt, rank) = truncated_svd(&a, m, n, 0.0).expect("svd ok");
assert!(rank >= 2, "rank={}", rank);
assert_eq!(sigma.len(), rank);
let vt_full = {
let k_dim = rank;
let mut vt_s = vec![0.0_f64; k_dim * n];
for k in 0..k_dim {
for j in 0..n {
vt_s[k * n + j] = sigma[k] * vt[k * n + j];
}
}
vt_s
};
let recon = matmul(&u, &vt_full, m, rank, n);
let err = frobenius_diff(&a, &recon);
assert!(
err < 1e-8 * a_norm,
"reconstruction error too large: {} (norm={})",
err,
a_norm
);
}
#[test]
fn test_truncated_svd_tolerance() {
let m = 8;
let n = 6;
let a = rank2_matrix(m, n);
let (u, sigma, vt, rank) = truncated_svd(&a, m, n, 1e-6).expect("svd ok");
assert!(rank <= 6, "rank={}", rank);
let vt_full = {
let mut vt_s = vec![0.0_f64; rank * n];
for k in 0..rank {
for j in 0..n {
vt_s[k * n + j] = sigma[k] * vt[k * n + j];
}
}
vt_s
};
let recon = matmul(&u, &vt_full, m, rank, n);
let err = frobenius_diff(&a, &recon);
let a_norm = norm2(&a);
assert!(
err < 1e-5 * a_norm,
"tolerance truncation error too large: {} (norm={})",
err,
a_norm
);
}
#[test]
fn test_aca_rank1() {
let m = 6;
let n = 8;
let u_vec: Vec<f64> = (0..m).map(|i| (i + 1) as f64).collect();
let v_vec: Vec<f64> = (0..n).map(|j| (j + 1) as f64).collect();
let entry = |i: usize, j: usize| u_vec[i] * v_vec[j];
let res = aca(m, n, entry, 1e-8, 10).expect("aca ok");
let recon = reconstruct_lr(&res.u, &res.v, m, n, res.rank);
let expected: Vec<f64> = (0..m)
.flat_map(|i| (0..n).map(move |j| u_vec[i] * v_vec[j]))
.collect();
let err = frobenius_diff(&recon, &expected);
let norm = norm2(&expected);
assert!(
err < 1e-8 * norm,
"ACA rank-1 error too large: {} (norm={})",
err,
norm
);
}
#[test]
fn test_aca_plus_rank1() {
let m = 5;
let n = 7;
let u_vec: Vec<f64> = (0..m).map(|i| (i as f64 + 0.5)).collect();
let v_vec: Vec<f64> = (0..n).map(|j| 1.0 / ((j + 1) as f64)).collect();
let entry = |i: usize, j: usize| u_vec[i] * v_vec[j];
let res = aca_plus(m, n, entry, 1e-8, 10).expect("aca+ ok");
let recon = reconstruct_lr(&res.u, &res.v, m, n, res.rank);
let expected: Vec<f64> = (0..m)
.flat_map(|i| (0..n).map(move |j| u_vec[i] * v_vec[j]))
.collect();
let err = frobenius_diff(&recon, &expected);
let norm = norm2(&expected);
assert!(
err < 1e-8 * norm,
"ACA+ rank-1 error too large: {} (norm={})",
err,
norm
);
}
#[test]
fn test_hmatrix_truncate() {
let m = 6;
let n = 5;
let a = rank2_matrix(m, n);
let (u_svd, sigma, vt_svd, r) = truncated_svd(&a, m, n, 0.0).expect("svd ok");
let mut u = vec![0.0_f64; m * r];
let mut v = vec![0.0_f64; n * r];
for k in 0..r {
let sq = sigma[k].sqrt();
for i in 0..m {
u[i * r + k] = u_svd[i * r + k] * sq;
}
for j in 0..n {
v[j * r + k] = vt_svd[k * n + j] * sq;
}
}
let (u2, v2, r2) = hmatrix_truncate(&u, &v, m, n, r, 1e-6).expect("truncate ok");
assert!(r2 <= r, "truncated rank {} should be <= original {}", r2, r);
let recon = reconstruct_lr(&u2, &v2, m, n, r2);
let a_norm = norm2(&a);
let err = frobenius_diff(&a, &recon);
assert!(
err < 1e-5 * a_norm,
"truncation error too large: {} (norm={})",
err,
a_norm
);
}
#[test]
fn test_truncated_svd_error_cases() {
assert!(truncated_svd(&[], 0, 1, 0.0).is_err());
assert!(truncated_svd(&[], 1, 0, 0.0).is_err());
assert!(truncated_svd(&[1.0, 2.0], 2, 2, 0.0).is_err()); assert!(truncated_svd(&[1.0], 1, 1, -0.1).is_err());
}
#[test]
fn test_aca_error_cases() {
assert!(aca(0, 1, |_, _| 0.0, 0.0, 10).is_err());
assert!(aca(1, 0, |_, _| 0.0, 0.0, 10).is_err());
assert!(aca(1, 1, |_, _| 0.0, -1.0, 10).is_err());
}
}