1use crate::dense::{small_svd, svd_flip, tsqr};
29use crate::error::{Result, SvdLibError};
30use crate::matrix::SparseMatDense;
31use crate::types::{Algorithm, Detail, Diagnostics, SvdFloat, SvdRec};
32use ndarray::{s, Array1, Array2, Axis};
33use rand::rngs::StdRng;
34use rand::{rng, Rng, SeedableRng};
35use rand_distr::{Distribution, Normal};
36
37pub const DEFAULT_OVERSAMPLES: usize = 10;
39pub const DEFAULT_POWER_ITERATIONS: usize = 2;
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
44pub enum Normalizer {
45 #[default]
47 Tsqr,
48 ColumnNorm,
51 None,
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum Sketch {
58 PowerIteration { iterations: usize },
62 BlockKrylov { blocks: usize },
67}
68
69impl Default for Sketch {
70 fn default() -> Self {
71 Sketch::PowerIteration {
72 iterations: DEFAULT_POWER_ITERATIONS,
73 }
74 }
75}
76
77#[derive(Debug, Clone)]
82pub struct RandomizedConfig {
83 pub rank: usize,
85 pub oversamples: usize,
87 pub sketch: Sketch,
88 pub normalizer: Normalizer,
89 pub mean_center: bool,
91 pub seed: Option<u64>,
96}
97
98impl RandomizedConfig {
99 pub fn new(rank: usize) -> Self {
100 Self {
101 rank,
102 oversamples: DEFAULT_OVERSAMPLES,
103 sketch: Sketch::default(),
104 normalizer: Normalizer::default(),
105 mean_center: false,
106 seed: None,
107 }
108 }
109 pub fn oversamples(mut self, n: usize) -> Self {
110 self.oversamples = n;
111 self
112 }
113 pub fn power_iterations(mut self, n: usize) -> Self {
114 self.sketch = Sketch::PowerIteration { iterations: n };
115 self
116 }
117 pub fn block_krylov(mut self, blocks: usize) -> Self {
118 self.sketch = Sketch::BlockKrylov { blocks };
119 self
120 }
121 pub fn normalizer(mut self, n: Normalizer) -> Self {
122 self.normalizer = n;
123 self
124 }
125 pub fn mean_center(mut self, yes: bool) -> Self {
126 self.mean_center = yes;
127 self
128 }
129 pub fn seed(mut self, seed: u64) -> Self {
130 self.seed = Some(seed);
131 self
132 }
133}
134
135pub type Progress<'a> = &'a (dyn Fn(&str) + Sync);
140
141pub fn svd<T: SvdFloat, M: SparseMatDense<T>>(a: &M, rank: usize) -> Result<SvdRec<T>> {
143 svd_with(a, &RandomizedConfig::new(rank), None)
144}
145
146pub fn svd_seed<T: SvdFloat, M: SparseMatDense<T>>(
148 a: &M,
149 rank: usize,
150 seed: u64,
151) -> Result<SvdRec<T>> {
152 svd_with(a, &RandomizedConfig::new(rank).seed(seed), None)
153}
154
155pub fn svd_block_krylov<T: SvdFloat, M: SparseMatDense<T>>(
157 a: &M,
158 rank: usize,
159 blocks: usize,
160 seed: Option<u64>,
161) -> Result<SvdRec<T>> {
162 let mut cfg = RandomizedConfig::new(rank).block_krylov(blocks);
163 cfg.seed = seed;
164 svd_with(a, &cfg, None)
165}
166
167pub fn svd_centered<T: SvdFloat, M: SparseMatDense<T>>(
169 a: &M,
170 rank: usize,
171 seed: Option<u64>,
172) -> Result<SvdRec<T>> {
173 let mut cfg = RandomizedConfig::new(rank).mean_center(true);
174 cfg.seed = seed;
175 svd_with(a, &cfg, None)
176}
177
178pub fn svd_with<T: SvdFloat, M: SparseMatDense<T>>(
180 a: &M,
181 cfg: &RandomizedConfig,
182 progress: Option<Progress<'_>>,
183) -> Result<SvdRec<T>> {
184 let note = |msg: &str| {
185 if let Some(p) = progress {
186 p(msg);
187 }
188 };
189
190 let (rows, cols) = (a.rows(), a.cols());
191 let min_dim = rows.min(cols);
192 if cfg.rank == 0 {
193 return Err(SvdLibError::invalid("randomized: rank must be at least 1"));
194 }
195 if cfg.rank > min_dim {
196 return Err(SvdLibError::invalid(format!(
197 "randomized: rank {} exceeds min(rows, cols) = {min_dim}",
198 cfg.rank
199 )));
200 }
201 if let Sketch::BlockKrylov { blocks } = cfg.sketch {
202 if blocks == 0 {
203 return Err(SvdLibError::invalid(
204 "randomized: block_krylov needs at least one block",
205 ));
206 }
207 }
208
209 let rank = cfg.rank;
210 let l = (rank + cfg.oversamples).min(min_dim);
212 let seed = cfg.seed.unwrap_or_else(|| rng().next_u64());
213 let mut rng_state = StdRng::seed_from_u64(seed);
214
215 let means: Option<Array1<T>> = if cfg.mean_center {
216 note("computing column means");
217 Some(a.col_means())
218 } else {
219 None
220 };
221 let mut matvecs = 0usize;
222
223 let mul = |rhs: &Array2<T>, out: &mut Array2<T>, trans: bool| match &means {
225 Some(m) => a.mul_dense_centered(rhs.view(), out.view_mut(), trans, m.view()),
226 None => a.mul_dense(rhs.view(), out.view_mut(), trans),
227 };
228
229 note("drawing the random sketch");
230 let omega = gaussian(cols, l, &mut rng_state);
231
232 let mut basis = match cfg.sketch {
234 Sketch::PowerIteration { iterations } => {
235 note("projecting");
236 let mut y = Array2::<T>::zeros((rows, l));
237 mul(&omega, &mut y, false);
238 matvecs += l;
239 normalize(&mut y, cfg.normalizer)?;
240
241 let mut z = Array2::<T>::zeros((cols, l));
242 for i in 0..iterations {
243 note(&format!("power iteration {}/{}", i + 1, iterations));
244 mul(&y, &mut z, true);
245 matvecs += l;
246 normalize(&mut z, cfg.normalizer)?;
247 mul(&z, &mut y, false);
248 matvecs += l;
249 normalize(&mut y, cfg.normalizer)?;
250 }
251 y
252 }
253 Sketch::BlockKrylov { blocks } => {
254 note("building the Krylov block basis");
255 let width = (blocks * l).min(min_dim);
260 let mut k = Array2::<T>::zeros((rows, width));
261 let mut y = Array2::<T>::zeros((rows, l));
262 let mut z = Array2::<T>::zeros((cols, l));
263
264 mul(&omega, &mut y, false);
265 matvecs += l;
266 normalize(&mut y, cfg.normalizer)?;
267
268 let mut filled = 0usize;
269 for b in 0..blocks {
270 if filled >= width {
271 break;
272 }
273 let take = l.min(width - filled);
274 k.slice_mut(s![.., filled..filled + take])
275 .assign(&y.slice(s![.., ..take]));
276 filled += take;
277 if b + 1 == blocks {
278 break;
279 }
280 note(&format!("krylov block {}/{}", b + 2, blocks));
281 mul(&y, &mut z, true);
282 matvecs += l;
283 normalize(&mut z, cfg.normalizer)?;
284 mul(&z, &mut y, false);
285 matvecs += l;
286 normalize(&mut y, cfg.normalizer)?;
287 }
288 if filled < width {
289 k = k.slice(s![.., ..filled]).to_owned();
290 }
291 k
292 }
293 };
294
295 note("orthonormalising the basis");
296 tsqr(&mut basis)?;
297 let width = basis.ncols();
298
299 note("projecting onto the basis");
304 let mut bt = Array2::<T>::zeros((cols, width));
305 mul(&basis, &mut bt, true);
306 matvecs += width;
307
308 note("reducing");
309 let r_c = tsqr(&mut bt)?; let small = small_svd(r_c.t())?; let keep = rank.min(small.s.len());
314 let u_hat = small.u.slice(s![.., ..keep]);
315 let v_hat = small.vt.slice(s![..keep, ..]).t().to_owned(); let mut u = basis.dot(&u_hat);
318 let mut vt = bt
319 .dot(&v_hat)
320 .reversed_axes()
321 .as_standard_layout()
322 .to_owned();
323 let s = small.s.slice(s![..keep]).to_owned();
324
325 svd_flip(&mut u, &mut vt);
326
327 let (oversamples, power_iterations, block_size) = match cfg.sketch {
328 Sketch::PowerIteration { iterations } => (cfg.oversamples, iterations, l),
329 Sketch::BlockKrylov { blocks } => (cfg.oversamples, blocks, l),
330 };
331
332 Ok(SvdRec {
333 d: keep,
334 u,
335 s,
336 vt,
337 total_squared_norm: T::from_f64_val(crate::matrix::total_squared_norm(
338 a,
339 means.as_ref().map(|m| m.view()),
340 )),
341 diagnostics: Diagnostics {
342 algorithm: match cfg.sketch {
343 Sketch::PowerIteration { .. } => Algorithm::Randomized,
344 Sketch::BlockKrylov { .. } => Algorithm::BlockKrylov,
345 },
346 non_zero: a.nnz(),
347 dimensions: rank,
348 significant_values: keep,
349 transposed: false,
350 random_seed: seed,
351 matvecs,
352 detail: Detail::Randomized {
353 oversamples,
354 power_iterations,
355 block_size,
356 },
357 },
358 })
359}
360
361fn gaussian<T: SvdFloat>(rows: usize, cols: usize, rng: &mut StdRng) -> Array2<T> {
363 let normal = Normal::new(0.0, 1.0).expect("N(0,1) is well-formed");
364 Array2::from_shape_fn((rows, cols), |_| T::from_f64_val(normal.sample(rng)))
365}
366
367fn normalize<T: SvdFloat>(m: &mut Array2<T>, how: Normalizer) -> Result<()> {
368 match how {
369 Normalizer::Tsqr => {
370 tsqr(m)?;
371 Ok(())
372 }
373 Normalizer::ColumnNorm => {
374 let floor = T::from_f64_val(1e-10);
375 for mut col in m.axis_iter_mut(Axis(1)) {
376 let n = col.iter().map(|&x| x * x).sum::<T>().sqrt();
377 if n > floor {
378 let inv = T::one() / n;
379 col.map_inplace(|x| *x *= inv);
380 }
381 }
382 Ok(())
383 }
384 Normalizer::None => Ok(()),
385 }
386}
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391 use crate::matrix::SvdMat;
392 use crate::testing::{dense_of, gen_lowrank, gen_sparse, reference_singular_values, Lcg};
393 use sprs::TriMatI;
394
395 fn diagonal(n: usize) -> SvdMat<f64> {
396 let mut t = TriMatI::<f64, u32>::new((n, n));
397 for i in 0..n {
398 t.add_triplet(i, i, (n - i) as f64);
399 }
400 t.to_csr::<u64>()
401 }
402
403 fn max_rel_error(a: &SvdMat<f64>, got: &SvdRec<f64>) -> f64 {
405 let want = reference_singular_values(&dense_of(a));
406 got.s
407 .iter()
408 .enumerate()
409 .map(|(i, &g)| (g - want[i]).abs() / want[i].abs().max(1e-30))
410 .fold(0.0f64, f64::max)
411 }
412
413 #[test]
416 fn accurate_on_decaying_spectrum() {
417 let a = gen_lowrank(400, 120, 10, 5);
418 let got = svd_seed(&a, 10, 42).unwrap();
419 let err = max_rel_error(&a, &got);
420 assert!(err < 1e-6, "max relative error {err:.3e}");
421 }
422
423 #[test]
428 fn power_iteration_converges_slowly_on_linear_decay() {
429 let a = diagonal(60);
430 let loose = svd_with(
431 &a,
432 &RandomizedConfig::new(10).seed(42).power_iterations(0),
433 None,
434 )
435 .unwrap();
436 let tight = svd_with(
437 &a,
438 &RandomizedConfig::new(10).seed(42).power_iterations(7),
439 None,
440 )
441 .unwrap();
442 let e0 = max_rel_error(&a, &loose);
443 let e7 = max_rel_error(&a, &tight);
444 assert!(
445 e0 > 1e-2,
446 "q=0 should be visibly inaccurate here, got {e0:.3e}"
447 );
448 assert!(
449 e7 < e0 / 50.0,
450 "7 power iterations should improve substantially: {e0:.3e} -> {e7:.3e}"
451 );
452 }
453
454 #[test]
458 fn block_krylov_is_near_exact_on_linear_decay() {
459 let a = diagonal(60);
460 let got = svd_with(
461 &a,
462 &RandomizedConfig::new(10).seed(42).block_krylov(4),
463 None,
464 )
465 .unwrap();
466 let err = max_rel_error(&a, &got);
467 assert!(err < 1e-10, "block krylov max relative error {err:.3e}");
468 }
469
470 #[test]
472 fn power_iterations_improve_accuracy() {
473 let a = gen_sparse(600, 200, 0.05, 13);
474 let mut prev = f64::INFINITY;
475 for q in [0usize, 1, 2, 4, 7] {
476 let cfg = RandomizedConfig::new(15).seed(42).power_iterations(q);
477 let got = svd_with(&a, &cfg, None).unwrap();
478 let err = max_rel_error(&a, &got);
479 assert!(
480 err <= prev * 1.5 + 1e-9,
481 "q={q} error {err:.3e} is worse than q's predecessor {prev:.3e}"
482 );
483 prev = err;
484 }
485 let plain = svd_with(
489 &a,
490 &RandomizedConfig::new(15).seed(42).power_iterations(0),
491 None,
492 )
493 .unwrap();
494 let e0 = max_rel_error(&a, &plain);
495 assert!(
496 prev < e0 / 10.0,
497 "7 power iterations ({prev:.3e}) should be well under the q=0 error ({e0:.3e})"
498 );
499 }
500
501 #[test]
504 fn block_krylov_beats_power_iteration_on_flat_spectrum() {
505 let a = gen_sparse(800, 200, 0.04, 29);
507 let rank = 20;
508
509 let power = svd_with(
510 &a,
511 &RandomizedConfig::new(rank).seed(42).power_iterations(3),
512 None,
513 )
514 .unwrap();
515 let krylov = svd_with(
516 &a,
517 &RandomizedConfig::new(rank).seed(42).block_krylov(4),
518 None,
519 )
520 .unwrap();
521
522 let e_power = max_rel_error(&a, &power);
523 let e_krylov = max_rel_error(&a, &krylov);
524 assert!(
525 e_krylov <= e_power,
526 "block krylov {e_krylov:.3e} did not improve on power iteration {e_power:.3e}"
527 );
528 assert_eq!(krylov.diagnostics.algorithm, Algorithm::BlockKrylov);
529 }
530
531 #[test]
532 fn orientation_is_correct_for_wide_and_tall() {
533 for (r, c) in [(400usize, 80usize), (80, 400)] {
534 let a = gen_sparse(r, c, 0.08, 11);
535 let got = svd_seed(&a, 10, 42).unwrap();
536 assert_eq!(got.u.dim(), (r, 10), "u shape for {r}x{c}");
537 assert_eq!(got.vt.dim(), (10, c), "vt shape for {r}x{c}");
538 }
539 }
540
541 #[test]
542 fn singular_vectors_are_orthonormal() {
543 let a = gen_lowrank(300, 100, 12, 71);
544 let got = svd_seed(&a, 12, 42).unwrap();
545 let ou = crate::dense::orthogonality_error(&got.u.view());
546 assert!(ou < 1e-8, "||UᵀU - I|| = {ou:.3e}");
547 let vt_t = got.vt.t().to_owned();
548 let ov = crate::dense::orthogonality_error(&vt_t.view());
549 assert!(ov < 1e-8, "||VᵀV - I|| = {ov:.3e}");
550 }
551
552 #[test]
555 fn works_on_csr_and_csc_without_panicking() {
556 let a = gen_sparse(300, 120, 0.05, 3);
557 let csc = a.to_other_storage();
558 let x = svd_seed(&a, 10, 42).unwrap();
559 let y = svd_seed(&csc, 10, 42).unwrap();
560 for (p, q) in x.s.iter().zip(y.s.iter()) {
561 approx::assert_relative_eq!(p, q, max_relative = 1e-9);
562 }
563 }
564
565 #[test]
566 fn works_on_masked_matrices() {
567 let a = gen_sparse(300, 60, 0.1, 19);
568 let cols: Vec<usize> = (0..60).filter(|c| c % 2 == 0).collect();
569 let masked = crate::matrix::MaskedCsMat::with_columns(&a, &cols);
570 let got = svd_seed(&masked, 8, 42).unwrap();
571 assert_eq!(got.u.nrows(), 300);
572 assert_eq!(got.vt.ncols(), 30);
573 for w in got.s.to_vec().windows(2) {
574 assert!(w[0] >= w[1]);
575 }
576 }
577
578 #[test]
580 fn mean_centering_matches_dense_pca() {
581 let a = gen_lowrank(300, 60, 8, 37);
582 let dense = dense_of(&a);
583 let means = dense.mean_axis(Axis(0)).unwrap();
584 let centered = &dense - &means.view().insert_axis(Axis(0));
585 let want = reference_singular_values(¢ered);
586
587 let cfg = RandomizedConfig::new(8)
588 .seed(42)
589 .mean_center(true)
590 .power_iterations(5);
591 let got = svd_with(&a, &cfg, None).unwrap();
592 for (i, &g) in got.s.iter().enumerate() {
593 let rel = (g - want[i]).abs() / want[i].abs().max(1e-30);
594 assert!(
595 rel < 1e-5,
596 "centered singular value {i}: {g:.9e} vs {:.9e} (rel {rel:.3e})",
597 want[i]
598 );
599 }
600 }
601
602 #[test]
605 fn unseeded_runs_differ() {
606 let a = gen_sparse(300, 100, 0.05, 47);
607 let cfg = RandomizedConfig::new(6).power_iterations(0);
608 let x = svd_with(&a, &cfg, None).unwrap();
609 let y = svd_with(&a, &cfg, None).unwrap();
610 assert_ne!(
611 x.diagnostics.random_seed, y.diagnostics.random_seed,
612 "an unseeded config produced the same seed twice"
613 );
614 assert_ne!(x.u, y.u, "unseeded runs produced identical bases");
616 }
617
618 #[test]
619 fn seeded_runs_are_reproducible() {
620 let a = gen_sparse(300, 100, 0.05, 51);
621 let x = svd_seed(&a, 8, 999).unwrap();
622 let y = svd_seed(&a, 8, 999).unwrap();
623 assert_eq!(x.s, y.s);
624 assert_eq!(x.u, y.u);
625 assert_eq!(x.vt, y.vt);
626 }
627
628 #[test]
629 fn agrees_with_irlba() {
630 let a = gen_lowrank(400, 150, 12, 61);
631 let rand = svd_with(
632 &a,
633 &RandomizedConfig::new(12).seed(42).power_iterations(6),
634 None,
635 )
636 .unwrap();
637 let exact = crate::irlba::svd_seed(&a, 12, 42).unwrap();
638 for i in 0..12 {
639 let rel = (rand.s[i] - exact.s[i]).abs() / exact.s[i];
640 assert!(rel < 1e-6, "triplet {i}: randomized vs irlba rel {rel:.3e}");
641 }
642 }
643
644 #[test]
645 fn normalizers_all_produce_usable_results() {
646 let a = gen_lowrank(400, 100, 10, 67);
647 for n in [Normalizer::Tsqr, Normalizer::ColumnNorm, Normalizer::None] {
648 let cfg = RandomizedConfig::new(10)
649 .seed(42)
650 .power_iterations(1)
651 .normalizer(n);
652 let got = svd_with(&a, &cfg, None).unwrap();
653 let err = max_rel_error(&a, &got);
654 assert!(err < 1e-2, "{n:?} gave max relative error {err:.3e}");
655 }
656 }
657
658 #[test]
659 fn progress_callback_is_invoked() {
660 let a = gen_sparse(200, 80, 0.1, 73);
661 let seen = std::sync::Mutex::new(Vec::<String>::new());
662 let sink = |msg: &str| seen.lock().unwrap().push(msg.to_string());
663 let cfg = RandomizedConfig::new(6).seed(42).power_iterations(2);
664 svd_with(&a, &cfg, Some(&sink)).unwrap();
665 let msgs = seen.into_inner().unwrap();
666 assert!(!msgs.is_empty(), "no progress reported");
667 assert!(
668 msgs.iter().any(|m| m.contains("power iteration")),
669 "power iterations were not reported: {msgs:?}"
670 );
671 }
672
673 #[test]
676 fn block_krylov_clamps_basis_to_matrix_rank() {
677 let a = gen_sparse(500, 60, 0.08, 7);
679 let got = svd_block_krylov(&a, 12, 4, Some(42)).expect("should clamp, not fail");
680 assert_eq!(got.d, 12);
681 assert_eq!(got.u.dim(), (500, 12));
682 assert_eq!(got.vt.dim(), (12, 60));
683
684 let b = gen_sparse(60, 500, 0.08, 11);
686 let got = svd_block_krylov(&b, 12, 4, Some(42)).expect("should clamp, not fail");
687 assert_eq!(got.u.dim(), (60, 12));
688 assert_eq!(got.vt.dim(), (12, 500));
689 }
690
691 #[test]
692 fn rejects_bad_configuration() {
693 let a = gen_sparse(50, 30, 0.2, 1);
694 assert!(matches!(svd(&a, 0), Err(SvdLibError::InvalidArgument(_))));
695 assert!(matches!(svd(&a, 31), Err(SvdLibError::InvalidArgument(_))));
696 let cfg = RandomizedConfig::new(5).block_krylov(0);
697 assert!(matches!(
698 svd_with(&a, &cfg, None),
699 Err(SvdLibError::InvalidArgument(_))
700 ));
701 }
702
703 #[test]
704 fn f32_works() {
705 let a64 = gen_lowrank(300, 80, 8, 79);
706 let want = reference_singular_values(&dense_of(&a64));
707 let mut t = TriMatI::<f32, u32>::new((300, 80));
708 for (v, (i, j)) in a64.iter() {
709 t.add_triplet(i as usize, j as usize, *v as f32);
710 }
711 let a32: SvdMat<f32> = t.to_csr::<u64>();
712 let cfg = RandomizedConfig::new(8).seed(42).power_iterations(4);
713 let got = svd_with(&a32, &cfg, None).unwrap();
714 for (i, &g) in got.s.iter().enumerate() {
715 let rel = ((g as f64) - want[i]).abs() / want[i].abs().max(1e-30);
716 assert!(rel < 1e-3, "f32 singular value {i}: rel {rel:.3e}");
717 }
718 }
719
720 #[test]
722 fn oversampling_clamps_to_matrix_rank() {
723 let a = gen_sparse(40, 20, 0.3, 83);
724 let cfg = RandomizedConfig::new(5).seed(42).oversamples(1000);
725 let got = svd_with(&a, &cfg, None).unwrap();
726 assert_eq!(got.d, 5);
727 let mut rng = Lcg::new(1);
728 let _ = rng.next_u64();
729 }
730
731 #[test]
732 fn diagnostics_report_matvecs_and_algorithm() {
733 let a = gen_sparse(200, 80, 0.1, 89);
734 let got = svd_seed(&a, 6, 42).unwrap();
735 assert_eq!(got.diagnostics.algorithm, Algorithm::Randomized);
736 assert!(got.diagnostics.matvecs > 0);
737 match got.diagnostics.detail {
738 Detail::Randomized {
739 power_iterations, ..
740 } => {
741 assert_eq!(power_iterations, DEFAULT_POWER_ITERATIONS);
742 }
743 ref other => panic!("wrong detail variant: {other:?}"),
744 }
745 }
746
747 #[test]
751 #[ignore = "diagnostic, run explicitly"]
752 fn report_convergence_rates() {
753 let cases: Vec<(&str, SvdMat<f64>, usize)> = vec![
754 ("diag_60_linear", diagonal(60), 10),
755 ("lowrank_400x120_r10", gen_lowrank(400, 120, 10, 5), 10),
756 ("sparse_600x200_flat", gen_sparse(600, 200, 0.05, 13), 15),
757 ];
758 for (name, a, rank) in cases {
759 let want = reference_singular_values(&dense_of(&a));
760 print!(
761 "{name:<22} sigma_ratio={:.3} ",
762 want[rank] / want[rank - 1]
763 );
764 for q in [0usize, 1, 2, 4, 7] {
765 let cfg = RandomizedConfig::new(rank).seed(42).power_iterations(q);
766 let got = svd_with(&a, &cfg, None).unwrap();
767 print!("q{q}={:.2e} ", max_rel_error(&a, &got));
768 }
769 for b in [2usize, 4] {
770 let cfg = RandomizedConfig::new(rank).seed(42).block_krylov(b);
771 let got = svd_with(&a, &cfg, None).unwrap();
772 print!("bk{b}={:.2e} ", max_rel_error(&a, &got));
773 }
774 println!();
775 }
776 }
777}