single_svdlib/
lib.rs

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