use crate::error::SolverError;
#[derive(Debug)]
pub struct SvdResult {
pub u: Vec<f32>,
pub sigma: Vec<f32>,
pub vt: Vec<f32>,
pub m: usize,
pub n: usize,
}
#[allow(clippy::cast_precision_loss)]
pub fn svd(a: &[f32], m: usize, n: usize) -> Result<SvdResult, SolverError> {
if a.len() != m * n {
return Err(SolverError::SvdDimensionMismatch { m, n });
}
let min_mn = m.min(n);
let mut work = a.to_vec();
let mut v = vec![0.0f32; n * n];
for i in 0..n {
v[i * n + i] = 1.0;
}
let tol = f32::EPSILON * (m as f32).sqrt();
jacobi_sweeps(&mut work, &mut v, m, n, tol);
let sigma = extract_singular_values(&work, m, n, min_mn);
let u = compute_u_matrix(&work, &sigma, m, n, min_mn);
Ok(assemble_sorted_result(&u, &v, &sigma, m, n, min_mn))
}
fn jacobi_sweeps(work: &mut [f32], v: &mut [f32], m: usize, n: usize, tol: f32) {
let max_sweeps = 100;
for _sweep in 0..max_sweeps {
let mut converged = true;
for p in 0..n {
for q in (p + 1)..n {
if apply_jacobi_rotation(work, v, m, n, p, q, tol) {
converged = false;
}
}
}
if converged {
break;
}
}
}
fn apply_jacobi_rotation(
work: &mut [f32],
v: &mut [f32],
m: usize,
n: usize,
p: usize,
q: usize,
tol: f32,
) -> bool {
let (app, apq, aqq) = gram_elements(work, m, n, p, q);
let scale = (app * aqq).sqrt();
if apq.abs() < f64::from(tol) * scale || scale < f64::EPSILON {
return false;
}
let (c, s) = jacobi_rotation_angle(app, apq, aqq);
rotate_columns(work, m, n, p, q, c, s);
rotate_columns(v, n, n, p, q, c, s);
true
}
fn gram_elements(work: &[f32], m: usize, n: usize, p: usize, q: usize) -> (f64, f64, f64) {
let mut app = 0.0f64;
let mut apq = 0.0f64;
let mut aqq = 0.0f64;
for i in 0..m {
let wp = f64::from(work[i * n + p]);
let wq = f64::from(work[i * n + q]);
app += wp * wp;
apq += wp * wq;
aqq += wq * wq;
}
(app, apq, aqq)
}
fn jacobi_rotation_angle(app: f64, apq: f64, aqq: f64) -> (f64, f64) {
let tau = (aqq - app) / (2.0 * apq);
let t = if tau >= 0.0 {
1.0 / (tau + (1.0 + tau * tau).sqrt())
} else {
-1.0 / (-tau + (1.0 + tau * tau).sqrt())
};
let c = 1.0 / (1.0 + t * t).sqrt();
let s = t * c;
(c, s)
}
fn rotate_columns(mat: &mut [f32], rows: usize, cols: usize, p: usize, q: usize, c: f64, s: f64) {
for i in 0..rows {
let mp = f64::from(mat[i * cols + p]);
let mq = f64::from(mat[i * cols + q]);
mat[i * cols + p] = (c * mp - s * mq) as f32;
mat[i * cols + q] = (s * mp + c * mq) as f32;
}
}
fn extract_singular_values(work: &[f32], m: usize, n: usize, min_mn: usize) -> Vec<f32> {
let mut sigma = vec![0.0f32; min_mn];
for j in 0..min_mn {
let mut norm_sq = 0.0f64;
for i in 0..m {
let val = f64::from(work[i * n + j]);
norm_sq += val * val;
}
sigma[j] = norm_sq.sqrt() as f32;
}
sigma
}
fn compute_u_matrix(work: &[f32], sigma: &[f32], m: usize, n: usize, min_mn: usize) -> Vec<f32> {
let mut u = vec![0.0f32; m * m];
for j in 0..min_mn {
if sigma[j] > f32::EPSILON {
let inv_sigma = 1.0 / sigma[j];
for i in 0..m {
u[i * m + j] = work[i * n + j] * inv_sigma;
}
}
}
for j in min_mn..m {
u[j * m + j] = 1.0;
}
u
}
#[allow(clippy::cast_precision_loss)]
fn assemble_sorted_result(
u: &[f32],
v: &[f32],
sigma: &[f32],
m: usize,
n: usize,
min_mn: usize,
) -> SvdResult {
let mut indices: Vec<usize> = (0..min_mn).collect();
indices.sort_by(|&a, &b| {
sigma[b]
.partial_cmp(&sigma[a])
.unwrap_or(std::cmp::Ordering::Equal)
});
let mut sigma_sorted = vec![0.0f32; min_mn];
let mut u_sorted = vec![0.0f32; m * m];
let mut v_sorted = vec![0.0f32; n * n];
for (new_j, &old_j) in indices.iter().enumerate() {
sigma_sorted[new_j] = sigma[old_j];
for i in 0..m {
u_sorted[i * m + new_j] = u[i * m + old_j];
}
for i in 0..n {
v_sorted[i * n + new_j] = v[i * n + old_j];
}
}
for j in min_mn..m {
u_sorted[j * m + j] = 1.0;
}
for j in min_mn..n {
v_sorted[j * n + j] = 1.0;
}
let mut vt = vec![0.0f32; n * n];
for i in 0..n {
for j in 0..n {
vt[i * n + j] = v_sorted[j * n + i];
}
}
SvdResult {
u: u_sorted,
sigma: sigma_sorted,
vt,
m,
n,
}
}