1use memmap2::{Mmap, MmapOptions};
11use scirs2_core::ndarray::{s, Array2};
12use sklears_core::error::{Result as SklearsResult, SklearsError};
13use std::fs::File;
14use std::path::Path;
15
16type DecompositionOperation<T> = Box<dyn Fn(&T) -> SklearsResult<T>>;
18
19pub struct OutOfCoreDecomposition {
21 chunk_size: usize,
22 temp_dir: std::path::PathBuf,
23 n_components: usize,
24}
25
26impl OutOfCoreDecomposition {
27 pub fn new(chunk_size: usize, temp_dir: Option<&Path>) -> Self {
28 let temp_dir = temp_dir
29 .map(|p| p.to_path_buf())
30 .unwrap_or_else(|| std::env::temp_dir().join("sklears_decomposition"));
31
32 Self {
33 chunk_size,
34 temp_dir,
35 n_components: 10,
36 }
37 }
38
39 pub fn with_n_components(mut self, n_components: usize) -> Self {
40 self.n_components = n_components;
41 self
42 }
43
44 pub fn incremental_svd<P: AsRef<Path>>(
46 &self,
47 matrix_file: P,
48 n_rows: usize,
49 n_cols: usize,
50 ) -> SklearsResult<(Array2<f64>, Array2<f64>, Array2<f64>)> {
51 std::fs::create_dir_all(&self.temp_dir).map_err(|e| {
53 SklearsError::InvalidInput(format!("Failed to create temp directory: {e}"))
54 })?;
55
56 let mut u_running = Array2::zeros((0, self.n_components));
58 let mut s_running = Array2::zeros((self.n_components, 1));
59 let mut vt_running = Array2::zeros((self.n_components, n_cols));
60
61 let file = File::open(matrix_file)
63 .map_err(|e| SklearsError::InvalidInput(format!("Failed to open matrix file: {e}")))?;
64
65 let mmap = unsafe {
66 MmapOptions::new().map(&file).map_err(|e| {
67 SklearsError::InvalidInput(format!("Failed to memory map file: {e}"))
68 })?
69 };
70
71 let chunk_rows = self.chunk_size / (n_cols * 8); let n_chunks = n_rows.div_ceil(chunk_rows);
73
74 for chunk_idx in 0..n_chunks {
75 let start_row = chunk_idx * chunk_rows;
76 let end_row = std::cmp::min(start_row + chunk_rows, n_rows);
77 let actual_chunk_rows = end_row - start_row;
78
79 let chunk = self.extract_chunk(&mmap, start_row, actual_chunk_rows, n_cols)?;
81
82 let (u_chunk, s_chunk, vt_chunk) = self.chunk_svd(&chunk)?;
84
85 (u_running, s_running, vt_running) = self.merge_svd(
87 (u_running, s_running, vt_running),
88 (u_chunk, s_chunk, vt_chunk),
89 chunk_idx == 0,
90 )?;
91 }
92
93 Ok((u_running, s_running, vt_running))
94 }
95
96 fn extract_chunk(
97 &self,
98 mmap: &Mmap,
99 start_row: usize,
100 n_rows: usize,
101 n_cols: usize,
102 ) -> SklearsResult<Array2<f64>> {
103 let mut chunk = Array2::zeros((n_rows, n_cols));
104 let bytes_per_row = n_cols * 8; for i in 0..n_rows {
107 let row_start = (start_row + i) * bytes_per_row;
108 let row_end = row_start + bytes_per_row;
109
110 if row_end <= mmap.len() {
111 let row_bytes = &mmap[row_start..row_end];
112 let row_data: &[f64] =
113 unsafe { std::slice::from_raw_parts(row_bytes.as_ptr() as *const f64, n_cols) };
114
115 for j in 0..n_cols {
116 chunk[(i, j)] = row_data[j];
117 }
118 }
119 }
120
121 Ok(chunk)
122 }
123
124 fn chunk_svd(
125 &self,
126 chunk: &Array2<f64>,
127 ) -> SklearsResult<(Array2<f64>, Array2<f64>, Array2<f64>)> {
128 let (n_rows, n_cols) = chunk.dim();
130 let n_components = std::cmp::min(self.n_components, std::cmp::min(n_rows, n_cols));
131
132 let u = Array2::zeros((n_rows, n_components));
134 let s = Array2::eye(n_components);
135 let vt = Array2::zeros((n_components, n_cols));
136
137 Ok((u, s, vt))
138 }
139
140 fn merge_svd(
141 &self,
142 running: (Array2<f64>, Array2<f64>, Array2<f64>),
143 chunk: (Array2<f64>, Array2<f64>, Array2<f64>),
144 is_first: bool,
145 ) -> SklearsResult<(Array2<f64>, Array2<f64>, Array2<f64>)> {
146 if is_first {
147 return Ok(chunk);
148 }
149
150 let (u_running, s_running, _vt_running) = running;
151 let (u_chunk, s_chunk, vt_chunk) = chunk;
152
153 let n_keep = std::cmp::min(self.n_components, s_running.nrows());
155
156 let u_merged = if u_chunk.nrows() > 0 {
157 let n_components_to_take = std::cmp::min(n_keep, u_chunk.ncols());
158 u_chunk.slice(s![.., ..n_components_to_take]).to_owned()
159 } else {
160 let n_components_to_take = std::cmp::min(n_keep, u_running.ncols());
161 u_running.slice(s![.., ..n_components_to_take]).to_owned()
162 };
163
164 let s_merged = {
165 let n_components_to_take = std::cmp::min(n_keep, s_chunk.nrows());
166 s_chunk
167 .slice(s![..n_components_to_take, ..n_components_to_take])
168 .to_owned()
169 };
170
171 let vt_merged = {
172 let n_components_to_take = std::cmp::min(n_keep, vt_chunk.nrows());
173 vt_chunk.slice(s![..n_components_to_take, ..]).to_owned()
174 };
175
176 Ok((u_merged, s_merged, vt_merged))
177 }
178}
179
180pub struct MemoryMappedMatrix {
182 mmap: Mmap,
183 shape: (usize, usize),
184 dtype_size: usize,
185}
186
187impl MemoryMappedMatrix {
188 pub fn open<P: AsRef<Path>>(
189 path: P,
190 shape: (usize, usize),
191 dtype_size: usize,
192 ) -> SklearsResult<Self> {
193 let file = File::open(path)
194 .map_err(|e| SklearsError::InvalidInput(format!("Failed to open file: {e}")))?;
195
196 let mmap = unsafe {
197 MmapOptions::new()
198 .map(&file)
199 .map_err(|e| SklearsError::InvalidInput(format!("Failed to memory map: {e}")))?
200 };
201
202 Ok(Self {
203 mmap,
204 shape,
205 dtype_size,
206 })
207 }
208
209 pub fn shape(&self) -> (usize, usize) {
210 self.shape
211 }
212
213 pub fn read_row(&self, row: usize) -> SklearsResult<Vec<f64>> {
214 if row >= self.shape.0 {
215 return Err(SklearsError::InvalidInput(
216 "Row index out of bounds".to_string(),
217 ));
218 }
219
220 let start = row * self.shape.1 * self.dtype_size;
221 let end = start + self.shape.1 * self.dtype_size;
222
223 if end > self.mmap.len() {
224 return Err(SklearsError::InvalidInput("Invalid file size".to_string()));
225 }
226
227 let bytes = &self.mmap[start..end];
228 let data: &[f64] =
229 unsafe { std::slice::from_raw_parts(bytes.as_ptr() as *const f64, self.shape.1) };
230
231 Ok(data.to_vec())
232 }
233
234 pub fn read_chunk(&self, start_row: usize, n_rows: usize) -> SklearsResult<Array2<f64>> {
235 if start_row + n_rows > self.shape.0 {
236 return Err(SklearsError::InvalidInput(
237 "Chunk extends beyond matrix".to_string(),
238 ));
239 }
240
241 let mut chunk = Array2::zeros((n_rows, self.shape.1));
242
243 for i in 0..n_rows {
244 let row_data = self.read_row(start_row + i)?;
245 for j in 0..self.shape.1 {
246 chunk[(i, j)] = row_data[j];
247 }
248 }
249
250 Ok(chunk)
251 }
252}
253
254pub struct ChunkedProcessor {
256 chunk_size: usize,
257 overlap: usize,
258}
259
260impl ChunkedProcessor {
261 pub fn new(chunk_size: usize, overlap: usize) -> Self {
262 Self {
263 chunk_size,
264 overlap,
265 }
266 }
267
268 pub fn process_matrix<F, R>(
269 &self,
270 matrix: &Array2<f64>,
271 mut process_fn: F,
272 ) -> SklearsResult<Vec<R>>
273 where
274 F: FnMut(&Array2<f64>) -> SklearsResult<R>,
275 {
276 let mut results = Vec::new();
277 let n_rows = matrix.nrows();
278 let _n_cols = matrix.ncols();
279
280 let effective_chunk_size = self.chunk_size - self.overlap;
281 let n_chunks = n_rows.div_ceil(effective_chunk_size);
282
283 for chunk_idx in 0..n_chunks {
284 let start = chunk_idx * effective_chunk_size;
285 let end = std::cmp::min(start + self.chunk_size, n_rows);
286
287 let chunk = matrix.slice(s![start..end, ..]).to_owned();
288 let result = process_fn(&chunk)?;
289 results.push(result);
290 }
291
292 Ok(results)
293 }
294
295 pub fn process_columns<F, R>(
296 &self,
297 matrix: &Array2<f64>,
298 mut process_fn: F,
299 ) -> SklearsResult<Vec<R>>
300 where
301 F: FnMut(&Array2<f64>) -> SklearsResult<R>,
302 {
303 let mut results = Vec::new();
304 let _n_rows = matrix.nrows();
305 let n_cols = matrix.ncols();
306
307 let effective_chunk_size = self.chunk_size - self.overlap;
308 let n_chunks = n_cols.div_ceil(effective_chunk_size);
309
310 for chunk_idx in 0..n_chunks {
311 let start = chunk_idx * effective_chunk_size;
312 let end = std::cmp::min(start + self.chunk_size, n_cols);
313
314 let chunk = matrix.slice(s![.., start..end]).to_owned();
315 let result = process_fn(&chunk)?;
316 results.push(result);
317 }
318
319 Ok(results)
320 }
321}
322
323#[derive(Debug, Clone)]
325pub enum CompressionMethod {
326 SingularValueThreshold(f64),
328 TopK(usize),
330 Quantized(u8), BlockCompression(usize), }
335
336pub struct CompressedDecomposition {
337 u_compressed: CompressedMatrix,
338 s_compressed: Vec<f64>,
339 vt_compressed: CompressedMatrix,
340 compression_ratio: f64,
341}
342
343impl CompressedDecomposition {
344 pub fn compress(
345 u: &Array2<f64>,
346 s: &Array2<f64>,
347 vt: &Array2<f64>,
348 method: CompressionMethod,
349 ) -> SklearsResult<Self> {
350 let original_size = (u.len() + s.len() + vt.len()) * 8; let (u_compressed, s_compressed, vt_compressed) = match method {
353 CompressionMethod::SingularValueThreshold(threshold) => {
354 Self::threshold_compression(u, s, vt, threshold)?
355 }
356 CompressionMethod::TopK(k) => Self::topk_compression(u, s, vt, k)?,
357 CompressionMethod::Quantized(bits) => Self::quantized_compression(u, s, vt, bits)?,
358 CompressionMethod::BlockCompression(block_size) => {
359 Self::block_compression(u, s, vt, block_size)?
360 }
361 };
362
363 let compressed_size = u_compressed.compressed_size()
364 + s_compressed.len() * 8
365 + vt_compressed.compressed_size();
366
367 let compression_ratio = original_size as f64 / compressed_size as f64;
368
369 Ok(Self {
370 u_compressed,
371 s_compressed,
372 vt_compressed,
373 compression_ratio,
374 })
375 }
376
377 fn threshold_compression(
378 u: &Array2<f64>,
379 s: &Array2<f64>,
380 vt: &Array2<f64>,
381 threshold: f64,
382 ) -> SklearsResult<(CompressedMatrix, Vec<f64>, CompressedMatrix)> {
383 let min_dim = std::cmp::min(s.nrows(), s.ncols());
385 let s_diag: Vec<f64> = (0..min_dim).map(|i| s[(i, i)]).collect();
386
387 let significant_indices: Vec<usize> = s_diag
388 .iter()
389 .enumerate()
390 .filter(|(_, &val)| val > threshold)
391 .map(|(i, _)| i)
392 .collect();
393
394 let k = significant_indices.len();
395 if k == 0 {
396 return Err(SklearsError::InvalidInput(
397 "No significant singular values".to_string(),
398 ));
399 }
400
401 let u_reduced = u.slice(s![.., ..k]).to_owned();
402 let s_reduced: Vec<f64> = significant_indices.iter().map(|&i| s_diag[i]).collect();
403 let vt_reduced = vt.slice(s![..k, ..]).to_owned();
404
405 Ok((
406 CompressedMatrix::from_dense(&u_reduced),
407 s_reduced,
408 CompressedMatrix::from_dense(&vt_reduced),
409 ))
410 }
411
412 fn topk_compression(
413 u: &Array2<f64>,
414 s: &Array2<f64>,
415 vt: &Array2<f64>,
416 k: usize,
417 ) -> SklearsResult<(CompressedMatrix, Vec<f64>, CompressedMatrix)> {
418 let min_dim = std::cmp::min(s.nrows(), s.ncols());
419 let actual_k = std::cmp::min(k, min_dim);
420
421 let u_reduced = u.slice(s![.., ..actual_k]).to_owned();
422 let s_reduced: Vec<f64> = (0..actual_k).map(|i| s[(i, i)]).collect();
423 let vt_reduced = vt.slice(s![..actual_k, ..]).to_owned();
424
425 Ok((
426 CompressedMatrix::from_dense(&u_reduced),
427 s_reduced,
428 CompressedMatrix::from_dense(&vt_reduced),
429 ))
430 }
431
432 fn quantized_compression(
433 u: &Array2<f64>,
434 s: &Array2<f64>,
435 vt: &Array2<f64>,
436 bits: u8,
437 ) -> SklearsResult<(CompressedMatrix, Vec<f64>, CompressedMatrix)> {
438 let levels = (1u64 << bits) as f64;
440
441 let u_quantized = Self::quantize_matrix(u, levels);
442 let vt_quantized = Self::quantize_matrix(vt, levels);
443
444 let min_dim = std::cmp::min(s.nrows(), s.ncols());
445 let s_diag: Vec<f64> = (0..min_dim).map(|i| s[(i, i)]).collect();
446
447 Ok((
448 CompressedMatrix::quantized(&u_quantized, bits),
449 s_diag,
450 CompressedMatrix::quantized(&vt_quantized, bits),
451 ))
452 }
453
454 fn block_compression(
455 u: &Array2<f64>,
456 s: &Array2<f64>,
457 vt: &Array2<f64>,
458 block_size: usize,
459 ) -> SklearsResult<(CompressedMatrix, Vec<f64>, CompressedMatrix)> {
460 let min_dim = std::cmp::min(s.nrows(), s.ncols());
461 let s_diag: Vec<f64> = (0..min_dim).map(|i| s[(i, i)]).collect();
462
463 Ok((
464 CompressedMatrix::block_compressed(u, block_size),
465 s_diag,
466 CompressedMatrix::block_compressed(vt, block_size),
467 ))
468 }
469
470 fn quantize_matrix(matrix: &Array2<f64>, levels: f64) -> Array2<f64> {
471 let (min_val, max_val) = matrix
472 .iter()
473 .fold((f64::INFINITY, f64::NEG_INFINITY), |(min, max), &val| {
474 (min.min(val), max.max(val))
475 });
476
477 let scale = (max_val - min_val) / (levels - 1.0);
478
479 matrix.map(|&val| {
480 let quantized = ((val - min_val) / scale).round();
481 min_val + quantized * scale
482 })
483 }
484
485 pub fn compression_ratio(&self) -> f64 {
486 self.compression_ratio
487 }
488
489 pub fn decompress(&self) -> SklearsResult<(Array2<f64>, Array2<f64>, Array2<f64>)> {
490 let u = self.u_compressed.decompress()?;
491
492 let n = self.s_compressed.len();
494 let mut s = Array2::zeros((n, n));
495 for i in 0..n {
496 s[(i, i)] = self.s_compressed[i];
497 }
498
499 let vt = self.vt_compressed.decompress()?;
500
501 Ok((u, s, vt))
502 }
503}
504
505#[derive(Debug, Clone)]
507pub enum CompressedMatrix {
508 Dense(Array2<f64>),
509 Sparse(Vec<(usize, usize, f64)>, (usize, usize)), Quantized(Array2<f64>, u8), BlockCompressed(Vec<Array2<f64>>, (usize, usize), usize), }
513
514impl CompressedMatrix {
515 pub fn from_dense(matrix: &Array2<f64>) -> Self {
516 Self::Dense(matrix.clone())
517 }
518
519 pub fn sparse(data: Vec<(usize, usize, f64)>, shape: (usize, usize)) -> Self {
520 Self::Sparse(data, shape)
521 }
522
523 pub fn quantized(matrix: &Array2<f64>, bits: u8) -> Self {
524 Self::Quantized(matrix.clone(), bits)
525 }
526
527 pub fn block_compressed(matrix: &Array2<f64>, block_size: usize) -> Self {
528 let n_rows = matrix.nrows();
529 let n_cols = matrix.ncols();
530 let mut blocks = Vec::new();
531
532 for i in (0..n_rows).step_by(block_size) {
533 for j in (0..n_cols).step_by(block_size) {
534 let end_i = std::cmp::min(i + block_size, n_rows);
535 let end_j = std::cmp::min(j + block_size, n_cols);
536 let block = matrix.slice(s![i..end_i, j..end_j]).to_owned();
537 blocks.push(block);
538 }
539 }
540
541 Self::BlockCompressed(blocks, (n_rows, n_cols), block_size)
542 }
543
544 pub fn compressed_size(&self) -> usize {
545 match self {
546 Self::Dense(matrix) => matrix.len() * 8,
547 Self::Sparse(data, _) => data.len() * (8 + 8 + 8), Self::Quantized(matrix, bits) => matrix.len() * (*bits as usize / 8).max(1),
549 Self::BlockCompressed(blocks, _, _) => blocks.iter().map(|b| b.len() * 8).sum(),
550 }
551 }
552
553 pub fn decompress(&self) -> SklearsResult<Array2<f64>> {
554 match self {
555 Self::Dense(matrix) => Ok(matrix.clone()),
556 Self::Sparse(data, shape) => {
557 let mut matrix = Array2::zeros(*shape);
558 for &(i, j, val) in data {
559 matrix[(i, j)] = val;
560 }
561 Ok(matrix)
562 }
563 Self::Quantized(matrix, _) => Ok(matrix.clone()),
564 Self::BlockCompressed(blocks, shape, block_size) => {
565 let mut matrix = Array2::zeros(*shape);
566 let mut block_idx = 0;
567
568 for i in (0..shape.0).step_by(*block_size) {
569 for j in (0..shape.1).step_by(*block_size) {
570 if block_idx < blocks.len() {
571 let block = &blocks[block_idx];
572 let end_i = std::cmp::min(i + block_size, shape.0);
573 let end_j = std::cmp::min(j + block_size, shape.1);
574
575 for (bi, mi) in (i..end_i).enumerate() {
576 for (bj, mj) in (j..end_j).enumerate() {
577 if bi < block.nrows() && bj < block.ncols() {
578 matrix[(mi, mj)] = block[(bi, bj)];
579 }
580 }
581 }
582 block_idx += 1;
583 }
584 }
585 }
586
587 Ok(matrix)
588 }
589 }
590 }
591}
592
593pub struct LazyDecomposition<T> {
595 operations: Vec<DecompositionOperation<T>>,
596 cached_result: Option<T>,
597}
598
599impl<T: Clone> LazyDecomposition<T> {
600 pub fn new() -> Self {
601 Self {
602 operations: Vec::new(),
603 cached_result: None,
604 }
605 }
606
607 pub fn add_operation<F>(mut self, operation: F) -> Self
608 where
609 F: Fn(&T) -> SklearsResult<T> + 'static,
610 {
611 self.operations.push(Box::new(operation));
612 self.cached_result = None; self
614 }
615
616 pub fn evaluate(&mut self, input: &T) -> SklearsResult<T> {
617 if let Some(ref result) = self.cached_result {
618 return Ok(result.clone());
619 }
620
621 let mut current = input.clone();
622 for operation in &self.operations {
623 current = operation(¤t)?;
624 }
625
626 self.cached_result = Some(current.clone());
627 Ok(current)
628 }
629
630 pub fn clear_cache(&mut self) {
631 self.cached_result = None;
632 }
633}
634
635impl<T: Clone> Default for LazyDecomposition<T> {
636 fn default() -> Self {
637 Self::new()
638 }
639}
640
641#[allow(non_snake_case)]
642#[cfg(test)]
643mod tests {
644 use super::*;
645 use scirs2_core::ndarray::Array2;
646
647 #[test]
648 fn test_chunked_processor() {
649 let matrix = Array2::from_shape_fn((100, 50), |(i, j)| (i * j) as f64);
650 let processor = ChunkedProcessor::new(25, 5);
651
652 let results = processor
653 .process_matrix(&matrix, |chunk| Ok(chunk.sum()))
654 .expect("operation should succeed");
655
656 assert!(!results.is_empty());
657 assert_eq!(results.len(), 5); }
659
660 #[test]
661 fn test_compression_topk() {
662 let u = Array2::from_shape_fn((10, 5), |(i, j)| (i + j) as f64);
663 let s = Array2::eye(5);
664 let vt = Array2::from_shape_fn((5, 8), |(i, j)| (i * j) as f64);
665
666 let compressed = CompressedDecomposition::compress(&u, &s, &vt, CompressionMethod::TopK(3))
667 .expect("operation should succeed");
668
669 assert!(compressed.compression_ratio() > 1.0);
670
671 let (u_dec, _s_dec, vt_dec) = compressed.decompress().expect("operation should succeed");
672 assert_eq!(u_dec.ncols(), 3);
673 assert_eq!(vt_dec.nrows(), 3);
674 }
675
676 #[test]
677 fn test_lazy_decomposition() {
678 let mut lazy = LazyDecomposition::new()
679 .add_operation(|x: &f64| Ok(x * 2.0))
680 .add_operation(|x: &f64| Ok(x + 1.0))
681 .add_operation(|x: &f64| Ok(x * x));
682
683 let result = lazy.evaluate(&3.0).expect("operation should succeed");
684 assert_eq!(result, 49.0); let result2 = lazy.evaluate(&3.0).expect("operation should succeed");
688 assert_eq!(result2, 49.0);
689 }
690
691 #[test]
692 fn test_compressed_matrix_sparse() {
693 let data = vec![(0, 0, 1.0), (1, 1, 2.0), (2, 2, 3.0)];
694 let compressed = CompressedMatrix::sparse(data, (3, 3));
695
696 let decompressed = compressed.decompress().expect("operation should succeed");
697 assert_eq!(decompressed[(0, 0)], 1.0);
698 assert_eq!(decompressed[(1, 1)], 2.0);
699 assert_eq!(decompressed[(2, 2)], 3.0);
700 assert_eq!(decompressed[(0, 1)], 0.0);
701 }
702}