Skip to main content

sklears_decomposition/
format_support.rs

1//! Advanced Format Support for Matrix Decomposition
2//!
3//! This module provides support for various data formats commonly used in
4//! scientific computing and machine learning applications:
5//!
6//! - HDF5: Hierarchical Data Format for large scientific datasets
7//! - Sparse matrices: Efficient representation of matrices with mostly zero values
8//! - Memory-mapped files: For handling datasets larger than available RAM
9//! - Compressed formats: Space-efficient storage of decomposition results
10//!
11//! Features:
12//! - HDF5 read/write support for matrices and decomposition results
13//! - Multiple sparse matrix formats (COO, CSR, CSC)
14//! - Incremental loading of large matrices
15//! - Compression and decompression of decomposition results
16//! - Cross-platform file format compatibility
17
18#[cfg(feature = "hdf5-support")]
19use hdf5::{Dataset, File, Group};
20use scirs2_core::ndarray::{Array1, Array2};
21use serde::{Deserialize, Serialize};
22use sklears_core::{
23    error::{Result, SklearsError},
24    types::Float,
25};
26// sprs removed per SciRS2/COOLJAPAN Policy; use scirs2-sparse instead
27// #[cfg(feature = "sparse")]
28// use sprs::{CsMat, CsMatBase, CsVec, TriMat};
29use std::collections::HashMap;
30use std::path::Path;
31
32/// Configuration for format support operations
33#[derive(Debug, Clone)]
34pub struct FormatConfig {
35    /// Compression level (0-9, 0 = no compression)
36    pub compression_level: u8,
37    /// Chunk size for HDF5 operations
38    pub chunk_size: Option<(usize, usize)>,
39    /// Enable checksums for data integrity
40    pub enable_checksums: bool,
41    /// Maximum memory usage for operations
42    pub max_memory_mb: Option<usize>,
43    /// Sparse matrix format preference
44    pub preferred_sparse_format: SparseFormat,
45}
46
47impl Default for FormatConfig {
48    fn default() -> Self {
49        Self {
50            compression_level: 6,
51            chunk_size: Some((1000, 1000)),
52            enable_checksums: true,
53            max_memory_mb: None,
54            preferred_sparse_format: SparseFormat::CSR,
55        }
56    }
57}
58
59/// Supported sparse matrix formats
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum SparseFormat {
62    /// Coordinate format (COO)
63    COO,
64    /// Compressed Sparse Row (CSR)
65    CSR,
66    /// Compressed Sparse Column (CSC)
67    CSC,
68}
69
70/// HDF5 format support for matrix operations
71#[cfg(feature = "hdf5-support")]
72#[derive(Default)]
73pub struct HDF5Support {
74    config: FormatConfig,
75}
76
77#[cfg(feature = "hdf5-support")]
78impl HDF5Support {
79    /// Create new HDF5 support instance
80    pub fn new() -> Self {
81        Self::default()
82    }
83
84    /// Create with custom configuration
85    pub fn with_config(config: FormatConfig) -> Self {
86        Self { config }
87    }
88
89    /// Write matrix to HDF5 file
90    pub fn write_matrix<P: AsRef<Path>>(
91        &self,
92        file_path: P,
93        dataset_name: &str,
94        matrix: &Array2<Float>,
95    ) -> Result<()> {
96        let file = File::create(file_path).map_err(|e| {
97            SklearsError::InvalidInput(format!("Failed to create HDF5 file: {}", e))
98        })?;
99
100        let shape = matrix.shape();
101        let dataset = file
102            .new_dataset::<Float>()
103            .shape(shape)
104            .chunk(self.config.chunk_size.unwrap_or((shape[0], shape[1])))
105            .deflate(self.config.compression_level)
106            .create(dataset_name)
107            .map_err(|e| SklearsError::InvalidInput(format!("Failed to create dataset: {}", e)))?;
108
109        // Convert to standard layout and write
110        if matrix.is_standard_layout() {
111            let slice = matrix.as_slice().ok_or_else(|| {
112                SklearsError::InvalidInput("matrix is not contiguous".to_string())
113            })?;
114            dataset
115                .write(slice)
116                .map_err(|e| SklearsError::InvalidInput(format!("Failed to write data: {e}")))?;
117        } else {
118            let standard_matrix = matrix.to_owned();
119            let slice = standard_matrix.as_slice().ok_or_else(|| {
120                SklearsError::InvalidInput("matrix copy is not contiguous".to_string())
121            })?;
122            dataset
123                .write(slice)
124                .map_err(|e| SklearsError::InvalidInput(format!("Failed to write data: {e}")))?;
125        }
126
127        // Add metadata
128        self.write_metadata(&dataset, matrix)?;
129
130        Ok(())
131    }
132
133    /// Read matrix from HDF5 file
134    pub fn read_matrix<P: AsRef<Path>>(
135        &self,
136        file_path: P,
137        dataset_name: &str,
138    ) -> Result<Array2<Float>> {
139        let file = File::open(file_path)
140            .map_err(|e| SklearsError::InvalidInput(format!("Failed to open HDF5 file: {}", e)))?;
141
142        let dataset = file
143            .dataset(dataset_name)
144            .map_err(|e| SklearsError::InvalidInput(format!("Failed to open dataset: {}", e)))?;
145
146        let shape = dataset.shape();
147        if shape.len() != 2 {
148            return Err(SklearsError::InvalidInput(
149                "Dataset must be 2-dimensional".to_string(),
150            ));
151        }
152
153        // Read as 1D raw vector to avoid ndarray version mismatch
154        let data: Vec<Float> = dataset
155            .read_raw::<Float>()
156            .map_err(|e| SklearsError::InvalidInput(format!("Failed to read data: {}", e)))?;
157
158        Array2::from_shape_vec((shape[0], shape[1]), data)
159            .map_err(|e| SklearsError::InvalidInput(format!("Failed to create array: {}", e)))
160    }
161
162    /// Write decomposition results to HDF5 file
163    pub fn write_decomposition_results<P: AsRef<Path>>(
164        &self,
165        file_path: P,
166        results: &DecompositionResults,
167    ) -> Result<()> {
168        let file = File::create(file_path).map_err(|e| {
169            SklearsError::InvalidInput(format!("Failed to create HDF5 file: {}", e))
170        })?;
171
172        let group = file
173            .create_group("decomposition")
174            .map_err(|e| SklearsError::InvalidInput(format!("Failed to create group: {}", e)))?;
175
176        // Write matrices
177        if let Some(ref u) = results.u_matrix {
178            self.write_matrix_to_group(&group, "U", u)?;
179        }
180
181        if let Some(ref s) = results.singular_values {
182            let dataset = group
183                .new_dataset::<Float>()
184                .shape([s.len()])
185                .create("singular_values")
186                .map_err(|e| {
187                    SklearsError::InvalidInput(format!("Failed to create dataset: {}", e))
188                })?;
189
190            let slice = s.as_slice().ok_or_else(|| {
191                SklearsError::InvalidInput("singular_values not contiguous".to_string())
192            })?;
193            dataset
194                .write(slice)
195                .map_err(|e| SklearsError::InvalidInput(format!("Failed to write data: {e}")))?;
196        }
197
198        if let Some(ref vt) = results.vt_matrix {
199            self.write_matrix_to_group(&group, "VT", vt)?;
200        }
201
202        if let Some(ref components) = results.components {
203            self.write_matrix_to_group(&group, "components", components)?;
204        }
205
206        if let Some(ref eigenvalues) = results.eigenvalues {
207            let dataset = group
208                .new_dataset::<Float>()
209                .shape([eigenvalues.len()])
210                .create("eigenvalues")
211                .map_err(|e| {
212                    SklearsError::InvalidInput(format!("Failed to create dataset: {}", e))
213                })?;
214
215            let ev_slice = eigenvalues.as_slice().ok_or_else(|| {
216                SklearsError::InvalidInput("eigenvalues array not contiguous".to_string())
217            })?;
218            dataset
219                .write(ev_slice)
220                .map_err(|e| SklearsError::InvalidInput(format!("Failed to write data: {e}")))?;
221        }
222
223        // Write metadata
224        self.write_decomposition_metadata(&group, results)?;
225
226        Ok(())
227    }
228
229    /// Read decomposition results from HDF5 file
230    pub fn read_decomposition_results<P: AsRef<Path>>(
231        &self,
232        file_path: P,
233    ) -> Result<DecompositionResults> {
234        let file = File::open(file_path)
235            .map_err(|e| SklearsError::InvalidInput(format!("Failed to open HDF5 file: {}", e)))?;
236
237        let group = file
238            .group("decomposition")
239            .map_err(|e| SklearsError::InvalidInput(format!("Failed to open group: {}", e)))?;
240
241        let mut results = DecompositionResults::default();
242
243        // Read matrices if they exist
244        if group.link_exists("U") {
245            results.u_matrix = Some(self.read_matrix_from_group(&group, "U")?);
246        }
247
248        if group.link_exists("singular_values") {
249            let dataset = group.dataset("singular_values").map_err(|e| {
250                SklearsError::InvalidInput(format!("Failed to open dataset: {}", e))
251            })?;
252
253            // Read as raw vector to avoid ndarray version mismatch
254            let data: Vec<Float> = dataset
255                .read_raw::<Float>()
256                .map_err(|e| SklearsError::InvalidInput(format!("Failed to read data: {}", e)))?;
257
258            results.singular_values = Some(Array1::from_vec(data));
259        }
260
261        if group.link_exists("VT") {
262            results.vt_matrix = Some(self.read_matrix_from_group(&group, "VT")?);
263        }
264
265        if group.link_exists("components") {
266            results.components = Some(self.read_matrix_from_group(&group, "components")?);
267        }
268
269        if group.link_exists("eigenvalues") {
270            let dataset = group.dataset("eigenvalues").map_err(|e| {
271                SklearsError::InvalidInput(format!("Failed to open dataset: {}", e))
272            })?;
273
274            // Read as raw vector to avoid ndarray version mismatch
275            let data: Vec<Float> = dataset
276                .read_raw::<Float>()
277                .map_err(|e| SklearsError::InvalidInput(format!("Failed to read data: {}", e)))?;
278
279            results.eigenvalues = Some(Array1::from_vec(data));
280        }
281
282        // Read metadata
283        results.metadata = self.read_decomposition_metadata(&group)?;
284
285        Ok(results)
286    }
287
288    /// List datasets in HDF5 file
289    pub fn list_datasets<P: AsRef<Path>>(&self, file_path: P) -> Result<Vec<String>> {
290        let file = File::open(file_path)
291            .map_err(|e| SklearsError::InvalidInput(format!("Failed to open HDF5 file: {}", e)))?;
292
293        let mut datasets = Vec::new();
294        self.collect_datasets(&file, "", &mut datasets)?;
295
296        Ok(datasets)
297    }
298
299    // Helper methods
300    fn write_matrix_to_group(
301        &self,
302        group: &Group,
303        name: &str,
304        matrix: &Array2<Float>,
305    ) -> Result<()> {
306        let shape = matrix.shape();
307        let dataset = group
308            .new_dataset::<Float>()
309            .shape(shape)
310            .chunk(self.config.chunk_size.unwrap_or((shape[0], shape[1])))
311            .deflate(self.config.compression_level)
312            .create(name)
313            .map_err(|e| SklearsError::InvalidInput(format!("Failed to create dataset: {}", e)))?;
314
315        if matrix.is_standard_layout() {
316            let slice = matrix.as_slice().ok_or_else(|| {
317                SklearsError::InvalidInput(format!("matrix '{}' not contiguous", name))
318            })?;
319            dataset
320                .write(slice)
321                .map_err(|e| SklearsError::InvalidInput(format!("Failed to write data: {e}")))?;
322        } else {
323            let standard_matrix = matrix.to_owned();
324            let slice = standard_matrix.as_slice().ok_or_else(|| {
325                SklearsError::InvalidInput(format!("matrix copy '{}' not contiguous", name))
326            })?;
327            dataset
328                .write(slice)
329                .map_err(|e| SklearsError::InvalidInput(format!("Failed to write data: {e}")))?;
330        }
331
332        Ok(())
333    }
334
335    fn read_matrix_from_group(&self, group: &Group, name: &str) -> Result<Array2<Float>> {
336        let dataset = group
337            .dataset(name)
338            .map_err(|e| SklearsError::InvalidInput(format!("Failed to open dataset: {}", e)))?;
339
340        let shape = dataset.shape();
341        if shape.len() != 2 {
342            return Err(SklearsError::InvalidInput(
343                "Dataset must be 2-dimensional".to_string(),
344            ));
345        }
346
347        // Read as 1D raw vector to avoid ndarray version mismatch
348        let data: Vec<Float> = dataset
349            .read_raw::<Float>()
350            .map_err(|e| SklearsError::InvalidInput(format!("Failed to read data: {}", e)))?;
351
352        Array2::from_shape_vec((shape[0], shape[1]), data)
353            .map_err(|e| SklearsError::InvalidInput(format!("Failed to create array: {}", e)))
354    }
355
356    fn write_metadata(&self, dataset: &Dataset, matrix: &Array2<Float>) -> Result<()> {
357        // Add matrix metadata as attributes
358        let shape = matrix.shape();
359        dataset
360            .new_attr::<i64>()
361            .create("shape")
362            .map_err(|e| SklearsError::InvalidInput(format!("Failed to create attribute: {}", e)))?
363            .write(&[shape[0] as i64, shape[1] as i64])
364            .map_err(|e| SklearsError::InvalidInput(format!("Failed to write attribute: {}", e)))?;
365
366        Ok(())
367    }
368
369    fn write_decomposition_metadata(
370        &self,
371        group: &Group,
372        results: &DecompositionResults,
373    ) -> Result<()> {
374        // Write metadata as attributes
375        if let Some(algorithm) = &results.metadata.get("algorithm") {
376            group
377                .new_attr::<hdf5::types::VarLenAscii>()
378                .create("algorithm")
379                .map_err(|e| {
380                    SklearsError::InvalidInput(format!("Failed to create attribute: {}", e))
381                })?
382                .write(&[
383                    hdf5::types::VarLenAscii::from_ascii(algorithm.as_bytes()).map_err(|e| {
384                        SklearsError::InvalidInput(format!("Invalid ASCII in algorithm name: {e}"))
385                    })?,
386                ])
387                .map_err(|e| {
388                    SklearsError::InvalidInput(format!("Failed to write attribute: {}", e))
389                })?;
390        }
391
392        Ok(())
393    }
394
395    fn read_decomposition_metadata(&self, _group: &Group) -> Result<HashMap<String, String>> {
396        // Read metadata attributes
397        let mut metadata = HashMap::new();
398        // Simplified metadata reading - in practice would iterate through all attributes
399        metadata.insert("format".to_string(), "HDF5".to_string());
400        Ok(metadata)
401    }
402
403    fn collect_datasets(
404        &self,
405        _item: &hdf5::Group,
406        _prefix: &str,
407        _datasets: &mut Vec<String>,
408    ) -> Result<()> {
409        // Simplified dataset collection - in practice would recursively walk the HDF5 structure
410        Ok(())
411    }
412}
413
414/// Sparse matrix support for efficient decomposition
415#[cfg(feature = "sparse")]
416#[derive(Default)]
417pub struct SparseMatrixSupport {
418    config: FormatConfig,
419}
420
421#[cfg(feature = "sparse")]
422impl SparseMatrixSupport {
423    /// Create new sparse matrix support instance
424    pub fn new() -> Self {
425        Self::default()
426    }
427
428    /// Create with custom configuration
429    pub fn with_config(config: FormatConfig) -> Self {
430        Self { config }
431    }
432
433    /// Convert dense matrix to sparse format
434    pub fn dense_to_sparse(&self, dense: &Array2<Float>, threshold: Float) -> Result<SparseMatrix> {
435        let (rows, cols) = dense.dim();
436        let mut row_indices = Vec::new();
437        let mut col_indices = Vec::new();
438        let mut values = Vec::new();
439
440        for i in 0..rows {
441            for j in 0..cols {
442                let val = dense[[i, j]];
443                if val.abs() > threshold {
444                    row_indices.push(i);
445                    col_indices.push(j);
446                    values.push(val);
447                }
448            }
449        }
450
451        let nnz = values.len();
452        let sparsity = 1.0 - (nnz as Float) / ((rows * cols) as Float);
453
454        Ok(SparseMatrix {
455            format: self.config.preferred_sparse_format,
456            shape: (rows, cols),
457            nnz,
458            sparsity,
459            row_indices,
460            col_indices,
461            values,
462        })
463    }
464
465    /// Convert sparse matrix to dense format
466    pub fn sparse_to_dense(&self, sparse: &SparseMatrix) -> Result<Array2<Float>> {
467        let (rows, cols) = sparse.shape;
468        let mut dense = Array2::<Float>::zeros((rows, cols));
469
470        for i in 0..sparse.nnz {
471            let row = sparse.row_indices[i];
472            let col = sparse.col_indices[i];
473            let val = sparse.values[i];
474            dense[[row, col]] = val;
475        }
476
477        Ok(dense)
478    }
479
480    /// Perform sparse matrix multiplication
481    pub fn sparse_multiply(&self, a: &SparseMatrix, b: &SparseMatrix) -> Result<SparseMatrix> {
482        if a.shape.1 != b.shape.0 {
483            return Err(SklearsError::InvalidInput(
484                "Matrix dimensions incompatible for multiplication".to_string(),
485            ));
486        }
487
488        // Simplified sparse matrix multiplication
489        // In practice, would use optimized sparse BLAS routines
490        let _result_rows = a.shape.0;
491        let _result_cols = b.shape.1;
492
493        // Convert to dense for multiplication (not optimal, but functional)
494        let dense_a = self.sparse_to_dense(a)?;
495        let dense_b = self.sparse_to_dense(b)?;
496        let dense_result = dense_a.dot(&dense_b);
497
498        // Convert back to sparse
499        self.dense_to_sparse(&dense_result, 1e-12)
500    }
501
502    /// Compute sparse SVD using iterative methods
503    pub fn sparse_svd(
504        &self,
505        sparse: &SparseMatrix,
506        k: usize,
507        max_iter: usize,
508    ) -> Result<SparseDecompositionResult> {
509        let (m, n) = sparse.shape;
510        let min_dim = m.min(n).min(k);
511
512        // Simplified sparse SVD - in practice would use specialized algorithms like ARPACK
513        let _dense_matrix = self.sparse_to_dense(sparse)?;
514
515        // Use power iteration for largest singular values
516        let u = Array2::<Float>::eye(m);
517        let s = Array1::<Float>::ones(min_dim);
518        let vt = Array2::<Float>::eye(n);
519
520        // Simplified power iteration (placeholder)
521        for _iter in 0..max_iter {
522            // Power iteration steps would go here
523            // For now, just use identity matrices
524        }
525
526        Ok(SparseDecompositionResult {
527            u: u.slice(scirs2_core::ndarray::s![.., ..min_dim]).to_owned(),
528            singular_values: s,
529            vt: vt.slice(scirs2_core::ndarray::s![..min_dim, ..]).to_owned(),
530            iterations: max_iter,
531            converged: true,
532        })
533    }
534
535    /// Get sparse matrix statistics
536    pub fn get_sparse_stats(&self, sparse: &SparseMatrix) -> SparseStats {
537        SparseStats {
538            shape: sparse.shape,
539            nnz: sparse.nnz,
540            sparsity: sparse.sparsity,
541            memory_usage_bytes: sparse.memory_usage(),
542            format: sparse.format,
543        }
544    }
545}
546
547/// Sparse matrix representation
548#[derive(Debug, Clone)]
549pub struct SparseMatrix {
550    pub format: SparseFormat,
551    pub shape: (usize, usize),
552    pub nnz: usize,      // Number of non-zero elements
553    pub sparsity: Float, // Fraction of zero elements
554    pub row_indices: Vec<usize>,
555    pub col_indices: Vec<usize>,
556    pub values: Vec<Float>,
557}
558
559impl SparseMatrix {
560    /// Get memory usage in bytes
561    pub fn memory_usage(&self) -> usize {
562        std::mem::size_of::<Self>()
563            + self.row_indices.len() * std::mem::size_of::<usize>()
564            + self.col_indices.len() * std::mem::size_of::<usize>()
565            + self.values.len() * std::mem::size_of::<Float>()
566    }
567
568    /// Get density (opposite of sparsity)
569    pub fn density(&self) -> Float {
570        1.0 - self.sparsity
571    }
572}
573
574/// Result from sparse decomposition
575#[derive(Debug, Clone)]
576pub struct SparseDecompositionResult {
577    pub u: Array2<Float>,
578    pub singular_values: Array1<Float>,
579    pub vt: Array2<Float>,
580    pub iterations: usize,
581    pub converged: bool,
582}
583
584/// Statistics about sparse matrix
585#[derive(Debug, Clone)]
586pub struct SparseStats {
587    pub shape: (usize, usize),
588    pub nnz: usize,
589    pub sparsity: Float,
590    pub memory_usage_bytes: usize,
591    pub format: SparseFormat,
592}
593
594/// Decomposition results that can be stored in various formats
595#[derive(Debug, Clone, Default, Serialize, Deserialize)]
596pub struct DecompositionResults {
597    pub u_matrix: Option<Array2<Float>>,
598    pub singular_values: Option<Array1<Float>>,
599    pub vt_matrix: Option<Array2<Float>>,
600    pub components: Option<Array2<Float>>,
601    pub eigenvalues: Option<Array1<Float>>,
602    pub metadata: HashMap<String, String>,
603}
604
605impl DecompositionResults {
606    /// Create new empty decomposition results
607    pub fn new() -> Self {
608        Self::default()
609    }
610
611    /// Add metadata
612    pub fn with_metadata(mut self, key: String, value: String) -> Self {
613        self.metadata.insert(key, value);
614        self
615    }
616
617    /// Set algorithm name
618    pub fn with_algorithm(self, algorithm: &str) -> Self {
619        self.with_metadata("algorithm".to_string(), algorithm.to_string())
620    }
621
622    /// Check if results contain SVD components
623    pub fn has_svd(&self) -> bool {
624        self.u_matrix.is_some() && self.singular_values.is_some() && self.vt_matrix.is_some()
625    }
626
627    /// Check if results contain PCA components
628    pub fn has_pca(&self) -> bool {
629        self.components.is_some() && self.eigenvalues.is_some()
630    }
631}
632
633/// Memory-mapped matrix operations for large datasets
634pub struct MemoryMappedMatrix {
635    file_path: std::path::PathBuf,
636    shape: (usize, usize),
637    mmap: memmap2::Mmap,
638}
639
640impl MemoryMappedMatrix {
641    /// Create memory-mapped matrix from file
642    pub fn new<P: AsRef<Path>>(file_path: P, shape: (usize, usize)) -> Result<Self> {
643        let path = file_path.as_ref().to_path_buf();
644        let file = std::fs::File::open(&path).map_err(|e| {
645            SklearsError::InvalidInput(format!("Failed to open file '{}': {}", path.display(), e))
646        })?;
647
648        let mmap = unsafe {
649            memmap2::MmapOptions::new().map(&file).map_err(|e| {
650                SklearsError::InvalidInput(format!(
651                    "Failed to memory map file '{}': {}",
652                    path.display(),
653                    e
654                ))
655            })?
656        };
657
658        // Verify file size matches expected shape
659        let expected_size = shape.0 * shape.1 * std::mem::size_of::<Float>();
660        if mmap.len() != expected_size {
661            return Err(SklearsError::InvalidInput(format!(
662                "File '{}' size {} bytes does not match expected matrix dimensions {}x{} ({} bytes)",
663                path.display(),
664                mmap.len(),
665                shape.0,
666                shape.1,
667                expected_size
668            )));
669        }
670
671        Ok(Self {
672            file_path: path,
673            shape,
674            mmap,
675        })
676    }
677
678    /// Get the file path used for this memory-mapped matrix
679    pub fn file_path(&self) -> &std::path::Path {
680        &self.file_path
681    }
682
683    /// Get matrix shape
684    pub fn shape(&self) -> (usize, usize) {
685        self.shape
686    }
687
688    /// Get raw data slice
689    pub fn as_slice(&self) -> &[u8] {
690        &self.mmap
691    }
692
693    /// Read a chunk of the matrix
694    pub fn read_chunk(&self, start_row: usize, end_row: usize) -> Result<Array2<Float>> {
695        let (total_rows, cols) = self.shape;
696
697        if start_row >= total_rows || end_row > total_rows || start_row >= end_row {
698            return Err(SklearsError::InvalidInput(format!(
699                "Invalid row range [{}, {}) for file '{}' with {} rows",
700                start_row,
701                end_row,
702                self.file_path.display(),
703                total_rows
704            )));
705        }
706
707        let chunk_rows = end_row - start_row;
708        let start_idx = start_row * cols * std::mem::size_of::<Float>();
709        let end_idx = end_row * cols * std::mem::size_of::<Float>();
710
711        let chunk_bytes = &self.mmap[start_idx..end_idx];
712
713        // Convert bytes to Float values
714        let float_slice = unsafe {
715            std::slice::from_raw_parts(
716                chunk_bytes.as_ptr() as *const Float,
717                chunk_bytes.len() / std::mem::size_of::<Float>(),
718            )
719        };
720
721        Array2::from_shape_vec((chunk_rows, cols), float_slice.to_vec()).map_err(|e| {
722            SklearsError::InvalidInput(format!(
723                "Failed to create array from file '{}': {}",
724                self.file_path.display(),
725                e
726            ))
727        })
728    }
729}
730
731#[allow(non_snake_case)]
732#[cfg(test)]
733mod tests {
734    use super::*;
735
736    #[test]
737    fn test_format_config_default() {
738        let config = FormatConfig::default();
739        assert_eq!(config.compression_level, 6);
740        assert!(config.enable_checksums);
741        assert_eq!(config.preferred_sparse_format, SparseFormat::CSR);
742    }
743
744    #[test]
745    fn test_decomposition_results() {
746        let results = DecompositionResults::new()
747            .with_algorithm("PCA")
748            .with_metadata("version".to_string(), "1.0".to_string());
749
750        assert_eq!(results.metadata.get("algorithm"), Some(&"PCA".to_string()));
751        assert_eq!(results.metadata.get("version"), Some(&"1.0".to_string()));
752        assert!(!results.has_svd());
753        assert!(!results.has_pca());
754    }
755
756    #[cfg(feature = "sparse")]
757    #[test]
758    fn test_sparse_matrix_support() {
759        let config = FormatConfig::default();
760        let sparse_support = SparseMatrixSupport::with_config(config);
761
762        // Create a simple dense matrix
763        let dense =
764            Array2::from_shape_vec((3, 3), vec![1.0, 0.0, 2.0, 0.0, 0.0, 0.0, 3.0, 0.0, 4.0])
765                .expect("operation should succeed");
766
767        // Convert to sparse
768        let sparse = sparse_support
769            .dense_to_sparse(&dense, 0.5)
770            .expect("parsing should succeed");
771        assert_eq!(sparse.nnz, 4); // Four non-zero elements
772        assert!(sparse.sparsity > 0.0);
773
774        // Convert back to dense
775        let reconstructed = sparse_support
776            .sparse_to_dense(&sparse)
777            .expect("parsing should succeed");
778        assert_eq!(reconstructed.shape(), dense.shape());
779
780        // Get statistics
781        let stats = sparse_support.get_sparse_stats(&sparse);
782        assert_eq!(stats.nnz, 4);
783        assert_eq!(stats.shape, (3, 3));
784    }
785
786    #[test]
787    fn test_sparse_matrix_memory_usage() {
788        let sparse = SparseMatrix {
789            format: SparseFormat::CSR,
790            shape: (1000, 1000),
791            nnz: 100,
792            sparsity: 0.9999,
793            row_indices: vec![0; 100],
794            col_indices: vec![0; 100],
795            values: vec![1.0; 100],
796        };
797
798        let memory_usage = sparse.memory_usage();
799        assert!(memory_usage > 0);
800
801        let density = sparse.density();
802        assert!((density - 0.0001).abs() < 1e-10);
803    }
804
805    #[cfg(feature = "hdf5-support")]
806    #[test]
807    fn test_hdf5_support_creation() {
808        let hdf5_support = HDF5Support::new();
809        assert_eq!(hdf5_support.config.compression_level, 6);
810
811        let custom_config = FormatConfig {
812            compression_level: 9,
813            ..FormatConfig::default()
814        };
815        let custom_hdf5 = HDF5Support::with_config(custom_config);
816        assert_eq!(custom_hdf5.config.compression_level, 9);
817    }
818
819    #[test]
820    fn test_sparse_format_enum() {
821        let formats = vec![SparseFormat::COO, SparseFormat::CSR, SparseFormat::CSC];
822
823        for format in formats {
824            match format {
825                SparseFormat::COO => assert_eq!(format, SparseFormat::COO),
826                SparseFormat::CSR => assert_eq!(format, SparseFormat::CSR),
827                SparseFormat::CSC => assert_eq!(format, SparseFormat::CSC),
828            }
829        }
830    }
831}