single_svdlib/
lib.rs

1pub mod legacy;
2pub mod error;
3mod new;
4mod masked;
5
6pub use new::*;
7pub use masked::*;
8
9#[cfg(test)]
10mod simple_comparison_tests {
11    use super::*;
12    use legacy;
13    use nalgebra_sparse::coo::CooMatrix;
14    use nalgebra_sparse::CsrMatrix;
15    use rand::{Rng, SeedableRng};
16    use rand::rngs::StdRng;
17
18    fn create_sparse_matrix(rows: usize, cols: usize, density: f64) -> nalgebra_sparse::coo::CooMatrix<f64> {
19        use rand::{rngs::StdRng, Rng, SeedableRng};
20        use std::collections::HashSet;
21
22        let mut coo = nalgebra_sparse::coo::CooMatrix::new(rows, cols);
23
24        let mut rng = StdRng::seed_from_u64(42);
25
26        let nnz = (rows as f64 * cols as f64 * density).round() as usize;
27
28        let nnz = nnz.max(1);
29
30        let mut positions = HashSet::new();
31
32        while positions.len() < nnz {
33            let i = rng.gen_range(0..rows);
34            let j = rng.gen_range(0..cols);
35
36            if positions.insert((i, j)) {
37                let val = loop {
38                    let v: f64 = rng.gen_range(-10.0..10.0);
39                    if v.abs() > 1e-10 { // Ensure it's not too close to zero
40                        break v;
41                    }
42                };
43
44                coo.push(i, j, val);
45            }
46        }
47
48        // Verify the density is as expected
49        let actual_density = coo.nnz() as f64 / (rows as f64 * cols as f64);
50        println!("Created sparse matrix: {} x {}", rows, cols);
51        println!("  - Requested density: {:.6}", density);
52        println!("  - Actual density: {:.6}", actual_density);
53        println!("  - Sparsity: {:.4}%", (1.0 - actual_density) * 100.0);
54        println!("  - Non-zeros: {}", coo.nnz());
55
56        coo
57    }
58    //#[test]
59    fn simple_matrix_comparison() {
60        // Create a small, predefined test matrix
61        let mut test_matrix = CooMatrix::<f64>::new(3, 3);
62        test_matrix.push(0, 0, 1.0);
63        test_matrix.push(0, 1, 16.0);
64        test_matrix.push(0, 2, 49.0);
65        test_matrix.push(1, 0, 4.0);
66        test_matrix.push(1, 1, 25.0);
67        test_matrix.push(1, 2, 64.0);
68        test_matrix.push(2, 0, 9.0);
69        test_matrix.push(2, 1, 36.0);
70        test_matrix.push(2, 2, 81.0);
71
72        // Run both implementations with the same seed for deterministic behavior
73        let seed = 42;
74        let current_result = svd_dim_seed(&test_matrix, 0, seed).unwrap();
75        let legacy_result = legacy::svd_dim_seed(&test_matrix, 0, seed).unwrap();
76
77        // Compare dimensions
78        assert_eq!(current_result.d, legacy_result.d);
79
80        // Compare singular values
81        let epsilon = 1.0e-12;
82        for i in 0..current_result.d {
83            let diff = (current_result.s[i] - legacy_result.s[i]).abs();
84            assert!(
85                diff < epsilon,
86                "Singular value {} differs by {}: current = {}, legacy = {}",
87                i, diff, current_result.s[i], legacy_result.s[i]
88            );
89        }
90
91        // Compare reconstructed matrices
92        let current_reconstructed = current_result.recompose();
93        let legacy_reconstructed = legacy_result.recompose();
94
95        for i in 0..3 {
96            for j in 0..3 {
97                let diff = (current_reconstructed[[i, j]] - legacy_reconstructed[[i, j]]).abs();
98                assert!(
99                    diff < epsilon,
100                    "Reconstructed matrix element [{},{}] differs by {}: current = {}, legacy = {}",
101                    i, j, diff, current_reconstructed[[i, j]], legacy_reconstructed[[i, j]]
102                );
103            }
104        }
105    }
106
107    #[test]
108    fn random_matrix_comparison() {
109        let seed = 12345;
110        let (nrows, ncols) = (50, 30);
111        let mut rng = StdRng::seed_from_u64(seed);
112
113        // Create random sparse matrix
114        let mut coo = CooMatrix::<f64>::new(nrows, ncols);
115        // Insert some random non-zero elements
116        for _ in 0..(nrows * ncols / 5) {  // ~20% density
117            let i = rng.gen_range(0..nrows);
118            let j = rng.gen_range(0..ncols);
119            let value = rng.gen_range(-10.0..10.0);
120            coo.push(i, j, value);
121        }
122
123        let csr = CsrMatrix::from(&coo);
124
125        // Calculate SVD using original method
126        let legacy_svd = svd_dim_seed(&csr, 0, seed as u32).unwrap();
127
128        // Calculate SVD using our masked method (using all columns)
129        let mask = vec![true; ncols];
130        let masked_matrix = MaskedCSRMatrix::new(&csr, mask);
131        let current_svd = svd_dim_seed(&masked_matrix, 0, seed as u32).unwrap();
132
133        // Compare with relative tolerance
134        let rel_tol = 1e-3;  // 0.1% relative tolerance
135
136        assert_eq!(legacy_svd.d, current_svd.d, "Ranks differ");
137
138        for i in 0..legacy_svd.d {
139            let legacy_val = legacy_svd.s[i];
140            let current_val = current_svd.s[i];
141            let abs_diff = (legacy_val - current_val).abs();
142            let rel_diff = abs_diff / legacy_val.max(current_val);
143
144            assert!(
145                rel_diff <= rel_tol,
146                "Singular value {} differs too much: relative diff = {}, current = {}, legacy = {}",
147                i, rel_diff, current_val, legacy_val
148            );
149        }
150    }
151
152
153
154    #[test]
155    fn test_real_sparse_matrix() {
156        // Create a matrix with similar sparsity to your real one (99.02%)
157        let test_matrix = create_sparse_matrix(100, 100, 0.0098); // 0.98% non-zeros
158
159        // Should no longer fail with convergence error
160        let result = svd_dim_seed(&test_matrix, 50, 42); // Using your modified imtqlb
161        assert!(result.is_ok(), "{}", format!("SVD failed on 99.02% sparse matrix, {:?}", result.err().unwrap()));
162    }
163
164    #[test]
165    fn test_real_sparse_matrix_very_big() {
166        // Create a matrix with similar sparsity to your real one (99.02%)
167        let test_matrix = create_sparse_matrix(10000, 1000, 0.0098); // 0.98% non-zeros
168
169        // Should no longer fail with convergence error
170        let result = svd_dim_seed(&test_matrix, 50, 42); // Using your modified imtqlb
171        assert!(result.is_ok(), "{}", format!("SVD failed on 99.02% sparse matrix, {:?}", result.err().unwrap()));
172    }
173
174    #[test]
175    fn test_real_sparse_matrix_very_very_big() {
176        // Create a matrix with similar sparsity to your real one (99.02%)
177        let test_matrix = create_sparse_matrix(100000, 2500, 0.0098); // 0.98% non-zeros
178
179        // Should no longer fail with convergence error
180        let result = svd(&test_matrix); // Using your modified imtqlb
181        assert!(result.is_ok(), "{}", format!("SVD failed on 99.02% sparse matrix, {:?}", result.err().unwrap()));
182    }
183
184    //#[test]
185    fn f32_precision_test() {
186        let seed = 12345;
187        let (nrows, ncols) = (40, 20);
188        let mut rng = StdRng::seed_from_u64(seed);
189
190        // Create random sparse matrix with f32 values
191        let mut coo_f32 = CooMatrix::<f32>::new(nrows, ncols);
192        // And the same matrix with f64 values
193        let mut coo_f64 = CooMatrix::<f64>::new(nrows, ncols);
194
195        // Insert the same random non-zero elements in both matrices
196        for _ in 0..(nrows * ncols / 4) {  // ~25% density
197            let i = rng.gen_range(0..nrows);
198            let j = rng.gen_range(0..ncols);
199            let value = rng.gen_range(-10.0..10.0);
200            coo_f32.push(i, j, value as f32);
201            coo_f64.push(i, j, value);
202        }
203
204        let csr_f32 = CsrMatrix::from(&coo_f32);
205        let csr_f64 = CsrMatrix::from(&coo_f64);
206
207        // Calculate SVD for both types
208        let svd_f32 = svd_dim_seed(&csr_f32, 0, seed as u32).unwrap();
209        let svd_f64 = svd_dim_seed(&csr_f64, 0, seed as u32).unwrap();
210
211        // Adaptive tolerance - increases for smaller singular values
212        // and values further down the list (which are more affected by accumulated errors)
213        fn calculate_tolerance(index: usize, magnitude: f64) -> f64 {
214            // Base tolerance is 0.2%
215            let base_tol = 0.002;
216
217            // Scale up for smaller values (more affected by precision)
218            let magnitude_factor = if magnitude < 1.0 {
219                2.0  // Double tolerance for very small values
220            } else if magnitude < 10.0 {
221                1.5  // 1.5x tolerance for moderately small values
222            } else {
223                1.0  // Normal tolerance for larger values
224            };
225
226            // Scale up for later indices (more affected by accumulated errors)
227            let index_factor = 1.0 + (index as f64 * 0.001);  // Add 0.1% per index
228
229            base_tol * magnitude_factor * index_factor
230        }
231
232        println!("f32 rank: {}, f64 rank: {}", svd_f32.d, svd_f64.d);
233
234        // Compare singular values up to the minimum rank
235        let min_rank = svd_f32.d.min(svd_f64.d);
236        for i in 0..min_rank {
237            let f32_val = svd_f32.s[i];
238            let f64_val = svd_f64.s[i];
239            let abs_diff = (f64_val - f32_val as f64).abs();
240            let rel_diff = abs_diff / f64_val.max(f32_val as f64);
241
242            // Calculate appropriate tolerance for this value
243            let tol = calculate_tolerance(i, f64_val);
244
245            println!("Singular value {}: f32 = {}, f64 = {}, rel_diff = {:.6}%, tol = {:.6}%",
246                     i, f32_val, f64_val, rel_diff * 100.0, tol * 100.0);
247
248            assert!(
249                rel_diff <= tol,
250                "Singular value {} differs too much: f64 = {}, f32 = {}. Relative diff: {} > tolerance: {}",
251                i, f64_val, f32_val, rel_diff, tol
252            );
253        }
254
255        // Optional: Also check that overall behavior is reasonable
256        // For example, check that both implementations find similar condition numbers
257        if min_rank >= 2 {
258            let condition_f32 = svd_f32.s[0] / svd_f32.s[min_rank - 1];
259            let condition_f64 = svd_f64.s[0] / svd_f64.s[min_rank - 1];
260            let condition_rel_diff = ((condition_f64 - condition_f32 as f64) / condition_f64).abs();
261
262            println!("Condition number: f32 = {}, f64 = {}, rel_diff = {:.6}%",
263                     condition_f32, condition_f64, condition_rel_diff * 100.0);
264
265            // Condition number can vary more, use 5% tolerance
266            assert!(condition_rel_diff <= 0.05,
267                    "Condition numbers differ too much: f32 = {}, f64 = {}",
268                    condition_f32, condition_f64);
269        }
270    }
271}