pub mod jacobi;
pub mod tsqr;
pub use tsqr::{orthogonality_error, orthonormalize, tsqr};
use crate::error::{Result, SvdLibError};
use crate::types::SvdFloat;
use ndarray::{Array1, Array2, ArrayView2};
pub struct SmallSvd<T> {
pub u: Array2<T>,
pub s: Array1<T>,
pub vt: Array2<T>,
}
pub fn small_svd<T: SvdFloat>(a: ArrayView2<T>) -> Result<SmallSvd<T>> {
let (m, n) = a.dim();
if m == 0 || n == 0 {
return Err(SvdLibError::shape(format!(
"small_svd needs a non-empty matrix, got {m}x{n}"
)));
}
let k = m.min(n);
if let Some((i, j)) = a
.indexed_iter()
.find(|(_, v)| !num_traits::Float::is_finite(**v))
.map(|(idx, _)| idx)
{
return Err(SvdLibError::DenseFactorization {
factorization: "SVD",
message: format!("operand is not finite at ({i}, {j})"),
});
}
let a64 = Array2::<f64>::from_shape_fn((m, n), |(i, j)| a[[i, j]].to_f64());
let fail = || SvdLibError::DenseFactorization {
factorization: "SVD",
message: format!("one-sided Jacobi did not converge on a {m}x{n} operand"),
};
let (u64, s64, vt64) = if m >= n {
let j = jacobi::jacobi_svd(&a64).ok_or_else(fail)?;
(j.u, j.s, j.v.t().to_owned())
} else {
let j = jacobi::jacobi_svd(&a64.t().to_owned()).ok_or_else(fail)?;
(j.v, j.s, j.u.t().to_owned())
};
debug_assert_eq!(u64.dim(), (m, k));
debug_assert_eq!(vt64.dim(), (k, n));
let mut out_u = Array2::<T>::zeros((m, k));
let mut out_s = Array1::<T>::zeros(k);
let mut out_vt = Array2::<T>::zeros((k, n));
for idx in 0..k {
out_s[idx] = T::from_f64_val(s64[idx]);
for i in 0..m {
out_u[[i, idx]] = T::from_f64_val(u64[[i, idx]]);
}
for j in 0..n {
out_vt[[idx, j]] = T::from_f64_val(vt64[[idx, j]]);
}
}
Ok(SmallSvd {
u: out_u,
s: out_s,
vt: out_vt,
})
}
pub fn svd_flip<T: SvdFloat>(u: &mut Array2<T>, vt: &mut Array2<T>) {
let k = u.ncols().min(vt.nrows());
for j in 0..k {
let mut best = T::zero();
let mut sign = T::one();
for i in 0..u.nrows() {
let v = u[[i, j]];
let mag = num_traits::Float::abs(v);
if mag > best {
best = mag;
sign = if v < T::zero() { -T::one() } else { T::one() };
}
}
if sign < T::zero() {
for i in 0..u.nrows() {
u[[i, j]] = -u[[i, j]];
}
for j2 in 0..vt.ncols() {
vt[[j, j2]] = -vt[[j, j2]];
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testing::Lcg;
#[test]
fn small_svd_reconstructs_and_is_ordered() {
let mut rng = Lcg::new(11);
let a = Array2::from_shape_fn((9, 6), |_| rng.signed());
let svd = small_svd(a.view()).unwrap();
assert_eq!(svd.u.dim(), (9, 6));
assert_eq!(svd.s.len(), 6);
assert_eq!(svd.vt.dim(), (6, 6));
for w in svd.s.to_vec().windows(2) {
assert!(w[0] >= w[1], "singular values not descending: {:?}", svd.s);
}
let scaled = &svd.u * &svd.s.view().insert_axis(ndarray::Axis(0));
let recon = scaled.dot(&svd.vt);
let err: f64 = (&recon - &a).iter().map(|v| v * v).sum::<f64>().sqrt();
let scale: f64 = a.iter().map(|v| v * v).sum::<f64>().sqrt();
assert!(
err / scale < 1e-12,
"relative reconstruction {}",
err / scale
);
}
#[test]
fn small_svd_promotes_f32() {
let mut rng = Lcg::new(13);
let a = Array2::from_shape_fn((7, 5), |_| rng.signed() as f32);
let svd = small_svd(a.view()).unwrap();
let scaled = &svd.u * &svd.s.view().insert_axis(ndarray::Axis(0));
let recon = scaled.dot(&svd.vt);
let err: f32 = (&recon - &a).iter().map(|v| v * v).sum::<f32>().sqrt();
let scale: f32 = a.iter().map(|v| v * v).sum::<f32>().sqrt();
assert!(
err / scale < 1e-5,
"relative reconstruction {}",
err / scale
);
}
#[test]
fn svd_flip_makes_signs_deterministic() {
let mut u = ndarray::arr2(&[[-3.0f64, 1.0], [1.0, -4.0]]);
let mut vt = ndarray::arr2(&[[1.0f64, 2.0], [3.0, 4.0]]);
let before = u.dot(&vt);
svd_flip(&mut u, &mut vt);
assert!(u[[0, 0]] > 0.0);
assert!(u[[1, 1]] > 0.0);
let after = u.dot(&vt);
for (a, b) in before.iter().zip(after.iter()) {
approx::assert_relative_eq!(a, b, max_relative = 1e-14);
}
}
#[test]
fn small_svd_rejects_empty() {
let a = Array2::<f64>::zeros((0, 3));
assert!(small_svd(a.view()).is_err());
}
}