Skip to main content

scirs2_optimize/high_dimensional/
randomized_svd.rs

1//! Randomized SVD (Halko, Martinsson & Tropp, 2011)
2//!
3//! Computes approximate rank-k SVD using random projections.
4//! Useful for dimensionality reduction in high-dimensional optimization.
5//!
6//! ## Algorithm
7//!
8//! 1. Generate random Gaussian matrix Omega (n x (k+p))
9//! 2. Form Y = A * Omega
10//! 3. Power iteration: Y = A * (A^T * Y) repeated `n_power_iterations` times
11//! 4. QR decomposition of Y -> Q
12//! 5. Form B = Q^T * A (small matrix)
13//! 6. SVD of B -> U_hat, S, Vt
14//! 7. U = Q * U_hat
15//!
16//! ## Reference
17//!
18//! - Halko, N., Martinsson, P.G. and Tropp, J.A. (2011).
19//!   "Finding Structure with Randomness: Probabilistic Algorithms for
20//!   Constructing Approximate Matrix Decompositions"
21
22use crate::error::{OptimizeError, OptimizeResult};
23use scirs2_core::random::{rngs::StdRng, RngExt, SeedableRng};
24
25/// Configuration for randomized SVD
26#[derive(Debug, Clone)]
27pub struct RandomizedSVDConfig {
28    /// Target rank k
29    pub rank: usize,
30    /// Extra columns for oversampling (default: 10)
31    pub oversampling: usize,
32    /// Number of power iterations for improved accuracy (default: 2)
33    pub n_power_iterations: usize,
34    /// Random seed for reproducibility
35    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/// Result of randomized SVD decomposition
50#[derive(Debug, Clone)]
51pub struct RandomizedSVDResult {
52    /// Left singular vectors (m x k), stored as rows of inner vecs
53    pub u: Vec<Vec<f64>>,
54    /// Singular values (k)
55    pub s: Vec<f64>,
56    /// Right singular vectors (k x n), stored as rows of inner vecs
57    pub vt: Vec<Vec<f64>>,
58}
59
60/// Generate a random Gaussian matrix of shape (rows x cols)
61fn 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            // Box-Muller transform for standard normal
66            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
74/// Matrix multiply: C = A * B where A is (m x p), B is (p x n)
75fn 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
97/// Transpose a matrix
98fn 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
113/// Modified Gram-Schmidt QR decomposition
114/// Returns (Q, R) where Q is (m x k) orthonormal and R is (k x k) upper triangular
115fn 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    // Work column-major: extract columns of A (each column is length m)
123    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        // r[j][j] = ||cols[j]||
130        let norm: f64 = cols[j].iter().map(|v| v * v).sum::<f64>().sqrt();
131        if norm < 1e-14 {
132            // Near-zero column, keep it zero
133            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        // Normalize column j
141        for v in cols[j].iter_mut() {
142            *v /= norm;
143        }
144        // Orthogonalize remaining columns against column j
145        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    // Build Q (m x k) from the first k orthonormalized columns
160    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
170/// One-sided Jacobi SVD for small matrices.
171/// Computes SVD of B (m x n) where dimensions are small.
172/// Returns (U, S, Vt) where U is (m x k), S is (k,), Vt is (k x n), k = min(m,n)
173///
174/// Uses one-sided Jacobi rotations applied to columns of B.
175/// After convergence, the columns of B*V are orthogonal with norms equal
176/// to the singular values, and U = B*V * diag(1/sigma).
177fn 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    // Work on a copy of B^T stored column-major as cols[j] = j-th column of B
189    // We'll apply right rotations (Givens) to make B^T B diagonal
190    // This is equivalent to one-sided Jacobi SVD
191    let mut work = b.to_vec(); // m x n, row-major
192
193    // V accumulates the right rotations, starts as I_n
194    let mut v = vec![vec![0.0; n]; n];
195    for i in 0..n {
196        v[i][i] = 1.0;
197    }
198
199    // One-sided Jacobi: apply rotations on the right to make columns of B orthogonal
200    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                // Compute elements of B^T B for columns p and q
206                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                // Compute Jacobi rotation angle
222                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                // Apply rotation to columns p and q of work (B)
233                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                // Apply rotation to columns p and q of V
241                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    // Now work = B * V, and columns of work should be orthogonal
256    // Singular values = column norms of work
257    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    // Sort by descending singular value
265    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            // U column = work column / sigma
277            for i in 0..m {
278                u_mat[i][idx] = work[i][col_j] / sv;
279            }
280            // Vt row = V column (transposed)
281            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
290/// Compute randomized SVD of a matrix
291///
292/// Given an m x n matrix A and target rank k, computes an approximate
293/// rank-k SVD: A ~ U * diag(S) * Vt
294///
295/// # Arguments
296/// * `a` - Input matrix (m x n) stored as Vec of rows
297/// * `config` - Configuration parameters
298///
299/// # Returns
300/// * `RandomizedSVDResult` with U (m x k), S (k), Vt (k x n)
301pub 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    // Validate consistent row lengths
319    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); // total sketch columns
339
340    let mut rng = match config.seed {
341        Some(seed) => StdRng::seed_from_u64(seed),
342        None => StdRng::seed_from_u64(0),
343    };
344
345    // Step 1: Generate random Gaussian matrix Omega (n x l)
346    let omega = random_gaussian_matrix(n, l, &mut rng);
347
348    // Step 2: Form Y = A * Omega (m x l)
349    let mut y = mat_mul(a, &omega);
350
351    // Step 3: Power iteration with reorthogonalization for numerical stability
352    let at = transpose(a);
353    for _ in 0..config.n_power_iterations {
354        // z = A^T * Y, then QR(z) for stability
355        let z = mat_mul(&at, &y);
356        let (q_z, _) = qr_gram_schmidt(&z)?;
357        // Y = A * Q_z
358        y = mat_mul(a, &q_z);
359    }
360
361    // Step 4: QR decomposition of Y -> Q (m x l)
362    let (q, _r) = qr_gram_schmidt(&y)?;
363
364    // Step 5: Form B = Q^T * A (l x n)
365    let qt = transpose(&q);
366    let b = mat_mul(&qt, a);
367
368    // Step 6: SVD of B (small matrix, l x n)
369    let (u_hat, sigma, vt_full) = small_svd_jacobi(&b, 100)?;
370
371    // Step 7: U = Q * U_hat (m x k)
372    let u_full = mat_mul(&q, &u_hat);
373
374    // Truncate to rank k
375    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
385/// Compute the low-rank approximation A_k = U * diag(S) * Vt
386///
387/// # Arguments
388/// * `result` - The randomized SVD result
389///
390/// # Returns
391/// The reconstructed matrix as Vec of rows
392pub 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
413/// Compute the Frobenius norm of a matrix
414fn 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
424/// Compute the Frobenius norm of the difference of two matrices
425fn 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 1: Identity matrix SVD
441    #[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        // All singular values should be 1.0
461        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 2: Low-rank matrix approximation
472    #[test]
473    fn test_low_rank_approximation() {
474        // Create a rank-2 matrix using explicit outer products
475        let m = 8;
476        let n = 6;
477        let mut a = vec![vec![0.0; n]; m];
478
479        // First rank-1 component: u1 = [1,0,1,0,...], v1 = [1,1,0,0,...]
480        // sigma1 ~ large
481        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        // Second rank-1 component with smaller scale
489        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        // Reconstruction should capture the rank-2 structure
513        let reconstructed = reconstruct_from_svd(&result);
514        let error = frobenius_diff_norm(&a, &reconstructed);
515        let original_norm = frobenius_norm(&a);
516        // The matrix is exactly rank-2, so rank-2 SVD should reconstruct well
517        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 3: Singular values are non-negative and sorted
529    #[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        // Singular values should be non-negative
548        for &sv in &result.s {
549            assert!(sv >= 0.0, "Singular value {} should be non-negative", sv);
550        }
551
552        // Singular values should be in non-increasing order
553        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 4: U columns are approximately orthonormal
566    #[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        // Check U^T U ~ I_k
586        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 5: Empty matrix error
603    #[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 6: Rank-1 matrix (no power iteration)
612    #[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 6b: Rank-1 matrix (with power iteration)
649    #[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 7: Power iteration improves accuracy
686    #[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        // Power iteration should generally help or at least not hurt much
722        // (allow some tolerance for randomness)
723        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 8: Diagonal matrix SVD gives correct singular values
732    #[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        // Singular values should approximate the diagonal values (sorted descending)
753        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: Direct SVD with rank-deficient small matrix
768    #[test]
769    fn test_small_svd_rank_deficient() {
770        // B is rank-1: only row 0 is nonzero
771        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        // Only 1 nonzero singular value
778        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: Direct SVD of small matrix
794    #[test]
795    fn test_direct_small_svd() {
796        // 2x3 matrix with known SVD
797        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        // Singular values should be 3 and 2 (sorted descending)
801        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        // Reconstruction should match
805        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 9: Inconsistent row lengths error
815    #[test]
816    fn test_inconsistent_rows() {
817        let a = vec![vec![1.0, 2.0], vec![3.0]]; // inconsistent
818        let config = RandomizedSVDConfig::default();
819        let result = randomized_svd(&a, &config);
820        assert!(result.is_err());
821    }
822
823    /// Test 10: reconstruct_from_svd with empty result
824    #[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}