single_svdlib/
lib.rs

1pub mod legacy;
2pub mod error;
3pub(crate) mod utils;
4
5pub mod randomized;
6
7pub mod lanczos;
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 = lanczos::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 = lanczos::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 = lanczos::masked::MaskedCSRMatrix::new(&csr, mask);
135        let current_svd = lanczos::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 = lanczos::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        let test_matrix = create_sparse_matrix(1000, 250, 0.01); // 1% non-zeros
170
171        let csr = CsrMatrix::from(&test_matrix);
172
173        let result = randomized::randomized_svd(
174            &csr,
175            50,
176            10,
177            3,
178            randomized::PowerIterationNormalizer::QR,
179            false,
180            Some(42),
181            false
182        );
183
184        // Verify the computation succeeds on a highly sparse matrix
185        assert!(
186            result.is_ok(),
187            "Randomized SVD failed on 99% sparse matrix: {:?}",
188            result.err().unwrap()
189        );
190
191        // Additional checks on the result if successful
192        if let Ok(svd_result) = result {
193            // Verify dimensions match expectations
194            assert_eq!(svd_result.d, 50, "Expected rank of 50");
195
196            // Verify singular values are positive and in descending order
197            for i in 0..svd_result.s.len() {
198                assert!(svd_result.s[i] > 0.0, "Singular values should be positive");
199                if i > 0 {
200                    assert!(
201                        svd_result.s[i-1] >= svd_result.s[i],
202                        "Singular values should be in descending order"
203                    );
204                }
205            }
206
207            // Verify basics of U and V dimensions
208            assert_eq!(svd_result.u.nrows(), 50, "U transpose should have 50 rows");
209            assert_eq!(svd_result.u.ncols(), 1000, "U transpose should have 1000 columns");
210            assert_eq!(svd_result.vt.nrows(), 50, "V transpose should have 50 rows");
211            assert_eq!(svd_result.vt.ncols(), 250, "V transpose should have 250 columns");
212
213        }
214    }
215
216    #[test]
217    fn test_randomized_svd_very_large_sparse_matrix() {
218
219        // Create a very large matrix with high sparsity (99%)
220        let test_matrix = create_sparse_matrix(100000, 2500, 0.01); // 1% non-zeros
221
222        // Convert to CSR for processing
223        let csr = CsrMatrix::from(&test_matrix);
224    
225        // Run randomized SVD with reasonable defaults for a sparse matrix
226        let threadpool = ThreadPoolBuilder::new().num_threads(10).build().unwrap();
227        let result = threadpool.install(|| {
228            randomized::randomized_svd(
229                &csr,
230                50,                              // target rank
231                10,                              // oversampling parameter
232                7,                               // power iterations
233                randomized::PowerIterationNormalizer::QR,    // use QR normalization
234                false,
235                Some(42),
236                false// random seed
237            )
238        });
239
240
241        // Simply verify that the computation succeeds on a highly sparse matrix
242        assert!(
243            result.is_ok(),
244            "Randomized SVD failed on 99% sparse matrix: {:?}",
245            result.err().unwrap()
246        );
247    }
248
249    #[test]
250    fn test_randomized_svd_small_sparse_matrix() {
251
252        // Create a very large matrix with high sparsity (99%)
253        let test_matrix = create_sparse_matrix(1000, 250, 0.01); // 1% non-zeros
254
255        // Convert to CSR for processing
256        let csr = CsrMatrix::from(&test_matrix);
257
258        // Run randomized SVD with reasonable defaults for a sparse matrix
259        let threadpool = ThreadPoolBuilder::new().num_threads(10).build().unwrap();
260        let result = threadpool.install(|| {
261            randomized::randomized_svd(
262                &csr,
263                50,                              // target rank
264                10,                              // oversampling parameter
265                2,                               // power iterations
266                randomized::PowerIterationNormalizer::QR,    // use QR normalization
267                false,
268                Some(42),                        // random seed
269                false
270            )
271        });
272
273
274        // Simply verify that the computation succeeds on a highly sparse matrix
275        assert!(
276            result.is_ok(),
277            "Randomized SVD failed on 99% sparse matrix: {:?}",
278            result.err().unwrap()
279        );
280    }
281}