use crate::dense::{small_svd, svd_flip};
use crate::error::{Result, SvdLibError};
use crate::matrix::{SparseMat, SparseMatDense};
use crate::types::{Algorithm, Detail, Diagnostics, SvdFloat, SvdRec};
use ndarray::{s, Array1, Array2, ArrayView2};
use num_traits::Float;
use rand::rngs::StdRng;
use rand::{rng, Rng, RngExt, SeedableRng};
pub const DEFAULT_TOL: f64 = 1e-10;
pub const DEFAULT_EXTRA_WORK: usize = 7;
pub const DEFAULT_MAX_RESTARTS: usize = 1000;
#[derive(Debug, Clone)]
pub struct IrlbaConfig {
pub rank: usize,
pub work: Option<usize>,
pub tol: f64,
pub max_restarts: usize,
pub seed: Option<u64>,
pub mean_center: bool,
pub require_convergence: bool,
}
impl IrlbaConfig {
pub fn new(rank: usize) -> Self {
Self {
rank,
work: None,
tol: DEFAULT_TOL,
max_restarts: DEFAULT_MAX_RESTARTS,
seed: None,
mean_center: false,
require_convergence: true,
}
}
pub fn work(mut self, work: usize) -> Self {
self.work = Some(work);
self
}
pub fn tol(mut self, tol: f64) -> Self {
self.tol = tol;
self
}
pub fn max_restarts(mut self, n: usize) -> Self {
self.max_restarts = n;
self
}
pub fn seed(mut self, seed: u64) -> Self {
self.seed = Some(seed);
self
}
pub fn mean_center(mut self, yes: bool) -> Self {
self.mean_center = yes;
self
}
pub fn allow_unconverged(mut self) -> Self {
self.require_convergence = false;
self
}
}
pub fn svd<T: SvdFloat, M: SparseMat<T>>(a: &M, rank: usize) -> Result<SvdRec<T>> {
svd_with(a, &IrlbaConfig::new(rank), None)
}
pub fn svd_seed<T: SvdFloat, M: SparseMat<T>>(a: &M, rank: usize, seed: u64) -> Result<SvdRec<T>> {
svd_with(a, &IrlbaConfig::new(rank).seed(seed), None)
}
pub fn svd_centered<T: SvdFloat, M: SparseMatDense<T>>(
a: &M,
rank: usize,
seed: Option<u64>,
) -> Result<SvdRec<T>> {
let means = a.col_means();
let mut cfg = IrlbaConfig::new(rank).mean_center(true);
cfg.seed = seed;
svd_with(a, &cfg, Some(means))
}
struct Op<'a, T, M> {
a: &'a M,
means: Option<&'a [T]>,
_p: std::marker::PhantomData<T>,
}
impl<'a, T: SvdFloat, M: SparseMat<T>> Op<'a, T, M> {
fn rows(&self) -> usize {
self.a.rows()
}
fn cols(&self) -> usize {
self.a.cols()
}
fn mul(&self, x: &[T], y: &mut [T], trans: bool) {
self.a.mul_vec(x, y, trans);
let Some(m) = self.means else { return };
if !trans {
let c: T = m.iter().zip(x.iter()).map(|(&a, &b)| a * b).sum();
for yi in y.iter_mut() {
*yi -= c;
}
} else {
let sum: T = x.iter().copied().sum();
for (yi, &mi) in y.iter_mut().zip(m.iter()) {
*yi -= mi * sum;
}
}
}
}
fn reorthogonalize<T: SvdFloat>(
w: &mut Array1<T>,
basis: &ArrayView2<T>,
count: usize,
coeffs: &mut Array1<T>,
) {
if count == 0 {
return;
}
let b = basis.slice(s![..count, ..]);
let mut total = Array1::<T>::zeros(count);
{
let mut c = coeffs.slice_mut(s![..count]);
for _ in 0..2 {
ndarray::linalg::general_mat_vec_mul(T::one(), &b, w, T::zero(), &mut c);
ndarray::linalg::general_mat_vec_mul(-T::one(), &b.t(), &c, T::one(), w);
total += &c;
}
}
coeffs.slice_mut(s![..count]).assign(&total);
}
fn random_orthogonal<T: SvdFloat>(
out: &mut Array1<T>,
basis: &ArrayView2<T>,
count: usize,
coeffs: &mut Array1<T>,
rng_state: &mut StdRng,
) -> bool {
let floor = T::from_f64_val(0.1);
for _ in 0..4 {
random_unit(out, rng_state);
reorthogonalize(out, basis, count, coeffs);
let n = norm(out);
if n > floor && num_traits::Float::is_finite(n) {
*out /= n;
return true;
}
}
false
}
fn norm<T: SvdFloat>(v: &Array1<T>) -> T {
v.iter().map(|&x| x * x).sum::<T>().sqrt()
}
fn random_unit<T: SvdFloat>(v: &mut Array1<T>, rng_state: &mut StdRng) {
for x in v.iter_mut() {
*x = T::from_f64_val(rng_state.random_range(-1.0..1.0));
}
let n = norm(v);
if n > T::zero() {
*v /= n;
} else {
v.fill(T::zero());
v[0] = T::one();
}
}
pub fn svd_with<T: SvdFloat, M: SparseMat<T>>(
a: &M,
cfg: &IrlbaConfig,
means: Option<Array1<T>>,
) -> Result<SvdRec<T>> {
let (rows, cols) = (a.rows(), a.cols());
let min_dim = rows.min(cols);
if cfg.rank == 0 {
return Err(SvdLibError::invalid("irlba: rank must be at least 1"));
}
if cfg.rank > min_dim {
return Err(SvdLibError::invalid(format!(
"irlba: rank {} exceeds min(rows, cols) = {min_dim}",
cfg.rank
)));
}
if cfg.mean_center {
match &means {
Some(m) if m.len() == cols => {}
Some(m) => {
return Err(SvdLibError::shape(format!(
"irlba: means has length {} but the matrix has {cols} columns",
m.len()
)))
}
None => {
return Err(SvdLibError::invalid(
"irlba: mean_center is set but no means were supplied; \
use `svd_centered`",
))
}
}
}
let k = cfg.rank;
let seed = cfg.seed.unwrap_or_else(|| rng().next_u64());
if min_dim == 1 {
return trivial_rank_one(a, &means, cfg, seed);
}
let work = cfg
.work
.unwrap_or(k + DEFAULT_EXTRA_WORK)
.clamp(k + 1, min_dim.max(k + 1))
.min(min_dim);
if work <= k {
return Err(SvdLibError::invalid(format!(
"irlba: rank {k} needs a basis of at least {} vectors but the matrix only \
admits {min_dim}; request at most {} triplets, or use \
`single_svdlib::randomized`, which supports the full rank",
k + 1,
min_dim - 1
)));
}
let means_slice = if cfg.mean_center {
means.as_ref().map(|m| m.as_slice().unwrap())
} else {
None
};
let op = Op {
a,
means: means_slice,
_p: std::marker::PhantomData,
};
let mut state = Solve::new(&op, work, k, cfg.tol, seed);
let outcome = state.run(cfg.max_restarts)?;
let Solve { v, u, .. } = state;
let SolveOutcome {
p,
q,
sigma,
restarts,
converged,
max_residual,
matvecs,
} = outcome;
let pk = p.slice(s![.., ..k]);
let qk = q.slice(s![.., ..k]);
let mut u_out = pk
.t()
.dot(&u.slice(s![..work, ..]))
.reversed_axes()
.to_owned();
let mut vt_out = qk.t().dot(&v.slice(s![..work, ..])).to_owned();
let s_out = sigma.slice(s![..k]).to_owned();
if cfg.require_convergence && !converged {
return Err(SvdLibError::failed(
"irlba",
format!(
"did not converge in {restarts} restarts: largest residual is {:.3e} \
against a threshold of {:.3e} (tol {:.1e} x sigma_max). Raise \
`max_restarts` or `work`, loosen `tol`, or call `allow_unconverged` to \
accept a best effort.",
max_residual.to_f64(),
cfg.tol * sigma[0].to_f64(),
cfg.tol,
),
));
}
svd_flip(&mut u_out, &mut vt_out);
Ok(SvdRec {
d: k,
u: u_out,
s: s_out,
vt: vt_out,
total_squared_norm: T::from_f64_val(crate::matrix::total_squared_norm(
a,
means.as_ref().map(|m| m.view()),
)),
diagnostics: Diagnostics {
algorithm: Algorithm::Irlba,
non_zero: a.nnz(),
dimensions: k,
significant_values: k,
transposed: false,
random_seed: seed,
matvecs,
detail: Detail::Irlba {
restarts,
converged,
tolerance: T::from_f64_val(cfg.tol),
max_residual,
},
},
})
}
fn trivial_rank_one<T: SvdFloat, M: SparseMat<T>>(
a: &M,
means: &Option<Array1<T>>,
cfg: &IrlbaConfig,
seed: u64,
) -> Result<SvdRec<T>> {
let (rows, cols) = (a.rows(), a.cols());
let op = Op {
a,
means: if cfg.mean_center {
means.as_ref().map(|m| m.as_slice().unwrap())
} else {
None
},
_p: std::marker::PhantomData,
};
let (long, short, trans) = if rows == 1 {
(cols, rows, true)
} else {
(rows, cols, false)
};
let mut probe = vec![T::zero(); short];
probe[0] = T::one();
let mut vec = vec![T::zero(); long];
op.mul(&probe, &mut vec, trans);
let sigma = vec.iter().map(|&x| x * x).sum::<T>().sqrt();
if !num_traits::Float::is_finite(sigma) {
return Err(SvdLibError::failed("irlba", "the operand is not finite"));
}
let (u, vt) = if sigma > T::zero() {
let unit: Vec<T> = vec.iter().map(|&x| x / sigma).collect();
if rows == 1 {
(
Array2::from_shape_vec((1, 1), vec![T::one()])?,
Array2::from_shape_vec((1, cols), unit)?,
)
} else {
(
Array2::from_shape_vec((rows, 1), unit)?,
Array2::from_shape_vec((1, 1), vec![T::one()])?,
)
}
} else {
let mut u = Array2::<T>::zeros((rows, 1));
let mut vt = Array2::<T>::zeros((1, cols));
u[[0, 0]] = T::one();
vt[[0, 0]] = T::one();
(u, vt)
};
Ok(SvdRec {
d: 1,
u,
s: Array1::from_vec(vec![sigma]),
vt,
total_squared_norm: T::from_f64_val(crate::matrix::total_squared_norm(
a,
if cfg.mean_center {
means.as_ref().map(|m| m.view())
} else {
None
},
)),
diagnostics: Diagnostics {
algorithm: Algorithm::Irlba,
non_zero: a.nnz(),
dimensions: 1,
significant_values: 1,
transposed: false,
random_seed: seed,
matvecs: 1,
detail: Detail::Irlba {
restarts: 0,
converged: true,
tolerance: T::from_f64_val(cfg.tol),
max_residual: T::zero(),
},
},
})
}
struct SolveOutcome<T> {
p: Array2<T>,
q: Array2<T>,
sigma: Array1<T>,
restarts: usize,
converged: bool,
max_residual: T,
matvecs: usize,
}
struct Solve<'a, T, M> {
op: &'a Op<'a, T, M>,
work: usize,
k: usize,
tol: f64,
v: Array2<T>,
u: Array2<T>,
b: Array2<T>,
rng: StdRng,
matvecs: usize,
anorm: T,
}
impl<'a, T: SvdFloat, M: SparseMat<T>> Solve<'a, T, M> {
fn new(op: &'a Op<'a, T, M>, work: usize, k: usize, tol: f64, seed: u64) -> Self {
Self {
op,
work,
k,
tol,
v: Array2::zeros((work + 1, op.cols())),
u: Array2::zeros((work, op.rows())),
b: Array2::zeros((work, work)),
rng: StdRng::seed_from_u64(seed),
matvecs: 0,
anorm: T::zero(),
}
}
fn breakdown_threshold(&self) -> T {
let dim = T::from_f64_val((self.op.rows().max(self.op.cols()) as f64).sqrt());
self.anorm * T::eps() * dim
}
fn extend(&mut self, start: usize, coupling: Option<&Array1<T>>) -> Result<(T, Array1<T>)> {
let (rows, cols) = (self.op.rows(), self.op.cols());
let mut w = Array1::<T>::zeros(rows);
let mut z = Array1::<T>::zeros(cols);
let mut coeffs = Array1::<T>::zeros(self.work + 1);
for j in start..self.work {
{
let vj = self.v.row(j).to_owned();
self.op
.mul(vj.as_slice().unwrap(), w.as_slice_mut().unwrap(), false);
self.matvecs += 1;
}
if j == start && start > 0 {
let rho = coupling.expect("restart requires a coupling vector");
let uk = self.u.slice(s![..self.k, ..]);
w -= &uk.t().dot(rho);
} else if j > 0 {
let beta_prev = self.b[[j - 1, j]];
let uprev = self.u.row(j - 1);
w.scaled_add(-beta_prev, &uprev);
}
{
let ub = self.u.view();
reorthogonalize(&mut w, &ub, j, &mut coeffs);
}
for i in 0..j {
self.b[[i, j]] += coeffs[i];
}
let alpha = norm(&w);
if !num_traits::Float::is_finite(alpha) {
return Err(SvdLibError::failed(
"irlba",
"the left Krylov vector became non-finite; the matrix most likely \
contains NaN or infinity",
));
}
let alpha_kept = if alpha <= self.breakdown_threshold() {
let ub = self.u.view();
if !random_orthogonal(&mut w, &ub, j, &mut coeffs, &mut self.rng) {
return Ok((T::zero(), Array1::zeros(cols)));
}
T::zero()
} else {
self.anorm = Float::max(self.anorm, alpha);
w /= alpha;
alpha
};
self.u.row_mut(j).assign(&w);
self.b[[j, j]] = alpha_kept;
self.op
.mul(w.as_slice().unwrap(), z.as_slice_mut().unwrap(), true);
self.matvecs += 1;
{
let vj = self.v.row(j);
z.scaled_add(-alpha_kept, &vj);
}
{
let vb = self.v.view();
reorthogonalize(&mut z, &vb, j + 1, &mut coeffs);
}
let beta = norm(&z);
if !num_traits::Float::is_finite(beta) {
return Err(SvdLibError::failed(
"irlba",
"the right Krylov vector became non-finite; the matrix most likely \
contains NaN or infinity",
));
}
let (beta_kept, zn) = if beta <= self.breakdown_threshold() {
let vb = self.v.view();
if !random_orthogonal(&mut z, &vb, j + 1, &mut coeffs, &mut self.rng) {
return Ok((T::zero(), Array1::zeros(cols)));
}
(T::zero(), z.clone())
} else {
self.anorm = Float::max(self.anorm, beta);
(beta, &z / beta)
};
self.v.row_mut(j + 1).assign(&zn);
if j + 1 < self.work {
self.b[[j, j + 1]] = beta_kept;
} else {
return Ok((beta_kept, zn));
}
}
unreachable!("extend always terminates at the final column")
}
fn run(&mut self, max_restarts: usize) -> Result<SolveOutcome<T>> {
{
let mut probe = Array1::<T>::zeros(self.op.rows());
random_unit(&mut probe, &mut self.rng);
let mut v0 = Array1::<T>::zeros(self.op.cols());
self.op
.mul(probe.as_slice().unwrap(), v0.as_slice_mut().unwrap(), true);
self.matvecs += 1;
let n = norm(&v0);
if n > T::zero() && num_traits::Float::is_finite(n) {
v0 /= n;
} else {
random_unit(&mut v0, &mut self.rng);
}
self.v.row_mut(0).assign(&v0);
}
let mut start = 0usize;
let mut coupling: Option<Array1<T>> = None;
for restart in 0..=max_restarts {
let (beta, v_next) = self.extend(start, coupling.as_ref())?;
let svd = small_svd(self.b.view())?;
let sigma = svd.s;
let p = svd.u; let q = svd.vt.reversed_axes().as_standard_layout().to_owned();
let smax = Float::max(sigma[0], T::eps());
let thresh = T::from_f64_val(self.tol) * smax;
let mut max_resid = T::zero();
for i in 0..self.k {
let r = Float::abs(beta * p[[self.work - 1, i]]);
if r > max_resid {
max_resid = r;
}
}
if max_resid <= thresh || restart == max_restarts {
return Ok(SolveOutcome {
p,
q,
sigma,
restarts: restart,
converged: max_resid <= thresh,
max_residual: max_resid,
matvecs: self.matvecs,
});
}
let vk = q
.slice(s![.., ..self.k])
.t()
.dot(&self.v.slice(s![..self.work, ..]));
let uk = p
.slice(s![.., ..self.k])
.t()
.dot(&self.u.slice(s![..self.work, ..]));
let mut rho = Array1::<T>::zeros(self.k);
for i in 0..self.k {
rho[i] = beta * p[[self.work - 1, i]];
}
self.v.slice_mut(s![..self.k, ..]).assign(&vk);
self.u.slice_mut(s![..self.k, ..]).assign(&uk);
self.v.row_mut(self.k).assign(&v_next);
self.b.fill(T::zero());
for i in 0..self.k {
self.b[[i, i]] = sigma[i];
self.b[[i, self.k]] = rho[i];
}
start = self.k;
coupling = Some(rho);
}
unreachable!("the restart loop returns on its final iteration")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::matrix::SvdMat;
use crate::testing::{dense_of, gen_lowrank, gen_sparse, reference_singular_values, Lcg};
use ndarray::Axis;
use sprs::TriMatI;
fn diagonal(n: usize) -> SvdMat<f64> {
let mut t = TriMatI::<f64, u32>::new((n, n));
for i in 0..n {
t.add_triplet(i, i, (n - i) as f64);
}
t.to_csr::<u64>()
}
fn dense_random(r: usize, c: usize, seed: u64) -> SvdMat<f64> {
let mut rng = Lcg::new(seed);
let mut t = TriMatI::<f64, u32>::new((r, c));
for i in 0..r {
for j in 0..c {
t.add_triplet(i, j, rng.signed());
}
}
t.to_csr::<u64>()
}
fn assert_matches_lapack(name: &str, a: &SvdMat<f64>, rank: usize, tol: f64) -> SvdRec<f64> {
let want = reference_singular_values(&dense_of(a));
let got = svd_seed(a, rank, 42).unwrap_or_else(|e| panic!("{name}: {e}"));
assert_eq!(got.d, rank, "{name}: rank");
for (i, &g) in got.s.iter().enumerate() {
let rel = (g - want[i]).abs() / want[i].abs().max(1e-30);
assert!(
rel < tol,
"{name}: singular value {i}: irlba {g:.12e} vs LAPACK {:.12e} (rel {rel:.3e})",
want[i]
);
}
got
}
#[test]
fn exact_on_diagonal_matrix() {
let a = diagonal(40);
let got = assert_matches_lapack("diagonal_40", &a, 10, 1e-10);
approx::assert_relative_eq!(got.s[0], 40.0, max_relative = 1e-10);
approx::assert_relative_eq!(got.s[9], 31.0, max_relative = 1e-10);
}
#[test]
fn matches_lapack_on_dense_random() {
assert_matches_lapack("dense_random_60x40", &dense_random(60, 40, 3), 10, 1e-9);
}
#[test]
fn matches_lapack_on_lowrank() {
assert_matches_lapack("lowrank_80x50_r8", &gen_lowrank(80, 50, 8, 21), 8, 1e-9);
assert_matches_lapack(
"lowrank_200x80_r10",
&gen_lowrank(200, 80, 10, 555),
15,
1e-8,
);
}
#[test]
fn matches_lapack_on_sparse() {
assert_matches_lapack("sparse_500x40", &gen_sparse(500, 40, 0.10, 7), 10, 1e-9);
assert_matches_lapack("sparse_200x120", &gen_sparse(200, 120, 0.05, 3), 20, 1e-9);
assert_matches_lapack(
"sparse_100x100",
&gen_sparse(100, 100, 0.0098, 42),
20,
1e-8,
);
}
#[test]
fn matches_lapack_on_wide() {
assert_matches_lapack("wide_50x400", &gen_sparse(50, 400, 0.05, 1234), 10, 1e-9);
}
#[test]
fn orientation_and_reconstruction() {
for (r, c) in [(200usize, 60usize), (60, 200)] {
let a = gen_sparse(r, c, 0.1, 11);
let rank = 10;
let svd = svd_seed(&a, rank, 42).unwrap();
assert_eq!(svd.u.dim(), (r, rank), "u shape for {r}x{c}");
assert_eq!(svd.vt.dim(), (rank, c), "vt shape for {r}x{c}");
let dense = dense_of(&a);
let refs = reference_singular_values(&dense);
let tail: f64 = refs[rank..].iter().map(|v| v * v).sum::<f64>().sqrt();
let err: f64 = (&svd.recompose() - &dense)
.iter()
.map(|v| v * v)
.sum::<f64>()
.sqrt();
approx::assert_relative_eq!(err, tail, max_relative = 1e-6);
}
}
#[test]
fn singular_vectors_are_orthonormal_and_consistent() {
let a = gen_sparse(300, 120, 0.06, 17);
let rank = 12;
let svd = svd_seed(&a, rank, 42).unwrap();
let orth_u = crate::dense::orthogonality_error(&svd.u.view());
assert!(orth_u < 1e-9, "||UᵀU - I|| = {orth_u:.3e}");
let vt_t = svd.vt.t().to_owned();
let orth_v = crate::dense::orthogonality_error(&vt_t.view());
assert!(orth_v < 1e-9, "||VᵀV - I|| = {orth_v:.3e}");
for i in 0..rank {
let vi: Vec<f64> = svd.vt.row(i).to_vec();
let mut av = vec![0.0; a.rows()];
SparseMat::mul_vec(&a, &vi, &mut av, false);
let resid: f64 = av
.iter()
.zip(svd.u.column(i).iter())
.map(|(&x, &ui)| {
let d = x - svd.s[i] * ui;
d * d
})
.sum::<f64>()
.sqrt();
assert!(
resid / svd.s[0] < 1e-8,
"triplet {i}: ||A v - s u|| / s_max = {:.3e}",
resid / svd.s[0]
);
}
}
#[test]
fn csr_and_csc_agree() {
let a = gen_sparse(150, 90, 0.08, 5);
let csc = a.to_other_storage();
let x = svd_seed(&a, 12, 42).unwrap();
let y = svd_seed(&csc, 12, 42).unwrap();
for (p, q) in x.s.iter().zip(y.s.iter()) {
approx::assert_relative_eq!(p, q, max_relative = 1e-10);
}
}
#[test]
fn mean_centering_matches_dense_pca() {
let a = gen_lowrank(120, 40, 6, 31);
let dense = dense_of(&a);
let means = dense.mean_axis(Axis(0)).unwrap();
let centered = &dense - &means.view().insert_axis(Axis(0));
let want = reference_singular_values(¢ered);
let got = svd_centered(&a, 6, Some(42)).unwrap();
for (i, &g) in got.s.iter().enumerate() {
let rel = (g - want[i]).abs() / want[i].abs().max(1e-30);
assert!(
rel < 1e-8,
"centered singular value {i}: {g:.9e} vs {:.9e} (rel {rel:.3e})",
want[i]
);
}
}
#[test]
fn f32_matches_reference_at_f32_precision() {
let a64 = gen_lowrank(100, 50, 6, 77);
let want = reference_singular_values(&dense_of(&a64));
let mut t = TriMatI::<f32, u32>::new((100, 50));
for (v, (i, j)) in a64.iter() {
t.add_triplet(i as usize, j as usize, *v as f32);
}
let a32: SvdMat<f32> = t.to_csr::<u64>();
let got = svd_seed(&a32, 6, 42).unwrap();
for (i, &g) in got.s.iter().enumerate() {
let rel = ((g as f64) - want[i]).abs() / want[i].abs().max(1e-30);
assert!(rel < 1e-4, "f32 singular value {i}: rel {rel:.3e}");
}
}
#[test]
fn reports_convergence_and_bounded_restarts() {
let a = gen_sparse(200, 100, 0.05, 9);
let svd = svd_seed(&a, 10, 42).unwrap();
assert_eq!(svd.diagnostics.algorithm, Algorithm::Irlba);
match svd.diagnostics.detail {
Detail::Irlba {
converged,
restarts,
max_residual,
..
} => {
assert!(converged, "expected convergence");
assert!(restarts < 50, "unexpectedly many restarts: {restarts}");
assert!(max_residual >= 0.0);
}
ref other => panic!("wrong detail variant: {other:?}"),
}
assert!(svd.diagnostics.matvecs > 0);
}
#[test]
fn tolerance_is_monotone() {
let a = gen_lowrank(150, 60, 8, 44);
let want = reference_singular_values(&dense_of(&a));
let mut prev = f64::INFINITY;
for tol in [1e-4, 1e-8, 1e-12] {
let cfg = IrlbaConfig::new(8).seed(42).tol(tol);
let got = svd_with(&a, &cfg, None).unwrap();
let err = (0..8)
.map(|i| (got.s[i] - want[i]).abs() / want[i])
.fold(0.0f64, f64::max);
assert!(
err <= prev * 10.0 + 1e-12,
"tol {tol:.0e} gave error {err:.3e}, worse than the looser tolerance's {prev:.3e}"
);
prev = err.max(1e-16);
}
}
#[test]
fn rejects_bad_configuration() {
let a = gen_sparse(50, 30, 0.2, 1);
assert!(matches!(svd(&a, 0), Err(SvdLibError::InvalidArgument(_))));
assert!(matches!(svd(&a, 31), Err(SvdLibError::InvalidArgument(_))));
let cfg = IrlbaConfig::new(5).mean_center(true);
assert!(matches!(
svd_with(&a, &cfg, None),
Err(SvdLibError::InvalidArgument(_))
));
let cfg = IrlbaConfig::new(5).mean_center(true);
assert!(matches!(
svd_with(&a, &cfg, Some(Array1::zeros(7))),
Err(SvdLibError::ShapeMismatch(_))
));
}
#[test]
fn is_reproducible_given_a_seed() {
let a = gen_sparse(120, 70, 0.1, 23);
let x = svd_seed(&a, 8, 1234).unwrap();
let y = svd_seed(&a, 8, 1234).unwrap();
assert_eq!(x.s, y.s);
assert_eq!(x.u, y.u);
assert_eq!(x.vt, y.vt);
}
#[test]
fn full_rank_request() {
let a = gen_lowrank(30, 20, 20, 88);
assert_matches_lapack("full_rank_30x20", &a, 19, 1e-8);
}
}