1use crate::dense::{small_svd, svd_flip};
36use crate::error::{Result, SvdLibError};
37use crate::matrix::{SparseMat, SparseMatDense};
38use crate::types::{Algorithm, Detail, Diagnostics, SvdFloat, SvdRec};
39use ndarray::{s, Array1, Array2, ArrayView2};
40use num_traits::Float;
41use rand::rngs::StdRng;
42use rand::{rng, Rng, RngExt, SeedableRng};
43
44pub const DEFAULT_TOL: f64 = 1e-10;
46pub const DEFAULT_EXTRA_WORK: usize = 7;
48pub const DEFAULT_MAX_RESTARTS: usize = 1000;
50
51#[derive(Debug, Clone)]
53pub struct IrlbaConfig {
54 pub rank: usize,
56 pub work: Option<usize>,
85 pub tol: f64,
88 pub max_restarts: usize,
90 pub seed: Option<u64>,
92 pub mean_center: bool,
95 pub require_convergence: bool,
105}
106
107impl IrlbaConfig {
108 pub fn new(rank: usize) -> Self {
110 Self {
111 rank,
112 work: None,
113 tol: DEFAULT_TOL,
114 max_restarts: DEFAULT_MAX_RESTARTS,
115 seed: None,
116 mean_center: false,
117 require_convergence: true,
118 }
119 }
120 pub fn work(mut self, work: usize) -> Self {
122 self.work = Some(work);
123 self
124 }
125 pub fn tol(mut self, tol: f64) -> Self {
127 self.tol = tol;
128 self
129 }
130 pub fn max_restarts(mut self, n: usize) -> Self {
132 self.max_restarts = n;
133 self
134 }
135 pub fn seed(mut self, seed: u64) -> Self {
137 self.seed = Some(seed);
138 self
139 }
140 pub fn mean_center(mut self, yes: bool) -> Self {
142 self.mean_center = yes;
143 self
144 }
145 pub fn allow_unconverged(mut self) -> Self {
147 self.require_convergence = false;
148 self
149 }
150}
151
152pub fn svd<T: SvdFloat, M: SparseMat<T>>(a: &M, rank: usize) -> Result<SvdRec<T>> {
154 svd_with(a, &IrlbaConfig::new(rank), None)
155}
156
157pub fn svd_seed<T: SvdFloat, M: SparseMat<T>>(a: &M, rank: usize, seed: u64) -> Result<SvdRec<T>> {
159 svd_with(a, &IrlbaConfig::new(rank).seed(seed), None)
160}
161
162pub fn svd_centered<T: SvdFloat, M: SparseMatDense<T>>(
167 a: &M,
168 rank: usize,
169 seed: Option<u64>,
170) -> Result<SvdRec<T>> {
171 let means = a.col_means();
172 let mut cfg = IrlbaConfig::new(rank).mean_center(true);
173 cfg.seed = seed;
174 svd_with(a, &cfg, Some(means))
175}
176
177struct Op<'a, T, M> {
182 a: &'a M,
183 means: Option<&'a [T]>,
185 _p: std::marker::PhantomData<T>,
186}
187
188impl<'a, T: SvdFloat, M: SparseMat<T>> Op<'a, T, M> {
189 fn rows(&self) -> usize {
190 self.a.rows()
191 }
192 fn cols(&self) -> usize {
193 self.a.cols()
194 }
195
196 fn mul(&self, x: &[T], y: &mut [T], trans: bool) {
199 self.a.mul_vec(x, y, trans);
200 let Some(m) = self.means else { return };
201 if !trans {
202 let c: T = m.iter().zip(x.iter()).map(|(&a, &b)| a * b).sum();
204 for yi in y.iter_mut() {
205 *yi -= c;
206 }
207 } else {
208 let sum: T = x.iter().copied().sum();
210 for (yi, &mi) in y.iter_mut().zip(m.iter()) {
211 *yi -= mi * sum;
212 }
213 }
214 }
215}
216
217fn reorthogonalize<T: SvdFloat>(
237 w: &mut Array1<T>,
238 basis: &ArrayView2<T>,
239 count: usize,
240 coeffs: &mut Array1<T>,
241) {
242 if count == 0 {
243 return;
244 }
245 let b = basis.slice(s![..count, ..]);
246 let mut total = Array1::<T>::zeros(count);
247 {
248 let mut c = coeffs.slice_mut(s![..count]);
249 for _ in 0..2 {
250 ndarray::linalg::general_mat_vec_mul(T::one(), &b, w, T::zero(), &mut c);
252 ndarray::linalg::general_mat_vec_mul(-T::one(), &b.t(), &c, T::one(), w);
254 total += &c;
255 }
256 }
257 coeffs.slice_mut(s![..count]).assign(&total);
258}
259
260fn random_orthogonal<T: SvdFloat>(
271 out: &mut Array1<T>,
272 basis: &ArrayView2<T>,
273 count: usize,
274 coeffs: &mut Array1<T>,
275 rng_state: &mut StdRng,
276) -> bool {
277 let floor = T::from_f64_val(0.1);
278 for _ in 0..4 {
279 random_unit(out, rng_state);
280 reorthogonalize(out, basis, count, coeffs);
283 let n = norm(out);
284 if n > floor && num_traits::Float::is_finite(n) {
285 *out /= n;
286 return true;
287 }
288 }
289 false
290}
291
292fn norm<T: SvdFloat>(v: &Array1<T>) -> T {
293 v.iter().map(|&x| x * x).sum::<T>().sqrt()
294}
295
296fn random_unit<T: SvdFloat>(v: &mut Array1<T>, rng_state: &mut StdRng) {
298 for x in v.iter_mut() {
299 *x = T::from_f64_val(rng_state.random_range(-1.0..1.0));
300 }
301 let n = norm(v);
302 if n > T::zero() {
303 *v /= n;
304 } else {
305 v.fill(T::zero());
306 v[0] = T::one();
307 }
308}
309
310pub fn svd_with<T: SvdFloat, M: SparseMat<T>>(
315 a: &M,
316 cfg: &IrlbaConfig,
317 means: Option<Array1<T>>,
318) -> Result<SvdRec<T>> {
319 let (rows, cols) = (a.rows(), a.cols());
320 let min_dim = rows.min(cols);
321
322 if cfg.rank == 0 {
323 return Err(SvdLibError::invalid("irlba: rank must be at least 1"));
324 }
325 if cfg.rank > min_dim {
326 return Err(SvdLibError::invalid(format!(
327 "irlba: rank {} exceeds min(rows, cols) = {min_dim}",
328 cfg.rank
329 )));
330 }
331 if cfg.mean_center {
332 match &means {
333 Some(m) if m.len() == cols => {}
334 Some(m) => {
335 return Err(SvdLibError::shape(format!(
336 "irlba: means has length {} but the matrix has {cols} columns",
337 m.len()
338 )))
339 }
340 None => {
341 return Err(SvdLibError::invalid(
342 "irlba: mean_center is set but no means were supplied; \
343 use `svd_centered`",
344 ))
345 }
346 }
347 }
348
349 let k = cfg.rank;
350 let seed = cfg.seed.unwrap_or_else(|| rng().next_u64());
351
352 if min_dim == 1 {
356 return trivial_rank_one(a, &means, cfg, seed);
357 }
358
359 let work = cfg
360 .work
361 .unwrap_or(k + DEFAULT_EXTRA_WORK)
362 .clamp(k + 1, min_dim.max(k + 1))
363 .min(min_dim);
364 if work <= k {
365 return Err(SvdLibError::invalid(format!(
366 "irlba: rank {k} needs a basis of at least {} vectors but the matrix only \
367 admits {min_dim}; request at most {} triplets, or use \
368 `single_svdlib::randomized`, which supports the full rank",
369 k + 1,
370 min_dim - 1
371 )));
372 }
373
374 let means_slice = if cfg.mean_center {
375 means.as_ref().map(|m| m.as_slice().unwrap())
376 } else {
377 None
378 };
379 let op = Op {
380 a,
381 means: means_slice,
382 _p: std::marker::PhantomData,
383 };
384
385 let mut state = Solve::new(&op, work, k, cfg.tol, seed);
386 let outcome = state.run(cfg.max_restarts)?;
387
388 let Solve { v, u, .. } = state;
390 let SolveOutcome {
391 p,
392 q,
393 sigma,
394 restarts,
395 converged,
396 max_residual,
397 matvecs,
398 } = outcome;
399
400 let pk = p.slice(s![.., ..k]);
403 let qk = q.slice(s![.., ..k]);
404 let mut u_out = pk
405 .t()
406 .dot(&u.slice(s![..work, ..]))
407 .reversed_axes()
408 .to_owned();
409 let mut vt_out = qk.t().dot(&v.slice(s![..work, ..])).to_owned();
410 let s_out = sigma.slice(s![..k]).to_owned();
411
412 if cfg.require_convergence && !converged {
413 return Err(SvdLibError::failed(
414 "irlba",
415 format!(
416 "did not converge in {restarts} restarts: largest residual is {:.3e} \
417 against a threshold of {:.3e} (tol {:.1e} x sigma_max). Raise \
418 `max_restarts` or `work`, loosen `tol`, or call `allow_unconverged` to \
419 accept a best effort.",
420 max_residual.to_f64(),
421 cfg.tol * sigma[0].to_f64(),
422 cfg.tol,
423 ),
424 ));
425 }
426
427 svd_flip(&mut u_out, &mut vt_out);
429
430 Ok(SvdRec {
431 d: k,
432 u: u_out,
433 s: s_out,
434 vt: vt_out,
435 total_squared_norm: T::from_f64_val(crate::matrix::total_squared_norm(
436 a,
437 means.as_ref().map(|m| m.view()),
438 )),
439 diagnostics: Diagnostics {
440 algorithm: Algorithm::Irlba,
441 non_zero: a.nnz(),
442 dimensions: k,
443 significant_values: k,
444 transposed: false,
445 random_seed: seed,
446 matvecs,
447 detail: Detail::Irlba {
448 restarts,
449 converged,
450 tolerance: T::from_f64_val(cfg.tol),
451 max_residual,
452 },
453 },
454 })
455}
456
457fn trivial_rank_one<T: SvdFloat, M: SparseMat<T>>(
463 a: &M,
464 means: &Option<Array1<T>>,
465 cfg: &IrlbaConfig,
466 seed: u64,
467) -> Result<SvdRec<T>> {
468 let (rows, cols) = (a.rows(), a.cols());
469 let op = Op {
470 a,
471 means: if cfg.mean_center {
472 means.as_ref().map(|m| m.as_slice().unwrap())
473 } else {
474 None
475 },
476 _p: std::marker::PhantomData,
477 };
478
479 let (long, short, trans) = if rows == 1 {
481 (cols, rows, true)
482 } else {
483 (rows, cols, false)
484 };
485 let mut probe = vec![T::zero(); short];
486 probe[0] = T::one();
487 let mut vec = vec![T::zero(); long];
488 op.mul(&probe, &mut vec, trans);
489
490 let sigma = vec.iter().map(|&x| x * x).sum::<T>().sqrt();
491 if !num_traits::Float::is_finite(sigma) {
492 return Err(SvdLibError::failed("irlba", "the operand is not finite"));
493 }
494
495 let (u, vt) = if sigma > T::zero() {
496 let unit: Vec<T> = vec.iter().map(|&x| x / sigma).collect();
497 if rows == 1 {
498 (
500 Array2::from_shape_vec((1, 1), vec![T::one()])?,
501 Array2::from_shape_vec((1, cols), unit)?,
502 )
503 } else {
504 (
506 Array2::from_shape_vec((rows, 1), unit)?,
507 Array2::from_shape_vec((1, 1), vec![T::one()])?,
508 )
509 }
510 } else {
511 let mut u = Array2::<T>::zeros((rows, 1));
513 let mut vt = Array2::<T>::zeros((1, cols));
514 u[[0, 0]] = T::one();
515 vt[[0, 0]] = T::one();
516 (u, vt)
517 };
518
519 Ok(SvdRec {
520 d: 1,
521 u,
522 s: Array1::from_vec(vec![sigma]),
523 vt,
524 total_squared_norm: T::from_f64_val(crate::matrix::total_squared_norm(
525 a,
526 if cfg.mean_center {
527 means.as_ref().map(|m| m.view())
528 } else {
529 None
530 },
531 )),
532 diagnostics: Diagnostics {
533 algorithm: Algorithm::Irlba,
534 non_zero: a.nnz(),
535 dimensions: 1,
536 significant_values: 1,
537 transposed: false,
538 random_seed: seed,
539 matvecs: 1,
540 detail: Detail::Irlba {
541 restarts: 0,
542 converged: true,
543 tolerance: T::from_f64_val(cfg.tol),
544 max_residual: T::zero(),
545 },
546 },
547 })
548}
549
550struct SolveOutcome<T> {
551 p: Array2<T>,
553 q: Array2<T>,
555 sigma: Array1<T>,
556 restarts: usize,
557 converged: bool,
558 max_residual: T,
559 matvecs: usize,
560}
561
562struct Solve<'a, T, M> {
565 op: &'a Op<'a, T, M>,
566 work: usize,
567 k: usize,
568 tol: f64,
569 v: Array2<T>,
571 u: Array2<T>,
573 b: Array2<T>,
575 rng: StdRng,
576 matvecs: usize,
577 anorm: T,
582}
583
584impl<'a, T: SvdFloat, M: SparseMat<T>> Solve<'a, T, M> {
585 fn new(op: &'a Op<'a, T, M>, work: usize, k: usize, tol: f64, seed: u64) -> Self {
586 Self {
587 op,
588 work,
589 k,
590 tol,
591 v: Array2::zeros((work + 1, op.cols())),
592 u: Array2::zeros((work, op.rows())),
593 b: Array2::zeros((work, work)),
594 rng: StdRng::seed_from_u64(seed),
595 matvecs: 0,
596 anorm: T::zero(),
597 }
598 }
599
600 fn breakdown_threshold(&self) -> T {
605 let dim = T::from_f64_val((self.op.rows().max(self.op.cols()) as f64).sqrt());
606 self.anorm * T::eps() * dim
607 }
608
609 fn extend(&mut self, start: usize, coupling: Option<&Array1<T>>) -> Result<(T, Array1<T>)> {
617 let (rows, cols) = (self.op.rows(), self.op.cols());
618 let mut w = Array1::<T>::zeros(rows);
619 let mut z = Array1::<T>::zeros(cols);
620 let mut coeffs = Array1::<T>::zeros(self.work + 1);
622
623 for j in start..self.work {
624 {
626 let vj = self.v.row(j).to_owned();
627 self.op
628 .mul(vj.as_slice().unwrap(), w.as_slice_mut().unwrap(), false);
629 self.matvecs += 1;
630 }
631 if j == start && start > 0 {
632 let rho = coupling.expect("restart requires a coupling vector");
633 let uk = self.u.slice(s![..self.k, ..]);
635 w -= &uk.t().dot(rho);
636 } else if j > 0 {
637 let beta_prev = self.b[[j - 1, j]];
638 let uprev = self.u.row(j - 1);
639 w.scaled_add(-beta_prev, &uprev);
640 }
641
642 {
643 let ub = self.u.view();
644 reorthogonalize(&mut w, &ub, j, &mut coeffs);
645 }
646 for i in 0..j {
649 self.b[[i, j]] += coeffs[i];
650 }
651 let alpha = norm(&w);
652 if !num_traits::Float::is_finite(alpha) {
656 return Err(SvdLibError::failed(
657 "irlba",
658 "the left Krylov vector became non-finite; the matrix most likely \
659 contains NaN or infinity",
660 ));
661 }
662 let alpha_kept = if alpha <= self.breakdown_threshold() {
668 let ub = self.u.view();
669 if !random_orthogonal(&mut w, &ub, j, &mut coeffs, &mut self.rng) {
670 return Ok((T::zero(), Array1::zeros(cols)));
673 }
674 T::zero()
675 } else {
676 self.anorm = Float::max(self.anorm, alpha);
677 w /= alpha;
678 alpha
679 };
680 self.u.row_mut(j).assign(&w);
681 self.b[[j, j]] = alpha_kept;
682
683 self.op
685 .mul(w.as_slice().unwrap(), z.as_slice_mut().unwrap(), true);
686 self.matvecs += 1;
687 {
688 let vj = self.v.row(j);
689 z.scaled_add(-alpha_kept, &vj);
690 }
691 {
692 let vb = self.v.view();
693 reorthogonalize(&mut z, &vb, j + 1, &mut coeffs);
694 }
695 let beta = norm(&z);
696 if !num_traits::Float::is_finite(beta) {
697 return Err(SvdLibError::failed(
698 "irlba",
699 "the right Krylov vector became non-finite; the matrix most likely \
700 contains NaN or infinity",
701 ));
702 }
703 let (beta_kept, zn) = if beta <= self.breakdown_threshold() {
704 let vb = self.v.view();
705 if !random_orthogonal(&mut z, &vb, j + 1, &mut coeffs, &mut self.rng) {
706 return Ok((T::zero(), Array1::zeros(cols)));
713 }
714 (T::zero(), z.clone())
715 } else {
716 self.anorm = Float::max(self.anorm, beta);
717 (beta, &z / beta)
718 };
719 self.v.row_mut(j + 1).assign(&zn);
720 if j + 1 < self.work {
721 self.b[[j, j + 1]] = beta_kept;
722 } else {
723 return Ok((beta_kept, zn));
724 }
725 }
726 unreachable!("extend always terminates at the final column")
727 }
728
729 fn run(&mut self, max_restarts: usize) -> Result<SolveOutcome<T>> {
730 {
743 let mut probe = Array1::<T>::zeros(self.op.rows());
744 random_unit(&mut probe, &mut self.rng);
745 let mut v0 = Array1::<T>::zeros(self.op.cols());
746 self.op
747 .mul(probe.as_slice().unwrap(), v0.as_slice_mut().unwrap(), true);
748 self.matvecs += 1;
749
750 let n = norm(&v0);
751 if n > T::zero() && num_traits::Float::is_finite(n) {
752 v0 /= n;
753 } else {
754 random_unit(&mut v0, &mut self.rng);
757 }
758 self.v.row_mut(0).assign(&v0);
759 }
760
761 let mut start = 0usize;
762 let mut coupling: Option<Array1<T>> = None;
763
764 for restart in 0..=max_restarts {
765 let (beta, v_next) = self.extend(start, coupling.as_ref())?;
766
767 let svd = small_svd(self.b.view())?;
768 let sigma = svd.s;
769 let p = svd.u; let q = svd.vt.reversed_axes().as_standard_layout().to_owned(); let smax = Float::max(sigma[0], T::eps());
774 let thresh = T::from_f64_val(self.tol) * smax;
775 let mut max_resid = T::zero();
776 for i in 0..self.k {
777 let r = Float::abs(beta * p[[self.work - 1, i]]);
778 if r > max_resid {
779 max_resid = r;
780 }
781 }
782
783 if max_resid <= thresh || restart == max_restarts {
784 return Ok(SolveOutcome {
785 p,
786 q,
787 sigma,
788 restarts: restart,
789 converged: max_resid <= thresh,
790 max_residual: max_resid,
791 matvecs: self.matvecs,
792 });
793 }
794
795 let vk = q
801 .slice(s![.., ..self.k])
802 .t()
803 .dot(&self.v.slice(s![..self.work, ..]));
804 let uk = p
805 .slice(s![.., ..self.k])
806 .t()
807 .dot(&self.u.slice(s![..self.work, ..]));
808
809 let mut rho = Array1::<T>::zeros(self.k);
810 for i in 0..self.k {
811 rho[i] = beta * p[[self.work - 1, i]];
812 }
813
814 self.v.slice_mut(s![..self.k, ..]).assign(&vk);
815 self.u.slice_mut(s![..self.k, ..]).assign(&uk);
816 self.v.row_mut(self.k).assign(&v_next);
817
818 self.b.fill(T::zero());
819 for i in 0..self.k {
820 self.b[[i, i]] = sigma[i];
821 self.b[[i, self.k]] = rho[i];
822 }
823
824 start = self.k;
825 coupling = Some(rho);
826 }
827 unreachable!("the restart loop returns on its final iteration")
828 }
829}
830
831#[cfg(test)]
832mod tests {
833 use super::*;
834 use crate::matrix::SvdMat;
835 use crate::testing::{dense_of, gen_lowrank, gen_sparse, reference_singular_values, Lcg};
836 use ndarray::Axis;
837 use sprs::TriMatI;
838
839 fn diagonal(n: usize) -> SvdMat<f64> {
840 let mut t = TriMatI::<f64, u32>::new((n, n));
841 for i in 0..n {
842 t.add_triplet(i, i, (n - i) as f64);
843 }
844 t.to_csr::<u64>()
845 }
846
847 fn dense_random(r: usize, c: usize, seed: u64) -> SvdMat<f64> {
848 let mut rng = Lcg::new(seed);
849 let mut t = TriMatI::<f64, u32>::new((r, c));
850 for i in 0..r {
851 for j in 0..c {
852 t.add_triplet(i, j, rng.signed());
853 }
854 }
855 t.to_csr::<u64>()
856 }
857
858 fn assert_matches_lapack(name: &str, a: &SvdMat<f64>, rank: usize, tol: f64) -> SvdRec<f64> {
860 let want = reference_singular_values(&dense_of(a));
861 let got = svd_seed(a, rank, 42).unwrap_or_else(|e| panic!("{name}: {e}"));
862 assert_eq!(got.d, rank, "{name}: rank");
863 for (i, &g) in got.s.iter().enumerate() {
864 let rel = (g - want[i]).abs() / want[i].abs().max(1e-30);
865 assert!(
866 rel < tol,
867 "{name}: singular value {i}: irlba {g:.12e} vs LAPACK {:.12e} (rel {rel:.3e})",
868 want[i]
869 );
870 }
871 got
872 }
873
874 #[test]
875 fn exact_on_diagonal_matrix() {
876 let a = diagonal(40);
878 let got = assert_matches_lapack("diagonal_40", &a, 10, 1e-10);
879 approx::assert_relative_eq!(got.s[0], 40.0, max_relative = 1e-10);
880 approx::assert_relative_eq!(got.s[9], 31.0, max_relative = 1e-10);
881 }
882
883 #[test]
884 fn matches_lapack_on_dense_random() {
885 assert_matches_lapack("dense_random_60x40", &dense_random(60, 40, 3), 10, 1e-9);
886 }
887
888 #[test]
889 fn matches_lapack_on_lowrank() {
890 assert_matches_lapack("lowrank_80x50_r8", &gen_lowrank(80, 50, 8, 21), 8, 1e-9);
891 assert_matches_lapack(
892 "lowrank_200x80_r10",
893 &gen_lowrank(200, 80, 10, 555),
894 15,
895 1e-8,
896 );
897 }
898
899 #[test]
900 fn matches_lapack_on_sparse() {
901 assert_matches_lapack("sparse_500x40", &gen_sparse(500, 40, 0.10, 7), 10, 1e-9);
902 assert_matches_lapack("sparse_200x120", &gen_sparse(200, 120, 0.05, 3), 20, 1e-9);
903 assert_matches_lapack(
904 "sparse_100x100",
905 &gen_sparse(100, 100, 0.0098, 42),
906 20,
907 1e-8,
908 );
909 }
910
911 #[test]
913 fn matches_lapack_on_wide() {
914 assert_matches_lapack("wide_50x400", &gen_sparse(50, 400, 0.05, 1234), 10, 1e-9);
915 }
916
917 #[test]
918 fn orientation_and_reconstruction() {
919 for (r, c) in [(200usize, 60usize), (60, 200)] {
920 let a = gen_sparse(r, c, 0.1, 11);
921 let rank = 10;
922 let svd = svd_seed(&a, rank, 42).unwrap();
923 assert_eq!(svd.u.dim(), (r, rank), "u shape for {r}x{c}");
924 assert_eq!(svd.vt.dim(), (rank, c), "vt shape for {r}x{c}");
925
926 let dense = dense_of(&a);
929 let refs = reference_singular_values(&dense);
930 let tail: f64 = refs[rank..].iter().map(|v| v * v).sum::<f64>().sqrt();
931 let err: f64 = (&svd.recompose() - &dense)
932 .iter()
933 .map(|v| v * v)
934 .sum::<f64>()
935 .sqrt();
936 approx::assert_relative_eq!(err, tail, max_relative = 1e-6);
937 }
938 }
939
940 #[test]
942 fn singular_vectors_are_orthonormal_and_consistent() {
943 let a = gen_sparse(300, 120, 0.06, 17);
944 let rank = 12;
945 let svd = svd_seed(&a, rank, 42).unwrap();
946
947 let orth_u = crate::dense::orthogonality_error(&svd.u.view());
948 assert!(orth_u < 1e-9, "||UᵀU - I|| = {orth_u:.3e}");
949 let vt_t = svd.vt.t().to_owned();
950 let orth_v = crate::dense::orthogonality_error(&vt_t.view());
951 assert!(orth_v < 1e-9, "||VᵀV - I|| = {orth_v:.3e}");
952
953 for i in 0..rank {
955 let vi: Vec<f64> = svd.vt.row(i).to_vec();
956 let mut av = vec![0.0; a.rows()];
957 SparseMat::mul_vec(&a, &vi, &mut av, false);
958 let resid: f64 = av
959 .iter()
960 .zip(svd.u.column(i).iter())
961 .map(|(&x, &ui)| {
962 let d = x - svd.s[i] * ui;
963 d * d
964 })
965 .sum::<f64>()
966 .sqrt();
967 assert!(
968 resid / svd.s[0] < 1e-8,
969 "triplet {i}: ||A v - s u|| / s_max = {:.3e}",
970 resid / svd.s[0]
971 );
972 }
973 }
974
975 #[test]
976 fn csr_and_csc_agree() {
977 let a = gen_sparse(150, 90, 0.08, 5);
978 let csc = a.to_other_storage();
979 let x = svd_seed(&a, 12, 42).unwrap();
980 let y = svd_seed(&csc, 12, 42).unwrap();
981 for (p, q) in x.s.iter().zip(y.s.iter()) {
982 approx::assert_relative_eq!(p, q, max_relative = 1e-10);
983 }
984 }
985
986 #[test]
989 fn mean_centering_matches_dense_pca() {
990 let a = gen_lowrank(120, 40, 6, 31);
991 let dense = dense_of(&a);
992 let means = dense.mean_axis(Axis(0)).unwrap();
993 let centered = &dense - &means.view().insert_axis(Axis(0));
994 let want = reference_singular_values(¢ered);
995
996 let got = svd_centered(&a, 6, Some(42)).unwrap();
997 for (i, &g) in got.s.iter().enumerate() {
998 let rel = (g - want[i]).abs() / want[i].abs().max(1e-30);
999 assert!(
1000 rel < 1e-8,
1001 "centered singular value {i}: {g:.9e} vs {:.9e} (rel {rel:.3e})",
1002 want[i]
1003 );
1004 }
1005 }
1006
1007 #[test]
1008 fn f32_matches_reference_at_f32_precision() {
1009 let a64 = gen_lowrank(100, 50, 6, 77);
1010 let want = reference_singular_values(&dense_of(&a64));
1011 let mut t = TriMatI::<f32, u32>::new((100, 50));
1013 for (v, (i, j)) in a64.iter() {
1014 t.add_triplet(i as usize, j as usize, *v as f32);
1015 }
1016 let a32: SvdMat<f32> = t.to_csr::<u64>();
1017 let got = svd_seed(&a32, 6, 42).unwrap();
1018 for (i, &g) in got.s.iter().enumerate() {
1019 let rel = ((g as f64) - want[i]).abs() / want[i].abs().max(1e-30);
1020 assert!(rel < 1e-4, "f32 singular value {i}: rel {rel:.3e}");
1021 }
1022 }
1023
1024 #[test]
1025 fn reports_convergence_and_bounded_restarts() {
1026 let a = gen_sparse(200, 100, 0.05, 9);
1027 let svd = svd_seed(&a, 10, 42).unwrap();
1028 assert_eq!(svd.diagnostics.algorithm, Algorithm::Irlba);
1029 match svd.diagnostics.detail {
1030 Detail::Irlba {
1031 converged,
1032 restarts,
1033 max_residual,
1034 ..
1035 } => {
1036 assert!(converged, "expected convergence");
1037 assert!(restarts < 50, "unexpectedly many restarts: {restarts}");
1038 assert!(max_residual >= 0.0);
1039 }
1040 ref other => panic!("wrong detail variant: {other:?}"),
1041 }
1042 assert!(svd.diagnostics.matvecs > 0);
1043 }
1044
1045 #[test]
1047 fn tolerance_is_monotone() {
1048 let a = gen_lowrank(150, 60, 8, 44);
1049 let want = reference_singular_values(&dense_of(&a));
1050 let mut prev = f64::INFINITY;
1051 for tol in [1e-4, 1e-8, 1e-12] {
1052 let cfg = IrlbaConfig::new(8).seed(42).tol(tol);
1053 let got = svd_with(&a, &cfg, None).unwrap();
1054 let err = (0..8)
1055 .map(|i| (got.s[i] - want[i]).abs() / want[i])
1056 .fold(0.0f64, f64::max);
1057 assert!(
1058 err <= prev * 10.0 + 1e-12,
1059 "tol {tol:.0e} gave error {err:.3e}, worse than the looser tolerance's {prev:.3e}"
1060 );
1061 prev = err.max(1e-16);
1062 }
1063 }
1064
1065 #[test]
1066 fn rejects_bad_configuration() {
1067 let a = gen_sparse(50, 30, 0.2, 1);
1068 assert!(matches!(svd(&a, 0), Err(SvdLibError::InvalidArgument(_))));
1069 assert!(matches!(svd(&a, 31), Err(SvdLibError::InvalidArgument(_))));
1070 let cfg = IrlbaConfig::new(5).mean_center(true);
1072 assert!(matches!(
1073 svd_with(&a, &cfg, None),
1074 Err(SvdLibError::InvalidArgument(_))
1075 ));
1076 let cfg = IrlbaConfig::new(5).mean_center(true);
1078 assert!(matches!(
1079 svd_with(&a, &cfg, Some(Array1::zeros(7))),
1080 Err(SvdLibError::ShapeMismatch(_))
1081 ));
1082 }
1083
1084 #[test]
1086 fn is_reproducible_given_a_seed() {
1087 let a = gen_sparse(120, 70, 0.1, 23);
1088 let x = svd_seed(&a, 8, 1234).unwrap();
1089 let y = svd_seed(&a, 8, 1234).unwrap();
1090 assert_eq!(x.s, y.s);
1091 assert_eq!(x.u, y.u);
1092 assert_eq!(x.vt, y.vt);
1093 }
1094
1095 #[test]
1097 fn full_rank_request() {
1098 let a = gen_lowrank(30, 20, 20, 88);
1099 assert_matches_lapack("full_rank_30x20", &a, 19, 1e-8);
1100 }
1101}