use nalgebra_sparse::{na::{linalg::{SVD, QR}, ComplexField, DMatrix, DVector}, CsrMatrix};
use rand_distr::{num_traits::Float, Distribution, Normal};
use rand::{SeedableRng, rngs::StdRng};
pub struct TruncatedSVD<T: ComplexField> {
pub u: DMatrix<T>,
pub singular_values: DVector<T::RealField>,
}
impl<T: Copy + ComplexField + Float> TruncatedSVD<T> {
pub fn range_random(a: &CsrMatrix<T>, k: usize) -> DMatrix<T> {
let k = k.min(a.nrows()).min(a.ncols());
let mut rng = StdRng::seed_from_u64(42);
let oversampled_k = (k + 5).min(a.ncols());
let omega = DMatrix::from_fn(a.ncols(), oversampled_k, |_, _| {
T::from(Normal::new(0.0, 1.0).unwrap().sample(&mut rng)).unwrap()
});
let mut y = a * ω
let power_iterations = 2; for _ in 0..power_iterations {
y = a.transpose() * &y;
y = a * &y;
}
let qr = QR::new(y);
let q_full = qr.q();
q_full.view_range(.., ..k).into_owned()
}
pub fn new(matrix: &CsrMatrix<T>, k: usize) -> Self {
if k == 0 {
return Self {
u: DMatrix::zeros(matrix.nrows(), 0),
singular_values: DVector::zeros(0),
};
}
let m = matrix.nrows();
let n = matrix.ncols();
let min_dim = m.min(n);
if min_dim <= 100 || k > min_dim * 3 / 4 {
let mut dense_matrix = DMatrix::<T>::zeros(m, n);
for (row_idx, row) in matrix.row_iter().enumerate() {
for (&col_idx, &value) in row.col_indices().iter().zip(row.values().iter()) {
dense_matrix[(row_idx, col_idx)] = value;
}
}
let full_svd = SVD::new(dense_matrix, true, false);
let u_full = full_svd.u.unwrap();
let singular_values_full = full_svd.singular_values;
let k_actual = k.min(u_full.ncols()).min(singular_values_full.len());
let u = u_full.columns(0, k_actual).into_owned();
let singular_values = singular_values_full.rows(0, k_actual).into_owned();
return Self { u, singular_values };
}
let q = Self::range_random(matrix, k);
let b = matrix.transpose() * &q;
let svd = SVD::new(b.transpose(), true, false);
let u_small = svd.u.unwrap();
let singular_values = svd.singular_values;
let u = &q * &u_small;
let k_actual = k.min(u.ncols()).min(singular_values.len());
let u = u.columns(0, k_actual).into_owned();
let singular_values = singular_values.rows(0, k_actual).into_owned();
Self {
u,
singular_values,
}
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_truncated_svd() {
use nalgebra_sparse::CsrMatrix;
use nalgebra_sparse::na::DMatrix;
let dense_matrix = DMatrix::from_row_slice(3, 3, &[
1.0, 2.0, 0.0,
0.0, 3.0, 4.0,
5.0, 6.0, 7.0,
]);
let a = CsrMatrix::from(&dense_matrix);
let svd = super::TruncatedSVD::new(&a, 2);
assert_eq!(svd.u.nrows(), 3);
assert_eq!(svd.u.ncols(), 2);
assert_eq!(svd.singular_values.len(), 2);
assert!(svd.singular_values[0] >= svd.singular_values[1]);
assert!(svd.singular_values[1] > 0.0);
}
#[test]
fn test_truncated_svd_single_rank() {
use nalgebra_sparse::CsrMatrix;
use nalgebra_sparse::na::DMatrix;
let dense_matrix = DMatrix::from_row_slice(3, 2, &[
1.0, 2.0,
2.0, 4.0,
3.0, 6.0,
]);
let a = CsrMatrix::from(&dense_matrix);
let svd = super::TruncatedSVD::new(&a, 1);
assert_eq!(svd.u.nrows(), 3);
assert_eq!(svd.u.ncols(), 1);
assert_eq!(svd.singular_values.len(), 1);
assert!(svd.singular_values[0] > 0.0);
}
#[test]
fn test_truncated_svd_zero_rank() {
use nalgebra_sparse::CsrMatrix;
use nalgebra_sparse::na::DMatrix;
let dense_matrix = DMatrix::from_row_slice(3, 3, &[
1.0, 2.0, 0.0,
0.0, 3.0, 4.0,
5.0, 6.0, 7.0,
]);
let a = CsrMatrix::from(&dense_matrix);
let svd = super::TruncatedSVD::new(&a, 0);
assert_eq!(svd.u.nrows(), 3);
assert_eq!(svd.u.ncols(), 0);
assert_eq!(svd.singular_values.len(), 0);
}
#[test]
fn test_truncated_svd_identity_matrix() {
use nalgebra_sparse::CsrMatrix;
use nalgebra_sparse::na::DMatrix;
let dense_matrix = DMatrix::<f64>::identity(4, 4);
let a = CsrMatrix::from(&dense_matrix);
let svd = super::TruncatedSVD::new(&a, 3);
assert_eq!(svd.u.nrows(), 4);
assert_eq!(svd.u.ncols(), 3);
assert_eq!(svd.singular_values.len(), 3);
for &val in svd.singular_values.iter() {
assert!((val - 1.0f64).abs() < 1e-10f64, "Singular value {} should be close to 1.0", val);
}
for i in 1..svd.singular_values.len() {
assert!(svd.singular_values[i-1] >= svd.singular_values[i]);
}
}
#[test]
fn test_truncated_svd_diagonal_matrix() {
use nalgebra_sparse::CsrMatrix;
use nalgebra_sparse::na::DMatrix;
let mut dense_matrix = DMatrix::<f64>::zeros(4, 4);
dense_matrix[(0, 0)] = 5.0;
dense_matrix[(1, 1)] = 3.0;
dense_matrix[(2, 2)] = 2.0;
dense_matrix[(3, 3)] = 1.0;
let a = CsrMatrix::from(&dense_matrix);
let svd = super::TruncatedSVD::new(&a, 3);
assert_eq!(svd.u.nrows(), 4);
assert_eq!(svd.u.ncols(), 3);
assert_eq!(svd.singular_values.len(), 3);
let expected = [5.0, 3.0, 2.0];
for (i, &expected_val) in expected.iter().enumerate() {
assert!(
(svd.singular_values[i] - expected_val).abs() < 1e-10f64,
"Singular value {} should be close to {}, got {}",
i, expected_val, svd.singular_values[i]
);
}
}
#[test]
fn test_truncated_svd_rank_deficient_matrix() {
use nalgebra_sparse::CsrMatrix;
use nalgebra_sparse::na::DMatrix;
let dense_matrix = DMatrix::from_row_slice(4, 3, &[
1.0, 2.0, 3.0,
1.0, 2.0, 3.0,
4.0, 5.0, 6.0,
7.0, 8.0, 9.0,
]);
let a = CsrMatrix::from(&dense_matrix);
let svd = super::TruncatedSVD::new(&a, 3);
assert_eq!(svd.u.nrows(), 4);
assert_eq!(svd.u.ncols(), 3);
assert_eq!(svd.singular_values.len(), 3);
assert!(svd.singular_values[0] > svd.singular_values[1]);
assert!(svd.singular_values[2] < svd.singular_values[1] * 0.1);
}
#[test]
fn test_truncated_svd_orthogonality() {
use nalgebra_sparse::CsrMatrix;
use nalgebra_sparse::na::DMatrix;
let dense_matrix = DMatrix::from_row_slice(5, 4, &[
1.0, 2.0, 0.0, 1.0,
0.0, 3.0, 4.0, 2.0,
5.0, 6.0, 7.0, 0.0,
2.0, 1.0, 3.0, 4.0,
1.0, 0.0, 2.0, 5.0,
]);
let a = CsrMatrix::from(&dense_matrix);
let svd = super::TruncatedSVD::new(&a, 3);
let u_t_u = svd.u.transpose() * &svd.u;
for i in 0..u_t_u.nrows() {
assert!(
(u_t_u[(i, i)] - 1.0f64).abs() < 1e-10,
"Diagonal element ({}, {}) should be close to 1.0, got {}",
i, i, u_t_u[(i, i)]
);
}
for i in 0..u_t_u.nrows() {
for j in 0..u_t_u.ncols() {
if i != j {
assert!(
u_t_u[(i, j)].abs() < 1e-10f64,
"Off-diagonal element ({}, {}) should be close to 0.0, got {}",
i, j, u_t_u[(i, j)]
);
}
}
}
}
#[test]
fn test_truncated_svd_reconstruction_approximation() {
use nalgebra_sparse::CsrMatrix;
use nalgebra_sparse::na::DMatrix;
let dense_matrix = DMatrix::from_row_slice(3, 3, &[
4.0, 0.0, 0.0,
0.0, 3.0, 0.0,
0.0, 0.0, 2.0,
]);
let a = CsrMatrix::from(&dense_matrix);
let svd = super::TruncatedSVD::new(&a, 2);
let first_two_energy: f64 = svd.singular_values.iter().take(2).map(|x| x * x).sum();
let energy_ratio = first_two_energy / (4.0*4.0 + 3.0*3.0 + 2.0*2.0);
assert!(energy_ratio > 0.8, "Energy ratio {} should be > 0.8", energy_ratio);
}
#[test]
fn test_truncated_svd_larger_k_than_rank() {
use nalgebra_sparse::CsrMatrix;
use nalgebra_sparse::na::DMatrix;
let dense_matrix = DMatrix::from_row_slice(4, 3, &[
1.0, 2.0, 3.0,
4.0, 5.0, 6.0,
7.0, 8.0, 9.0,
10.0, 11.0, 12.0,
]);
let a = CsrMatrix::from(&dense_matrix);
let svd = super::TruncatedSVD::new(&a, 5);
assert!(svd.u.ncols() <= 3);
assert!(svd.singular_values.len() <= 3);
assert_eq!(svd.u.nrows(), 4);
for &val in svd.singular_values.iter() {
assert!(val > -0.01f64, "Singular value {} should be positive", val);
}
}
}