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    #[test]
19    fn simple_matrix_comparison() {
20        // Create a small, predefined test matrix
21        let mut test_matrix = CooMatrix::<f64>::new(3, 3);
22        test_matrix.push(0, 0, 1.0);
23        test_matrix.push(0, 1, 16.0);
24        test_matrix.push(0, 2, 49.0);
25        test_matrix.push(1, 0, 4.0);
26        test_matrix.push(1, 1, 25.0);
27        test_matrix.push(1, 2, 64.0);
28        test_matrix.push(2, 0, 9.0);
29        test_matrix.push(2, 1, 36.0);
30        test_matrix.push(2, 2, 81.0);
31
32        // Run both implementations with the same seed for deterministic behavior
33        let seed = 42;
34        let current_result = svd_dim_seed(&test_matrix, 0, seed).unwrap();
35        let legacy_result = legacy::svd_dim_seed(&test_matrix, 0, seed).unwrap();
36
37        // Compare dimensions
38        assert_eq!(current_result.d, legacy_result.d);
39
40        // Compare singular values
41        let epsilon = 1.0e-12;
42        for i in 0..current_result.d {
43            let diff = (current_result.s[i] - legacy_result.s[i]).abs();
44            assert!(
45                diff < epsilon,
46                "Singular value {} differs by {}: current = {}, legacy = {}",
47                i, diff, current_result.s[i], legacy_result.s[i]
48            );
49        }
50
51        // Compare reconstructed matrices
52        let current_reconstructed = current_result.recompose();
53        let legacy_reconstructed = legacy_result.recompose();
54
55        for i in 0..3 {
56            for j in 0..3 {
57                let diff = (current_reconstructed[[i, j]] - legacy_reconstructed[[i, j]]).abs();
58                assert!(
59                    diff < epsilon,
60                    "Reconstructed matrix element [{},{}] differs by {}: current = {}, legacy = {}",
61                    i, j, diff, current_reconstructed[[i, j]], legacy_reconstructed[[i, j]]
62                );
63            }
64        }
65    }
66
67    #[test]
68    fn random_matrix_comparison() {
69        let seed = 12345;
70        let (nrows, ncols) = (50, 30);
71        let mut rng = StdRng::seed_from_u64(seed);
72
73        // Create random sparse matrix
74        let mut coo = CooMatrix::<f64>::new(nrows, ncols);
75        // Insert some random non-zero elements
76        for _ in 0..(nrows * ncols / 5) {  // ~20% density
77            let i = rng.gen_range(0..nrows);
78            let j = rng.gen_range(0..ncols);
79            let value = rng.gen_range(-10.0..10.0);
80            coo.push(i, j, value);
81        }
82
83        let csr = CsrMatrix::from(&coo);
84
85        // Calculate SVD using original method
86        let legacy_svd = svd_dim_seed(&csr, 0, seed as u32).unwrap();
87
88        // Calculate SVD using our masked method (using all columns)
89        let mask = vec![true; ncols];
90        let masked_matrix = MaskedCSRMatrix::new(&csr, mask);
91        let current_svd = svd_dim_seed(&masked_matrix, 0, seed as u32).unwrap();
92
93        // Compare with relative tolerance
94        let rel_tol = 1e-3;  // 0.1% relative tolerance
95
96        assert_eq!(legacy_svd.d, current_svd.d, "Ranks differ");
97
98        for i in 0..legacy_svd.d {
99            let legacy_val = legacy_svd.s[i];
100            let current_val = current_svd.s[i];
101            let abs_diff = (legacy_val - current_val).abs();
102            let rel_diff = abs_diff / legacy_val.max(current_val);
103
104            assert!(
105                rel_diff <= rel_tol,
106                "Singular value {} differs too much: relative diff = {}, current = {}, legacy = {}",
107                i, rel_diff, current_val, legacy_val
108            );
109        }
110    }
111
112    #[test]
113    fn f32_precision_test() {
114        let seed = 12345;
115        let (nrows, ncols) = (40, 20);
116        let mut rng = StdRng::seed_from_u64(seed);
117
118        // Create random sparse matrix with f32 values
119        let mut coo_f32 = CooMatrix::<f32>::new(nrows, ncols);
120        // And the same matrix with f64 values
121        let mut coo_f64 = CooMatrix::<f64>::new(nrows, ncols);
122
123        // Insert the same random non-zero elements in both matrices
124        for _ in 0..(nrows * ncols / 4) {  // ~25% density
125            let i = rng.gen_range(0..nrows);
126            let j = rng.gen_range(0..ncols);
127            let value = rng.gen_range(-10.0..10.0);
128            coo_f32.push(i, j, value as f32);
129            coo_f64.push(i, j, value);
130        }
131
132        let csr_f32 = CsrMatrix::from(&coo_f32);
133        let csr_f64 = CsrMatrix::from(&coo_f64);
134
135        // Calculate SVD for both types
136        let svd_f32 = svd_dim_seed(&csr_f32, 0, seed as u32).unwrap();
137        let svd_f64 = svd_dim_seed(&csr_f64, 0, seed as u32).unwrap();
138
139        // Adaptive tolerance - increases for smaller singular values
140        // and values further down the list (which are more affected by accumulated errors)
141        fn calculate_tolerance(index: usize, magnitude: f64) -> f64 {
142            // Base tolerance is 0.2%
143            let base_tol = 0.002;
144
145            // Scale up for smaller values (more affected by precision)
146            let magnitude_factor = if magnitude < 1.0 {
147                2.0  // Double tolerance for very small values
148            } else if magnitude < 10.0 {
149                1.5  // 1.5x tolerance for moderately small values
150            } else {
151                1.0  // Normal tolerance for larger values
152            };
153
154            // Scale up for later indices (more affected by accumulated errors)
155            let index_factor = 1.0 + (index as f64 * 0.001);  // Add 0.1% per index
156
157            base_tol * magnitude_factor * index_factor
158        }
159
160        println!("f32 rank: {}, f64 rank: {}", svd_f32.d, svd_f64.d);
161
162        // Compare singular values up to the minimum rank
163        let min_rank = svd_f32.d.min(svd_f64.d);
164        for i in 0..min_rank {
165            let f32_val = svd_f32.s[i];
166            let f64_val = svd_f64.s[i];
167            let abs_diff = (f64_val - f32_val as f64).abs();
168            let rel_diff = abs_diff / f64_val.max(f32_val as f64);
169
170            // Calculate appropriate tolerance for this value
171            let tol = calculate_tolerance(i, f64_val);
172
173            println!("Singular value {}: f32 = {}, f64 = {}, rel_diff = {:.6}%, tol = {:.6}%",
174                     i, f32_val, f64_val, rel_diff * 100.0, tol * 100.0);
175
176            assert!(
177                rel_diff <= tol,
178                "Singular value {} differs too much: f64 = {}, f32 = {}. Relative diff: {} > tolerance: {}",
179                i, f64_val, f32_val, rel_diff, tol
180            );
181        }
182
183        // Optional: Also check that overall behavior is reasonable
184        // For example, check that both implementations find similar condition numbers
185        if min_rank >= 2 {
186            let condition_f32 = svd_f32.s[0] / svd_f32.s[min_rank - 1];
187            let condition_f64 = svd_f64.s[0] / svd_f64.s[min_rank - 1];
188            let condition_rel_diff = ((condition_f64 - condition_f32 as f64) / condition_f64).abs();
189
190            println!("Condition number: f32 = {}, f64 = {}, rel_diff = {:.6}%",
191                     condition_f32, condition_f64, condition_rel_diff * 100.0);
192
193            // Condition number can vary more, use 5% tolerance
194            assert!(condition_rel_diff <= 0.05,
195                    "Condition numbers differ too much: f32 = {}, f64 = {}",
196                    condition_f32, condition_f64);
197        }
198    }
199}