use crate::arithmetic::Fx;
pub struct Svd {
pub u: Vec<Vec<Fx>>,
pub v: Vec<Vec<Fx>>,
pub sigma: Vec<Fx>,
}
pub fn dot(a: &[Fx], b: &[Fx]) -> Fx {
let mut s = Fx::ZERO;
for i in 0..a.len() {
s = s + a[i] * b[i];
}
s
}
fn fabs(x: Fx) -> Fx {
if x < Fx::ZERO {
Fx::ZERO - x
} else {
x
}
}
fn abs_max_normalize(v: &mut [Fx]) {
let mut m = Fx::ZERO;
for &x in v.iter() {
let a = fabs(x);
if a > m {
m = a;
}
}
if !m.is_zero() {
for x in v.iter_mut() {
*x = x.div(m);
}
}
}
pub fn orthonormalize(block: &mut [Vec<Fx>]) {
let k = block.len();
for j in 0..k {
for i in 0..j {
let denom = dot(&block[i], &block[i]);
if denom.is_zero() {
continue;
}
let coeff = dot(&block[i], &block[j]).div(denom);
for x in 0..block[j].len() {
block[j][x] = block[j][x] - coeff * block[i][x];
}
}
abs_max_normalize(&mut block[j]);
}
}
fn start_block(n: usize, k: usize) -> Vec<Vec<Fx>> {
(0..k)
.map(|c| {
let mut v: Vec<Fx> = (0..n)
.map(|i| Fx::from_int(((i * (2 * c + 3) + c) % 13 + 1) as i64))
.collect();
abs_max_normalize(&mut v);
v
})
.collect()
}
fn l2_normalize(v: &mut [Fx]) {
let norm2 = dot(v, v);
if norm2 > Fx::ZERO {
let inv = Fx::ONE.div(norm2.sqrt());
for x in v.iter_mut() {
*x = *x * inv;
}
}
}
fn sc1(u: &mut [Fx], v: &mut [Fx]) {
let mut peak = Fx::ZERO;
let mut sign_neg = false;
for &x in u.iter() {
let a = fabs(x);
if a > peak {
peak = a;
sign_neg = x < Fx::ZERO;
}
}
if sign_neg {
for x in u.iter_mut() {
*x = Fx::ZERO - *x;
}
for x in v.iter_mut() {
*x = Fx::ZERO - *x;
}
}
}
pub fn top_svd(
n: usize,
apply_m: &dyn Fn(&[Fx]) -> Vec<Fx>,
apply_mt: &dyn Fn(&[Fx]) -> Vec<Fx>,
k: usize,
iters: usize,
) -> Svd {
let k = k.min(n);
if k == 0 {
return Svd {
u: vec![],
v: vec![],
sigma: vec![],
};
}
let mtm = |x: &[Fx]| -> Vec<Fx> { apply_mt(&apply_m(x)) };
let mut block = start_block(n, k);
orthonormalize(&mut block);
for _ in 0..iters {
for col in block.iter_mut() {
*col = mtm(col);
}
orthonormalize(&mut block);
}
let mut triples: Vec<(Fx, Vec<Fx>, Vec<Fx>)> = block
.into_iter()
.map(|mut v| {
l2_normalize(&mut v);
let mvv = mtm(&v);
let eig = dot(&v, &mvv);
let sigma = if eig < Fx::ZERO { Fx::ZERO } else { eig.sqrt() };
let mut u = apply_m(&v);
l2_normalize(&mut u);
sc1(&mut u, &mut v);
(sigma, u, v)
})
.collect();
triples.sort_by_key(|t| core::cmp::Reverse(t.0));
let mut svd = Svd {
u: Vec::with_capacity(k),
v: Vec::with_capacity(k),
sigma: Vec::with_capacity(k),
};
for (s, u, v) in triples {
svd.sigma.push(s);
svd.u.push(u);
svd.v.push(v);
}
svd
}
pub fn dense_svd(p: &[Vec<Fx>], k: usize, iters: usize) -> Svd {
let n = p.len();
let apply_m = |x: &[Fx]| -> Vec<Fx> { (0..n).map(|i| dot(&p[i], x)).collect() };
let apply_mt = |x: &[Fx]| -> Vec<Fx> {
let mut out = vec![Fx::ZERO; n];
for i in 0..n {
for j in 0..n {
out[j] = out[j] + p[i][j] * x[i];
}
}
out
};
top_svd(n, &apply_m, &apply_mt, k, iters)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reconstructs_a_small_symmetric_matrix() {
let p = vec![
vec![Fx::from_int(2), Fx::from_int(1), Fx::ZERO],
vec![Fx::from_int(1), Fx::from_int(2), Fx::ZERO],
vec![Fx::ZERO, Fx::ZERO, Fx::from_int(3)],
];
let svd = dense_svd(&p, 3, 200);
let n = 3;
let mut err = 0.0;
let mut mag = 0.0;
for i in 0..n {
for j in 0..n {
let mut r = 0.0;
for c in 0..svd.sigma.len() {
r += svd.sigma[c].to_f64() * svd.u[c][i].to_f64() * svd.v[c][j].to_f64();
}
err += (r - p[i][j].to_f64()).powi(2);
mag += p[i][j].to_f64().powi(2);
}
}
let rel = (err / mag).sqrt();
assert!(rel < 0.05, "SVD reconstruction error {rel} too large");
}
#[test]
fn singular_values_descend() {
let p = vec![
vec![Fx::from_int(5), Fx::ZERO, Fx::ZERO],
vec![Fx::ZERO, Fx::from_int(3), Fx::ZERO],
vec![Fx::ZERO, Fx::ZERO, Fx::from_int(1)],
];
let svd = dense_svd(&p, 3, 100);
assert!(
svd.sigma[0] >= svd.sigma[1] && svd.sigma[1] >= svd.sigma[2],
"σ must descend"
);
assert!(
(svd.sigma[0].to_f64() - 5.0).abs() < 0.1,
"top σ ≈ 5, got {}",
svd.sigma[0].to_f64()
);
}
}