1use crate::error::{OptimizeError, OptimizeResult};
23use scirs2_core::random::{rngs::StdRng, RngExt, SeedableRng};
24
25#[derive(Debug, Clone)]
27pub struct RandomizedSVDConfig {
28 pub rank: usize,
30 pub oversampling: usize,
32 pub n_power_iterations: usize,
34 pub seed: Option<u64>,
36}
37
38impl Default for RandomizedSVDConfig {
39 fn default() -> Self {
40 Self {
41 rank: 5,
42 oversampling: 10,
43 n_power_iterations: 2,
44 seed: Some(42),
45 }
46 }
47}
48
49#[derive(Debug, Clone)]
51pub struct RandomizedSVDResult {
52 pub u: Vec<Vec<f64>>,
54 pub s: Vec<f64>,
56 pub vt: Vec<Vec<f64>>,
58}
59
60fn random_gaussian_matrix(rows: usize, cols: usize, rng: &mut StdRng) -> Vec<Vec<f64>> {
62 let mut mat = vec![vec![0.0; cols]; rows];
63 for row in mat.iter_mut() {
64 for val in row.iter_mut() {
65 let u1: f64 = rng.random::<f64>().max(1e-30);
67 let u2: f64 = rng.random::<f64>();
68 *val = (-2.0_f64 * u1.ln()).sqrt() * (2.0_f64 * std::f64::consts::PI * u2).cos();
69 }
70 }
71 mat
72}
73
74fn mat_mul(a: &[Vec<f64>], b: &[Vec<f64>]) -> Vec<Vec<f64>> {
76 let m = a.len();
77 if m == 0 {
78 return vec![];
79 }
80 let p = a[0].len();
81 if p == 0 || b.is_empty() {
82 return vec![vec![]; m];
83 }
84 let n = b[0].len();
85 let mut c = vec![vec![0.0; n]; m];
86 for i in 0..m {
87 for k in 0..p {
88 let a_ik = a[i][k];
89 for j in 0..n {
90 c[i][j] += a_ik * b[k][j];
91 }
92 }
93 }
94 c
95}
96
97fn transpose(a: &[Vec<f64>]) -> Vec<Vec<f64>> {
99 if a.is_empty() {
100 return vec![];
101 }
102 let m = a.len();
103 let n = a[0].len();
104 let mut at = vec![vec![0.0; m]; n];
105 for i in 0..m {
106 for j in 0..n {
107 at[j][i] = a[i][j];
108 }
109 }
110 at
111}
112
113fn qr_gram_schmidt(a: &[Vec<f64>]) -> OptimizeResult<(Vec<Vec<f64>>, Vec<Vec<f64>>)> {
116 let m = a.len();
117 if m == 0 {
118 return Ok((vec![], vec![]));
119 }
120 let n = a[0].len();
121
122 let mut cols: Vec<Vec<f64>> = (0..n).map(|j| (0..m).map(|i| a[i][j]).collect()).collect();
124
125 let k = m.min(n);
126 let mut r = vec![vec![0.0; n]; k];
127
128 for j in 0..k {
129 let norm: f64 = cols[j].iter().map(|v| v * v).sum::<f64>().sqrt();
131 if norm < 1e-14 {
132 r[j][j] = 0.0;
134 for v in cols[j].iter_mut() {
135 *v = 0.0;
136 }
137 continue;
138 }
139 r[j][j] = norm;
140 for v in cols[j].iter_mut() {
142 *v /= norm;
143 }
144 for jj in (j + 1)..n {
146 let dot: f64 = cols[j]
147 .iter()
148 .zip(cols[jj].iter())
149 .map(|(a, b)| a * b)
150 .sum();
151 r[j][jj] = dot;
152 let col_j: Vec<f64> = cols[j].clone();
153 for i in 0..m {
154 cols[jj][i] -= dot * col_j[i];
155 }
156 }
157 }
158
159 let mut q = vec![vec![0.0; k]; m];
161 for i in 0..m {
162 for j in 0..k {
163 q[i][j] = cols[j][i];
164 }
165 }
166
167 Ok((q, r))
168}
169
170fn small_svd_jacobi(
178 b: &[Vec<f64>],
179 max_sweeps: usize,
180) -> OptimizeResult<(Vec<Vec<f64>>, Vec<f64>, Vec<Vec<f64>>)> {
181 let m = b.len();
182 if m == 0 {
183 return Ok((vec![], vec![], vec![]));
184 }
185 let n = b[0].len();
186 let k = m.min(n);
187
188 let mut work = b.to_vec(); let mut v = vec![vec![0.0; n]; n];
195 for i in 0..n {
196 v[i][i] = 1.0;
197 }
198
199 for _sweep in 0..max_sweeps {
201 let mut max_off = 0.0_f64;
202
203 for p in 0..n {
204 for q in (p + 1)..n {
205 let mut app = 0.0;
207 let mut aqq = 0.0;
208 let mut apq = 0.0;
209 for i in 0..m {
210 app += work[i][p] * work[i][p];
211 aqq += work[i][q] * work[i][q];
212 apq += work[i][p] * work[i][q];
213 }
214
215 max_off = max_off.max(apq.abs());
216
217 if apq.abs() < 1e-15 * (app * aqq).sqrt().max(1e-30) {
218 continue;
219 }
220
221 let tau = (aqq - app) / (2.0 * apq);
223 let t = if tau.abs() > 1e15 {
224 1.0 / (2.0 * tau)
225 } else {
226 let sign = if tau >= 0.0 { 1.0 } else { -1.0 };
227 sign / (tau.abs() + (1.0 + tau * tau).sqrt())
228 };
229 let c = 1.0 / (1.0 + t * t).sqrt();
230 let s = t * c;
231
232 for i in 0..m {
234 let bp = work[i][p];
235 let bq = work[i][q];
236 work[i][p] = c * bp - s * bq;
237 work[i][q] = s * bp + c * bq;
238 }
239
240 for i in 0..n {
242 let vp = v[i][p];
243 let vq = v[i][q];
244 v[i][p] = c * vp - s * vq;
245 v[i][q] = s * vp + c * vq;
246 }
247 }
248 }
249
250 if max_off < 1e-14 {
251 break;
252 }
253 }
254
255 let mut col_norms: Vec<(usize, f64)> = (0..n)
258 .map(|j| {
259 let norm = (0..m).map(|i| work[i][j] * work[i][j]).sum::<f64>().sqrt();
260 (j, norm)
261 })
262 .collect();
263
264 col_norms.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
266
267 let mut sigma = vec![0.0; k];
268 let mut u_mat = vec![vec![0.0; k]; m];
269 let mut vt = vec![vec![0.0; n]; k];
270
271 for idx in 0..k {
272 let (col_j, sv) = col_norms[idx];
273 sigma[idx] = sv;
274
275 if sv > 1e-14 {
276 for i in 0..m {
278 u_mat[i][idx] = work[i][col_j] / sv;
279 }
280 for i in 0..n {
282 vt[idx][i] = v[i][col_j];
283 }
284 }
285 }
286
287 Ok((u_mat, sigma, vt))
288}
289
290pub fn randomized_svd(
302 a: &[Vec<f64>],
303 config: &RandomizedSVDConfig,
304) -> OptimizeResult<RandomizedSVDResult> {
305 let m = a.len();
306 if m == 0 {
307 return Err(OptimizeError::InvalidInput(
308 "Input matrix must have at least one row".to_string(),
309 ));
310 }
311 let n = a[0].len();
312 if n == 0 {
313 return Err(OptimizeError::InvalidInput(
314 "Input matrix must have at least one column".to_string(),
315 ));
316 }
317
318 for (i, row) in a.iter().enumerate() {
320 if row.len() != n {
321 return Err(OptimizeError::InvalidInput(format!(
322 "Row {} has length {} but expected {}",
323 i,
324 row.len(),
325 n
326 )));
327 }
328 }
329
330 let k = config.rank.min(m).min(n);
331 if k == 0 {
332 return Err(OptimizeError::InvalidInput(
333 "Target rank must be at least 1".to_string(),
334 ));
335 }
336
337 let p = config.oversampling;
338 let l = (k + p).min(m).min(n); let mut rng = match config.seed {
341 Some(seed) => StdRng::seed_from_u64(seed),
342 None => StdRng::seed_from_u64(0),
343 };
344
345 let omega = random_gaussian_matrix(n, l, &mut rng);
347
348 let mut y = mat_mul(a, &omega);
350
351 let at = transpose(a);
353 for _ in 0..config.n_power_iterations {
354 let z = mat_mul(&at, &y);
356 let (q_z, _) = qr_gram_schmidt(&z)?;
357 y = mat_mul(a, &q_z);
359 }
360
361 let (q, _r) = qr_gram_schmidt(&y)?;
363
364 let qt = transpose(&q);
366 let b = mat_mul(&qt, a);
367
368 let (u_hat, sigma, vt_full) = small_svd_jacobi(&b, 100)?;
370
371 let u_full = mat_mul(&q, &u_hat);
373
374 let actual_k = k.min(sigma.len());
376 let s: Vec<f64> = sigma[..actual_k].to_vec();
377
378 let u: Vec<Vec<f64>> = u_full.iter().map(|row| row[..actual_k].to_vec()).collect();
379
380 let vt: Vec<Vec<f64>> = vt_full[..actual_k].iter().cloned().collect();
381
382 Ok(RandomizedSVDResult { u, s, vt })
383}
384
385pub fn reconstruct_from_svd(result: &RandomizedSVDResult) -> Vec<Vec<f64>> {
393 let m = result.u.len();
394 if m == 0 || result.s.is_empty() || result.vt.is_empty() {
395 return vec![];
396 }
397 let n = result.vt[0].len();
398 let k = result.s.len();
399
400 let mut reconstructed = vec![vec![0.0; n]; m];
401 for i in 0..m {
402 for j in 0..n {
403 let mut val = 0.0;
404 for r in 0..k {
405 val += result.u[i][r] * result.s[r] * result.vt[r][j];
406 }
407 reconstructed[i][j] = val;
408 }
409 }
410 reconstructed
411}
412
413fn frobenius_norm(a: &[Vec<f64>]) -> f64 {
415 let mut sum = 0.0;
416 for row in a {
417 for &val in row {
418 sum += val * val;
419 }
420 }
421 sum.sqrt()
422}
423
424fn frobenius_diff_norm(a: &[Vec<f64>], b: &[Vec<f64>]) -> f64 {
426 let mut sum = 0.0;
427 for (row_a, row_b) in a.iter().zip(b.iter()) {
428 for (&va, &vb) in row_a.iter().zip(row_b.iter()) {
429 let d = va - vb;
430 sum += d * d;
431 }
432 }
433 sum.sqrt()
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439
440 #[test]
442 fn test_identity_svd() {
443 let n = 5;
444 let mut identity = vec![vec![0.0; n]; n];
445 for i in 0..n {
446 identity[i][i] = 1.0;
447 }
448
449 let config = RandomizedSVDConfig {
450 rank: 5,
451 oversampling: 5,
452 n_power_iterations: 2,
453 seed: Some(42),
454 };
455
456 let result = randomized_svd(&identity, &config);
457 assert!(result.is_ok());
458 let result = result.expect("SVD should succeed");
459
460 assert_eq!(result.s.len(), 5);
462 for &sv in &result.s {
463 assert!(
464 (sv - 1.0).abs() < 0.1,
465 "Singular value {} should be ~1.0",
466 sv
467 );
468 }
469 }
470
471 #[test]
473 fn test_low_rank_approximation() {
474 let m = 8;
476 let n = 6;
477 let mut a = vec![vec![0.0; n]; m];
478
479 for i in 0..m {
482 for j in 0..n {
483 let ui = if i % 2 == 0 { 3.0 } else { 0.0 };
484 let vj = if j < n / 2 { 2.0 } else { 0.0 };
485 a[i][j] += ui * vj;
486 }
487 }
488 for i in 0..m {
490 for j in 0..n {
491 let ui = if i < m / 2 { 1.0 } else { -1.0 };
492 let vj = if j % 2 == 0 { 0.5 } else { -0.5 };
493 a[i][j] += ui * vj;
494 }
495 }
496
497 let config = RandomizedSVDConfig {
498 rank: 2,
499 oversampling: 10,
500 n_power_iterations: 3,
501 seed: Some(123),
502 };
503
504 let result = randomized_svd(&a, &config);
505 assert!(result.is_ok());
506 let result = result.expect("SVD should succeed");
507
508 assert_eq!(result.s.len(), 2);
509 assert_eq!(result.u.len(), m);
510 assert_eq!(result.vt.len(), 2);
511
512 let reconstructed = reconstruct_from_svd(&result);
514 let error = frobenius_diff_norm(&a, &reconstructed);
515 let original_norm = frobenius_norm(&a);
516 let relative_error = error / original_norm.max(1e-14);
518
519 assert!(
520 relative_error < 0.5,
521 "Relative reconstruction error {} is too large (error={}, norm={})",
522 relative_error,
523 error,
524 original_norm
525 );
526 }
527
528 #[test]
530 fn test_singular_values_sorted() {
531 let m = 6;
532 let n = 4;
533 let mut rng = StdRng::seed_from_u64(77);
534 let a = random_gaussian_matrix(m, n, &mut rng);
535
536 let config = RandomizedSVDConfig {
537 rank: 3,
538 oversampling: 2,
539 n_power_iterations: 1,
540 seed: Some(42),
541 };
542
543 let result = randomized_svd(&a, &config);
544 assert!(result.is_ok());
545 let result = result.expect("SVD should succeed");
546
547 for &sv in &result.s {
549 assert!(sv >= 0.0, "Singular value {} should be non-negative", sv);
550 }
551
552 for i in 1..result.s.len() {
554 assert!(
555 result.s[i] <= result.s[i - 1] + 1e-10,
556 "Singular values not sorted: s[{}]={} > s[{}]={}",
557 i,
558 result.s[i],
559 i - 1,
560 result.s[i - 1]
561 );
562 }
563 }
564
565 #[test]
567 fn test_u_orthonormality() {
568 let m = 8;
569 let n = 6;
570 let mut rng = StdRng::seed_from_u64(55);
571 let a = random_gaussian_matrix(m, n, &mut rng);
572
573 let config = RandomizedSVDConfig {
574 rank: 3,
575 oversampling: 3,
576 n_power_iterations: 2,
577 seed: Some(42),
578 };
579
580 let result = randomized_svd(&a, &config);
581 assert!(result.is_ok());
582 let result = result.expect("SVD should succeed");
583
584 let k = result.s.len();
585 for i in 0..k {
587 for j in 0..k {
588 let dot: f64 = (0..m).map(|r| result.u[r][i] * result.u[r][j]).sum();
589 let expected = if i == j { 1.0 } else { 0.0 };
590 assert!(
591 (dot - expected).abs() < 0.3,
592 "U^T U[{},{}] = {}, expected {}",
593 i,
594 j,
595 dot,
596 expected
597 );
598 }
599 }
600 }
601
602 #[test]
604 fn test_empty_matrix_error() {
605 let a: Vec<Vec<f64>> = vec![];
606 let config = RandomizedSVDConfig::default();
607 let result = randomized_svd(&a, &config);
608 assert!(result.is_err());
609 }
610
611 #[test]
613 fn test_rank_1_matrix_no_power() {
614 let a = vec![
615 vec![1.0, 1.0, 1.0],
616 vec![2.0, 2.0, 2.0],
617 vec![3.0, 3.0, 3.0],
618 vec![4.0, 4.0, 4.0],
619 ];
620
621 let config = RandomizedSVDConfig {
622 rank: 1,
623 oversampling: 5,
624 n_power_iterations: 0,
625 seed: Some(42),
626 };
627
628 let result = randomized_svd(&a, &config);
629 assert!(result.is_ok());
630 let result = result.expect("SVD should succeed");
631
632 assert_eq!(result.s.len(), 1);
633 assert!(result.s[0] > 0.0, "Singular value should be positive");
634
635 let reconstructed = reconstruct_from_svd(&result);
636 let error = frobenius_diff_norm(&a, &reconstructed);
637 let original_norm = frobenius_norm(&a);
638 let relative_error = error / original_norm.max(1e-14);
639 assert!(
640 relative_error < 0.15,
641 "Rank-1 reconstruction relative error {} too large (error={}, norm={})",
642 relative_error,
643 error,
644 original_norm
645 );
646 }
647
648 #[test]
650 fn test_rank_1_matrix() {
651 let a = vec![
652 vec![1.0, 1.0, 1.0],
653 vec![2.0, 2.0, 2.0],
654 vec![3.0, 3.0, 3.0],
655 vec![4.0, 4.0, 4.0],
656 ];
657
658 let config = RandomizedSVDConfig {
659 rank: 1,
660 oversampling: 5,
661 n_power_iterations: 2,
662 seed: Some(42),
663 };
664
665 let result = randomized_svd(&a, &config);
666 assert!(result.is_ok());
667 let result = result.expect("SVD should succeed");
668
669 assert_eq!(result.s.len(), 1);
670 assert!(result.s[0] > 0.0, "Singular value should be positive");
671
672 let reconstructed = reconstruct_from_svd(&result);
673 let error = frobenius_diff_norm(&a, &reconstructed);
674 let original_norm = frobenius_norm(&a);
675 let relative_error = error / original_norm.max(1e-14);
676 assert!(
677 relative_error < 0.15,
678 "Rank-1 reconstruction relative error {} too large (error={}, norm={})",
679 relative_error,
680 error,
681 original_norm
682 );
683 }
684
685 #[test]
687 fn test_power_iteration_improvement() {
688 let m = 10;
689 let n = 8;
690 let mut rng = StdRng::seed_from_u64(99);
691 let a = random_gaussian_matrix(m, n, &mut rng);
692
693 let config_no_power = RandomizedSVDConfig {
694 rank: 3,
695 oversampling: 5,
696 n_power_iterations: 0,
697 seed: Some(42),
698 };
699
700 let config_with_power = RandomizedSVDConfig {
701 rank: 3,
702 oversampling: 5,
703 n_power_iterations: 3,
704 seed: Some(42),
705 };
706
707 let result_no = randomized_svd(&a, &config_no_power);
708 let result_with = randomized_svd(&a, &config_with_power);
709 assert!(result_no.is_ok());
710 assert!(result_with.is_ok());
711
712 let r_no = result_no.expect("should succeed");
713 let r_with = result_with.expect("should succeed");
714
715 let recon_no = reconstruct_from_svd(&r_no);
716 let recon_with = reconstruct_from_svd(&r_with);
717
718 let err_no = frobenius_diff_norm(&a, &recon_no);
719 let err_with = frobenius_diff_norm(&a, &recon_with);
720
721 assert!(
724 err_with <= err_no * 1.5 + 1e-10,
725 "Power iteration made things worse: {} vs {}",
726 err_with,
727 err_no
728 );
729 }
730
731 #[test]
733 fn test_diagonal_matrix() {
734 let diag_vals = vec![5.0, 3.0, 1.0, 0.1];
735 let n = diag_vals.len();
736 let mut a = vec![vec![0.0; n]; n];
737 for i in 0..n {
738 a[i][i] = diag_vals[i];
739 }
740
741 let config = RandomizedSVDConfig {
742 rank: 4,
743 oversampling: 5,
744 n_power_iterations: 3,
745 seed: Some(42),
746 };
747
748 let result = randomized_svd(&a, &config);
749 assert!(result.is_ok());
750 let result = result.expect("SVD should succeed");
751
752 let mut sorted_diag = diag_vals.clone();
754 sorted_diag.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
755
756 for (i, (&computed, &expected)) in result.s.iter().zip(sorted_diag.iter()).enumerate() {
757 assert!(
758 (computed - expected).abs() < 0.5,
759 "s[{}]={}, expected ~{}",
760 i,
761 computed,
762 expected
763 );
764 }
765 }
766
767 #[test]
769 fn test_small_svd_rank_deficient() {
770 let b = vec![
772 vec![5.0, 5.0, 5.0],
773 vec![0.0, 0.0, 0.0],
774 vec![0.0, 0.0, 0.0],
775 ];
776 let (u, s, vt) = small_svd_jacobi(&b, 100).expect("SVD should work");
777 assert!(s[0] > 1.0, "s[0]={} should be ~8.66", s[0]);
779
780 let recon = reconstruct_from_svd(&RandomizedSVDResult {
781 u: u.clone(),
782 s: s.clone(),
783 vt: vt.clone(),
784 });
785 let err = frobenius_diff_norm(&b, &recon);
786 assert!(
787 err < 1e-6,
788 "Rank-deficient SVD reconstruction error: {}",
789 err
790 );
791 }
792
793 #[test]
795 fn test_direct_small_svd() {
796 let b = vec![vec![3.0, 0.0, 0.0], vec![0.0, 2.0, 0.0]];
798 let (u, s, vt) = small_svd_jacobi(&b, 100).expect("SVD should work");
799 assert_eq!(s.len(), 2);
800 assert!((s[0] - 3.0).abs() < 0.01, "s[0]={}", s[0]);
802 assert!((s[1] - 2.0).abs() < 0.01, "s[1]={}", s[1]);
803
804 let recon = reconstruct_from_svd(&RandomizedSVDResult {
806 u: u.clone(),
807 s: s.clone(),
808 vt: vt.clone(),
809 });
810 let err = frobenius_diff_norm(&b, &recon);
811 assert!(err < 1e-10, "SVD reconstruction error: {}", err);
812 }
813
814 #[test]
816 fn test_inconsistent_rows() {
817 let a = vec![vec![1.0, 2.0], vec![3.0]]; let config = RandomizedSVDConfig::default();
819 let result = randomized_svd(&a, &config);
820 assert!(result.is_err());
821 }
822
823 #[test]
825 fn test_reconstruct_empty() {
826 let result = RandomizedSVDResult {
827 u: vec![],
828 s: vec![],
829 vt: vec![],
830 };
831 let recon = reconstruct_from_svd(&result);
832 assert!(recon.is_empty());
833 }
834}