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 { break v;
47 }
48 };
49
50 coo.push(i, j, val);
51 }
52 }
53
54 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 fn simple_matrix_comparison() {
66 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 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 assert_eq!(current_result.d, legacy_result.d);
85
86 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 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 let mut coo = CooMatrix::<f64>::new(nrows, ncols);
121 for _ in 0..(nrows * ncols / 5) { 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 let legacy_svd = svd_dim_seed(&csr, 0, seed as u32).unwrap();
133
134 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 let rel_tol = 1e-3; 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 let test_matrix = create_sparse_matrix(100, 100, 0.0098); 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 let test_matrix = create_sparse_matrix(1000, 250, 0.01); let csr = CsrMatrix::from(&test_matrix);
177
178 let result = randomized_svd(
180 &csr,
181 50, 10, 3, PowerIterationNormalizer::QR, Some(42), );
187
188 assert!(
190 result.is_ok(),
191 "Randomized SVD failed on 99% sparse matrix: {:?}",
192 result.err().unwrap()
193 );
194
195 if let Ok(svd_result) = result {
197 assert_eq!(svd_result.d, 50, "Expected rank of 50");
199
200 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 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 let test_matrix = create_sparse_matrix(100000, 2500, 0.01); let csr = CsrMatrix::from(&test_matrix);
229
230 let threadpool = ThreadPoolBuilder::new().num_threads(10).build().unwrap();
232 let result = threadpool.install(|| {
233 randomized_svd(
234 &csr,
235 50, 10, 2, PowerIterationNormalizer::QR, Some(42), )
241 });
242
243
244 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 let test_matrix = create_sparse_matrix(1000, 250, 0.01); let csr = CsrMatrix::from(&test_matrix);
261
262 let threadpool = ThreadPoolBuilder::new().num_threads(10).build().unwrap();
264 let result = threadpool.install(|| {
265 randomized_svd(
266 &csr,
267 50, 10, 2, PowerIterationNormalizer::QR, Some(42), )
273 });
274
275
276 assert!(
278 result.is_ok(),
279 "Randomized SVD failed on 99% sparse matrix: {:?}",
280 result.err().unwrap()
281 );
282 }
283}