single_svdlib/
lib.rs

1pub mod legacy;
2pub mod error;
3pub(crate) mod utils;
4
5pub mod randomized;
6
7pub mod laczos;
8
9pub use utils::*;
10
11
12#[cfg(test)]
13mod simple_comparison_tests {
14    use super::*;
15    use legacy;
16    use nalgebra_sparse::coo::CooMatrix;
17    use nalgebra_sparse::CsrMatrix;
18    use rand::{Rng, SeedableRng};
19    use rand::rngs::StdRng;
20    use rayon::ThreadPoolBuilder;
21
22    fn create_sparse_matrix(rows: usize, cols: usize, density: f64) -> nalgebra_sparse::coo::CooMatrix<f64> {
23        use rand::{rngs::StdRng, Rng, SeedableRng};
24        use std::collections::HashSet;
25
26        let mut coo = nalgebra_sparse::coo::CooMatrix::new(rows, cols);
27
28        let mut rng = StdRng::seed_from_u64(42);
29
30        let nnz = (rows as f64 * cols as f64 * density).round() as usize;
31
32        let nnz = nnz.max(1);
33
34        let mut positions = HashSet::new();
35
36        while positions.len() < nnz {
37            let i = rng.gen_range(0..rows);
38            let j = rng.gen_range(0..cols);
39
40            if positions.insert((i, j)) {
41                let val = loop {
42                    let v: f64 = rng.gen_range(-10.0..10.0);
43                    if v.abs() > 1e-10 { // Ensure it's not too close to zero
44                        break v;
45                    }
46                };
47
48                coo.push(i, j, val);
49            }
50        }
51
52        // Verify the density is as expected
53        let actual_density = coo.nnz() as f64 / (rows as f64 * cols as f64);
54        println!("Created sparse matrix: {} x {}", rows, cols);
55        println!("  - Requested density: {:.6}", density);
56        println!("  - Actual density: {:.6}", actual_density);
57        println!("  - Sparsity: {:.4}%", (1.0 - actual_density) * 100.0);
58        println!("  - Non-zeros: {}", coo.nnz());
59
60        coo
61    }
62    //#[test]
63    fn simple_matrix_comparison() {
64        // Create a small, predefined test matrix
65        let mut test_matrix = CooMatrix::<f64>::new(3, 3);
66        test_matrix.push(0, 0, 1.0);
67        test_matrix.push(0, 1, 16.0);
68        test_matrix.push(0, 2, 49.0);
69        test_matrix.push(1, 0, 4.0);
70        test_matrix.push(1, 1, 25.0);
71        test_matrix.push(1, 2, 64.0);
72        test_matrix.push(2, 0, 9.0);
73        test_matrix.push(2, 1, 36.0);
74        test_matrix.push(2, 2, 81.0);
75
76        // Run both implementations with the same seed for deterministic behavior
77        let seed = 42;
78        let current_result = laczos::svd_dim_seed(&test_matrix, 0, seed).unwrap();
79        let legacy_result = legacy::svd_dim_seed(&test_matrix, 0, seed).unwrap();
80
81        // Compare dimensions
82        assert_eq!(current_result.d, legacy_result.d);
83
84        // Compare singular values
85        let epsilon = 1.0e-12;
86        for i in 0..current_result.d {
87            let diff = (current_result.s[i] - legacy_result.s[i]).abs();
88            assert!(
89                diff < epsilon,
90                "Singular value {} differs by {}: current = {}, legacy = {}",
91                i, diff, current_result.s[i], legacy_result.s[i]
92            );
93        }
94
95        // Compare reconstructed matrices
96        let current_reconstructed = current_result.recompose();
97        let legacy_reconstructed = legacy_result.recompose();
98
99        for i in 0..3 {
100            for j in 0..3 {
101                let diff = (current_reconstructed[[i, j]] - legacy_reconstructed[[i, j]]).abs();
102                assert!(
103                    diff < epsilon,
104                    "Reconstructed matrix element [{},{}] differs by {}: current = {}, legacy = {}",
105                    i, j, diff, current_reconstructed[[i, j]], legacy_reconstructed[[i, j]]
106                );
107            }
108        }
109    }
110
111    #[test]
112    fn random_matrix_comparison() {
113        let seed = 12345;
114        let (nrows, ncols) = (50, 30);
115        let mut rng = StdRng::seed_from_u64(seed);
116
117        // Create random sparse matrix
118        let mut coo = CooMatrix::<f64>::new(nrows, ncols);
119        // Insert some random non-zero elements
120        for _ in 0..(nrows * ncols / 5) {  // ~20% density
121            let i = rng.gen_range(0..nrows);
122            let j = rng.gen_range(0..ncols);
123            let value = rng.gen_range(-10.0..10.0);
124            coo.push(i, j, value);
125        }
126
127        let csr = CsrMatrix::from(&coo);
128
129        // Calculate SVD using original method
130        let legacy_svd = laczos::svd_dim_seed(&csr, 0, seed as u32).unwrap();
131
132        // Calculate SVD using our masked method (using all columns)
133        let mask = vec![true; ncols];
134        let masked_matrix = laczos::masked::MaskedCSRMatrix::new(&csr, mask);
135        let current_svd = laczos::svd_dim_seed(&masked_matrix, 0, seed as u32).unwrap();
136
137        // Compare with relative tolerance
138        let rel_tol = 1e-3;  // 0.1% relative tolerance
139
140        assert_eq!(legacy_svd.d, current_svd.d, "Ranks differ");
141
142        for i in 0..legacy_svd.d {
143            let legacy_val = legacy_svd.s[i];
144            let current_val = current_svd.s[i];
145            let abs_diff = (legacy_val - current_val).abs();
146            let rel_diff = abs_diff / legacy_val.max(current_val);
147
148            assert!(
149                rel_diff <= rel_tol,
150                "Singular value {} differs too much: relative diff = {}, current = {}, legacy = {}",
151                i, rel_diff, current_val, legacy_val
152            );
153        }
154    }
155
156    #[test]
157    fn test_real_sparse_matrix() {
158        // Create a matrix with similar sparsity to your real one (99.02%)
159        let test_matrix = create_sparse_matrix(100, 100, 0.0098); // 0.98% non-zeros
160        
161        // Should no longer fail with convergence error
162        let result = laczos::svd_dim_seed(&test_matrix, 50, 42);
163        assert!(result.is_ok(), "{}", format!("SVD failed on 99.02% sparse matrix, {:?}", result.err().unwrap()));
164    }
165
166    #[test]
167    fn test_random_svd_computation() {
168
169        // Create a matrix with high sparsity (99%)
170        let test_matrix = create_sparse_matrix(1000, 250, 0.01); // 1% non-zeros
171
172        // Convert to CSR for processing
173        let csr = CsrMatrix::from(&test_matrix);
174
175        // Run randomized SVD with reasonable defaults for a sparse matrix
176        let result = randomized::randomized_svd(
177            &csr,
178            50,                              // target rank
179            10,                              // oversampling parameter
180            3,                               // power iterations
181            randomized::PowerIterationNormalizer::QR,    // use QR normalization
182            Some(42),                        // random seed
183        );
184
185        // Verify the computation succeeds on a highly sparse matrix
186        assert!(
187            result.is_ok(),
188            "Randomized SVD failed on 99% sparse matrix: {:?}",
189            result.err().unwrap()
190        );
191
192        // Additional checks on the result if successful
193        if let Ok(svd_result) = result {
194            // Verify dimensions match expectations
195            assert_eq!(svd_result.d, 50, "Expected rank of 50");
196
197            // Verify singular values are positive and in descending order
198            for i in 0..svd_result.s.len() {
199                assert!(svd_result.s[i] > 0.0, "Singular values should be positive");
200                if i > 0 {
201                    assert!(
202                        svd_result.s[i-1] >= svd_result.s[i],
203                        "Singular values should be in descending order"
204                    );
205                }
206            }
207
208            // Verify basics of U and V dimensions
209            assert_eq!(svd_result.ut.nrows(), 50, "U transpose should have 50 rows");
210            assert_eq!(svd_result.ut.ncols(), 1000, "U transpose should have 1000 columns");
211            assert_eq!(svd_result.vt.nrows(), 50, "V transpose should have 50 rows");
212            assert_eq!(svd_result.vt.ncols(), 250, "V transpose should have 250 columns");
213
214        }
215    }
216
217    #[test]
218    fn test_randomized_svd_very_large_sparse_matrix() {
219
220        // Create a very large matrix with high sparsity (99%)
221        let test_matrix = create_sparse_matrix(100000, 2500, 0.01); // 1% non-zeros
222
223        // Convert to CSR for processing
224        let csr = CsrMatrix::from(&test_matrix);
225    
226        // Run randomized SVD with reasonable defaults for a sparse matrix
227        let threadpool = ThreadPoolBuilder::new().num_threads(10).build().unwrap();
228        let result = threadpool.install(|| {
229            randomized::randomized_svd(
230                &csr,
231                50,                              // target rank
232                10,                              // oversampling parameter
233                7,                               // power iterations
234                randomized::PowerIterationNormalizer::QR,    // use QR normalization
235                Some(42),                        // random seed
236            )
237        });
238
239
240        // Simply verify that the computation succeeds on a highly sparse matrix
241        assert!(
242            result.is_ok(),
243            "Randomized SVD failed on 99% sparse matrix: {:?}",
244            result.err().unwrap()
245        );
246    }
247
248    #[test]
249    fn test_randomized_svd_small_sparse_matrix() {
250
251        // Create a very large matrix with high sparsity (99%)
252        let test_matrix = create_sparse_matrix(1000, 250, 0.01); // 1% non-zeros
253
254        // Convert to CSR for processing
255        let csr = CsrMatrix::from(&test_matrix);
256
257        // Run randomized SVD with reasonable defaults for a sparse matrix
258        let threadpool = ThreadPoolBuilder::new().num_threads(10).build().unwrap();
259        let result = threadpool.install(|| {
260            randomized::randomized_svd(
261                &csr,
262                50,                              // target rank
263                10,                              // oversampling parameter
264                2,                               // power iterations
265                randomized::PowerIterationNormalizer::QR,    // use QR normalization
266                Some(42),                        // random seed
267            )
268        });
269
270
271        // Simply verify that the computation succeeds on a highly sparse matrix
272        assert!(
273            result.is_ok(),
274            "Randomized SVD failed on 99% sparse matrix: {:?}",
275            result.err().unwrap()
276        );
277    }
278}