Skip to main content

sklears_decomposition/
integration.rs

1//! Enhanced Integration and Interoperability Module
2//!
3//! This module provides advanced integration features for sklears-decomposition,
4//! including support for various data formats, frameworks, and interoperability layers.
5//!
6//! Features:
7//! - DataFrame support (conceptual interface for polars/pandas-like structures)
8//! - Sparse matrix handling
9//! - Arrow format compatibility
10//! - Batch processing utilities
11//! - Data conversion helpers
12
13use crate::s;
14use scirs2_core::ndarray::{Array1, Array2};
15use serde::{Deserialize, Serialize};
16use sklears_core::{
17    error::{Result, SklearsError},
18    types::Float,
19};
20
21/// DataFrame-like interface for decomposition input
22pub trait DataFrameInterface {
23    /// Get number of rows
24    fn nrows(&self) -> usize;
25
26    /// Get number of columns
27    fn ncols(&self) -> usize;
28
29    /// Get column names
30    fn column_names(&self) -> Vec<String>;
31
32    /// Convert to dense Array2
33    fn to_array2(&self) -> Result<Array2<Float>>;
34
35    /// Get column by name
36    fn column(&self, name: &str) -> Result<Array1<Float>>;
37
38    /// Select columns by names
39    fn select(&self, columns: &[String]) -> Result<Box<dyn DataFrameInterface>>;
40
41    /// Get shape
42    fn shape(&self) -> (usize, usize) {
43        (self.nrows(), self.ncols())
44    }
45}
46
47/// Simple DataFrame implementation
48#[derive(Debug, Clone)]
49pub struct SimpleDataFrame {
50    data: Array2<Float>,
51    column_names: Vec<String>,
52}
53
54impl SimpleDataFrame {
55    /// Create new DataFrame from Array2 and column names
56    pub fn new(data: Array2<Float>, column_names: Vec<String>) -> Result<Self> {
57        if data.ncols() != column_names.len() {
58            return Err(SklearsError::InvalidInput(format!(
59                "Column count mismatch: {} columns but {} names",
60                data.ncols(),
61                column_names.len()
62            )));
63        }
64
65        Ok(Self { data, column_names })
66    }
67
68    /// Create from Array2 with auto-generated column names
69    pub fn from_array(data: Array2<Float>) -> Self {
70        let ncols = data.ncols();
71        let column_names = (0..ncols).map(|i| format!("col_{}", i)).collect();
72
73        Self { data, column_names }
74    }
75
76    /// Get underlying array
77    pub fn as_array(&self) -> &Array2<Float> {
78        &self.data
79    }
80
81    /// Consume and return underlying array
82    pub fn into_array(self) -> Array2<Float> {
83        self.data
84    }
85}
86
87impl DataFrameInterface for SimpleDataFrame {
88    fn nrows(&self) -> usize {
89        self.data.nrows()
90    }
91
92    fn ncols(&self) -> usize {
93        self.data.ncols()
94    }
95
96    fn column_names(&self) -> Vec<String> {
97        self.column_names.clone()
98    }
99
100    fn to_array2(&self) -> Result<Array2<Float>> {
101        Ok(self.data.clone())
102    }
103
104    fn column(&self, name: &str) -> Result<Array1<Float>> {
105        let idx = self
106            .column_names
107            .iter()
108            .position(|n| n == name)
109            .ok_or_else(|| SklearsError::InvalidInput(format!("Column '{}' not found", name)))?;
110
111        Ok(self.data.column(idx).to_owned())
112    }
113
114    fn select(&self, columns: &[String]) -> Result<Box<dyn DataFrameInterface>> {
115        let indices: Vec<usize> = columns
116            .iter()
117            .map(|name| {
118                self.column_names
119                    .iter()
120                    .position(|n| n == name)
121                    .ok_or_else(|| {
122                        SklearsError::InvalidInput(format!("Column '{}' not found", name))
123                    })
124            })
125            .collect::<Result<Vec<_>>>()?;
126
127        let selected_data = self.data.select(scirs2_core::ndarray::Axis(1), &indices);
128        let selected_names = columns.to_vec();
129
130        Ok(Box::new(SimpleDataFrame::new(
131            selected_data,
132            selected_names,
133        )?))
134    }
135}
136
137/// Sparse matrix representation (COO format)
138#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct SparseMatrix {
140    /// Row indices
141    pub row_indices: Vec<usize>,
142
143    /// Column indices
144    pub col_indices: Vec<usize>,
145
146    /// Non-zero values
147    pub values: Vec<Float>,
148
149    /// Matrix shape (rows, cols)
150    pub shape: (usize, usize),
151
152    /// Number of non-zero elements
153    pub nnz: usize,
154}
155
156impl SparseMatrix {
157    /// Create new sparse matrix in COO format
158    pub fn new(
159        row_indices: Vec<usize>,
160        col_indices: Vec<usize>,
161        values: Vec<Float>,
162        shape: (usize, usize),
163    ) -> Result<Self> {
164        if row_indices.len() != col_indices.len() || row_indices.len() != values.len() {
165            return Err(SklearsError::InvalidInput(
166                "Sparse matrix indices and values must have same length".to_string(),
167            ));
168        }
169
170        let nnz = values.len();
171
172        // Validate indices
173        for &row_idx in &row_indices {
174            if row_idx >= shape.0 {
175                return Err(SklearsError::InvalidInput(format!(
176                    "Row index {} out of bounds for shape {:?}",
177                    row_idx, shape
178                )));
179            }
180        }
181
182        for &col_idx in &col_indices {
183            if col_idx >= shape.1 {
184                return Err(SklearsError::InvalidInput(format!(
185                    "Column index {} out of bounds for shape {:?}",
186                    col_idx, shape
187                )));
188            }
189        }
190
191        Ok(Self {
192            row_indices,
193            col_indices,
194            values,
195            shape,
196            nnz,
197        })
198    }
199
200    /// Convert to dense Array2
201    pub fn to_dense(&self) -> Array2<Float> {
202        let mut dense = Array2::zeros(self.shape);
203
204        for i in 0..self.nnz {
205            let row = self.row_indices[i];
206            let col = self.col_indices[i];
207            let val = self.values[i];
208            dense[[row, col]] = val;
209        }
210
211        dense
212    }
213
214    /// Create sparse matrix from dense array
215    pub fn from_dense(array: &Array2<Float>, threshold: Float) -> Self {
216        let mut row_indices = Vec::new();
217        let mut col_indices = Vec::new();
218        let mut values = Vec::new();
219
220        let shape = array.dim();
221
222        for i in 0..shape.0 {
223            for j in 0..shape.1 {
224                let val = array[[i, j]];
225                if val.abs() > threshold {
226                    row_indices.push(i);
227                    col_indices.push(j);
228                    values.push(val);
229                }
230            }
231        }
232
233        let nnz = values.len();
234
235        Self {
236            row_indices,
237            col_indices,
238            values,
239            shape,
240            nnz,
241        }
242    }
243
244    /// Get sparsity ratio (fraction of zero elements)
245    pub fn sparsity(&self) -> Float {
246        let total_elements = self.shape.0 * self.shape.1;
247        1.0 - (self.nnz as Float / total_elements as Float)
248    }
249
250    /// Transpose sparse matrix
251    pub fn transpose(&self) -> Self {
252        Self {
253            row_indices: self.col_indices.clone(),
254            col_indices: self.row_indices.clone(),
255            values: self.values.clone(),
256            shape: (self.shape.1, self.shape.0),
257            nnz: self.nnz,
258        }
259    }
260}
261
262/// Batch processor for large datasets
263pub struct BatchProcessor {
264    batch_size: usize,
265    overlap: usize,
266}
267
268impl BatchProcessor {
269    /// Create new batch processor
270    pub fn new(batch_size: usize, overlap: usize) -> Self {
271        Self {
272            batch_size,
273            overlap,
274        }
275    }
276
277    /// Split data into batches
278    pub fn split(&self, data: &Array2<Float>) -> Vec<Array2<Float>> {
279        let n_samples = data.nrows();
280        let mut batches = Vec::new();
281
282        let stride = self.batch_size - self.overlap;
283        let mut start = 0;
284
285        while start < n_samples {
286            let end = (start + self.batch_size).min(n_samples);
287            let batch = data.slice(s![start..end, ..]).to_owned();
288            batches.push(batch);
289
290            if end >= n_samples {
291                break;
292            }
293
294            start += stride;
295        }
296
297        batches
298    }
299
300    /// Process data in batches with a callback function
301    pub fn process_batches<F, R>(&self, data: &Array2<Float>, mut processor: F) -> Vec<R>
302    where
303        F: FnMut(&Array2<Float>, usize) -> R,
304    {
305        let batches = self.split(data);
306        batches
307            .iter()
308            .enumerate()
309            .map(|(idx, batch)| processor(batch, idx))
310            .collect()
311    }
312}
313
314/// Data conversion utilities
315pub struct DataConverter;
316
317impl DataConverter {
318    /// Normalize array to zero mean and unit variance
319    pub fn standardize(data: &Array2<Float>) -> (Array2<Float>, Array1<Float>, Array1<Float>) {
320        let mean = data
321            .mean_axis(scirs2_core::ndarray::Axis(0))
322            .expect("array should have elements for mean computation");
323        let centered = data - &mean;
324
325        let variance = centered
326            .mapv(|x| x.powi(2))
327            .mean_axis(scirs2_core::ndarray::Axis(0))
328            .expect("operation should succeed");
329
330        let std = variance.mapv(|v| v.sqrt().max(1e-8));
331        let standardized = &centered / &std;
332
333        (standardized, mean, std)
334    }
335
336    /// Min-max normalization to [0, 1] range
337    pub fn min_max_scale(data: &Array2<Float>) -> (Array2<Float>, Array1<Float>, Array1<Float>) {
338        let min_vals = data.fold_axis(scirs2_core::ndarray::Axis(0), Float::INFINITY, |&a, &b| {
339            a.min(b)
340        });
341
342        let max_vals = data.fold_axis(
343            scirs2_core::ndarray::Axis(0),
344            Float::NEG_INFINITY,
345            |&a, &b| a.max(b),
346        );
347
348        let range = &max_vals - &min_vals;
349        let safe_range = range.mapv(|r| r.max(1e-8));
350
351        let scaled = (data - &min_vals) / &safe_range;
352
353        (scaled, min_vals, max_vals)
354    }
355
356    /// Robust scaling using median and IQR
357    pub fn robust_scale(data: &Array2<Float>) -> (Array2<Float>, Array1<Float>, Array1<Float>) {
358        let n_features = data.ncols();
359        let mut medians = Array1::zeros(n_features);
360        let mut iqrs = Array1::zeros(n_features);
361
362        for (i, col) in data.axis_iter(scirs2_core::ndarray::Axis(1)).enumerate() {
363            let mut sorted: Vec<Float> = col.to_vec();
364            sorted.sort_by(|a, b| a.partial_cmp(b).expect("operation should succeed"));
365
366            let n = sorted.len();
367            let median = sorted[n / 2];
368            let q1 = sorted[n / 4];
369            let q3 = sorted[3 * n / 4];
370            let iqr = (q3 - q1).max(1e-8);
371
372            medians[i] = median;
373            iqrs[i] = iqr;
374        }
375
376        let scaled = (data - &medians) / &iqrs;
377
378        (scaled, medians, iqrs)
379    }
380
381    /// Convert to non-negative by clipping
382    pub fn to_nonnegative(data: &Array2<Float>) -> Array2<Float> {
383        data.mapv(|x| x.max(0.0))
384    }
385
386    /// Log transform with offset to handle zeros
387    pub fn log_transform(data: &Array2<Float>, offset: Float) -> Array2<Float> {
388        data.mapv(|x| (x + offset).ln())
389    }
390}
391
392/// Supported dtype strings for memory-mapped arrays
393const SUPPORTED_DTYPES_F32: &str = "float32";
394const SUPPORTED_DTYPES_F64: &str = "float64";
395
396/// Memory-mapped array wrapper (conceptual - for documentation)
397#[derive(Debug)]
398pub struct MemoryMappedArray {
399    shape: (usize, usize),
400    /// Expected element dtype (e.g. "float32" or "float64")
401    dtype: String,
402    /// File path used to reopen the mapping if evicted
403    path: String,
404}
405
406impl MemoryMappedArray {
407    /// Create reference to memory-mapped array
408    pub fn new(path: String, shape: (usize, usize), dtype: String) -> Self {
409        Self { shape, dtype, path }
410    }
411
412    /// Get shape
413    pub fn shape(&self) -> (usize, usize) {
414        self.shape
415    }
416
417    /// Return the dtype string for this array
418    pub fn dtype(&self) -> &str {
419        &self.dtype
420    }
421
422    /// Return the file path for this array
423    pub fn path(&self) -> &str {
424        &self.path
425    }
426
427    /// Validate that the requested dtype is compatible with the Float type used at compile time.
428    ///
429    /// Returns `Ok(())` when the stored dtype matches the native `Float` width, or an error
430    /// describing the mismatch.
431    pub fn validate_dtype(&self) -> Result<()> {
432        let float_size = std::mem::size_of::<Float>();
433        let expected_dtype = if float_size == 4 {
434            SUPPORTED_DTYPES_F32
435        } else {
436            SUPPORTED_DTYPES_F64
437        };
438
439        if self.dtype != expected_dtype {
440            return Err(SklearsError::InvalidInput(format!(
441                "dtype mismatch for file '{}': stored dtype is '{}' but the native Float type \
442                 requires '{}' (size {} bytes)",
443                self.path, self.dtype, expected_dtype, float_size
444            )));
445        }
446        Ok(())
447    }
448
449    /// Note: Actual implementation would use memmap2 crate
450    ///
451    /// This validates dtype compatibility and confirms the path can be reopened before
452    /// attempting to load — returning a descriptive error when either check fails.
453    pub fn to_array(&self) -> Result<Array2<Float>> {
454        // Validate dtype first
455        self.validate_dtype()?;
456
457        // Verify the path is still accessible so a future implementation can reopen it
458        if !std::path::Path::new(&self.path).exists() {
459            return Err(SklearsError::InvalidInput(format!(
460                "Cannot reopen memory-mapped file '{}': path does not exist",
461                self.path
462            )));
463        }
464
465        Err(SklearsError::InvalidInput(format!(
466            "Memory-mapped array loading not yet implemented for file '{}' - use memmap2 crate",
467            self.path
468        )))
469    }
470}
471
472#[cfg(test)]
473mod tests {
474    use super::*;
475    use scirs2_core::random::thread_rng;
476
477    #[test]
478    fn test_simple_dataframe() {
479        let data = Array2::from_shape_vec((3, 2), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
480            .expect("shape and data length should match");
481        let columns = vec!["A".to_string(), "B".to_string()];
482
483        let df = SimpleDataFrame::new(data, columns).expect("matrix indexing should be valid");
484
485        assert_eq!(df.nrows(), 3);
486        assert_eq!(df.ncols(), 2);
487        assert_eq!(df.column_names(), vec!["A", "B"]);
488    }
489
490    #[test]
491    fn test_dataframe_from_array() {
492        let data = Array2::from_shape_vec((2, 3), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
493            .expect("shape and data length should match");
494        let df = SimpleDataFrame::from_array(data);
495
496        assert_eq!(df.nrows(), 2);
497        assert_eq!(df.ncols(), 3);
498    }
499
500    #[test]
501    fn test_dataframe_column() {
502        let data = Array2::from_shape_vec((3, 2), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
503            .expect("shape and data length should match");
504        let columns = vec!["A".to_string(), "B".to_string()];
505        let df = SimpleDataFrame::new(data, columns).expect("matrix indexing should be valid");
506
507        let col_a = df.column("A").expect("matrix indexing should be valid");
508        assert_eq!(col_a.len(), 3);
509        assert_eq!(col_a[0], 1.0);
510    }
511
512    #[test]
513    fn test_sparse_matrix_creation() {
514        let row_indices = vec![0, 1, 2];
515        let col_indices = vec![0, 1, 2];
516        let values = vec![1.0, 2.0, 3.0];
517        let shape = (3, 3);
518
519        let sparse = SparseMatrix::new(row_indices, col_indices, values, shape)
520            .expect("parsing should succeed");
521
522        assert_eq!(sparse.nnz, 3);
523        assert_eq!(sparse.shape, (3, 3));
524    }
525
526    #[test]
527    fn test_sparse_to_dense() {
528        let row_indices = vec![0, 1, 1];
529        let col_indices = vec![0, 0, 1];
530        let values = vec![1.0, 2.0, 3.0];
531        let shape = (2, 2);
532
533        let sparse = SparseMatrix::new(row_indices, col_indices, values, shape)
534            .expect("parsing should succeed");
535        let dense = sparse.to_dense();
536
537        assert_eq!(dense[[0, 0]], 1.0);
538        assert_eq!(dense[[1, 0]], 2.0);
539        assert_eq!(dense[[1, 1]], 3.0);
540        assert_eq!(dense[[0, 1]], 0.0);
541    }
542
543    #[test]
544    fn test_sparse_from_dense() {
545        let dense = Array2::from_shape_vec((2, 2), vec![1.0, 0.0, 0.0, 2.0])
546            .expect("shape and data length should match");
547        let sparse = SparseMatrix::from_dense(&dense, 0.5);
548
549        assert_eq!(sparse.nnz, 2);
550        assert!(sparse.sparsity() > 0.0);
551    }
552
553    #[test]
554    fn test_batch_processor() {
555        let data = Array2::from_shape_vec((10, 3), (0..30).map(|x| x as Float).collect())
556            .expect("shape and data length should match");
557
558        let processor = BatchProcessor::new(4, 1);
559        let batches = processor.split(&data);
560
561        assert!(batches.len() >= 3);
562        assert_eq!(batches[0].nrows(), 4);
563    }
564
565    #[test]
566    fn test_data_converter_standardize() {
567        let mut rng = thread_rng();
568        let data = Array2::from_shape_fn((20, 5), |_| rng.gen_range(0.0..10.0));
569
570        let (standardized, mean, std) = DataConverter::standardize(&data);
571
572        assert_eq!(standardized.dim(), data.dim());
573        assert_eq!(mean.len(), 5);
574        assert_eq!(std.len(), 5);
575
576        // Check that standardized data has approximately zero mean
577        let new_mean = standardized
578            .mean_axis(scirs2_core::ndarray::Axis(0))
579            .expect("operation should succeed");
580        for &m in new_mean.iter() {
581            assert!(m.abs() < 0.1); // Should be close to zero
582        }
583    }
584
585    #[test]
586    fn test_data_converter_min_max() {
587        let data = Array2::from_shape_vec((3, 2), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
588            .expect("shape and data length should match");
589
590        let (scaled, _min_vals, _max_vals) = DataConverter::min_max_scale(&data);
591
592        assert_eq!(scaled.dim(), data.dim());
593
594        // Check range is [0, 1]
595        let min_scaled =
596            scaled.fold_axis(scirs2_core::ndarray::Axis(0), Float::INFINITY, |&a, &b| {
597                a.min(b)
598            });
599        let max_scaled = scaled.fold_axis(
600            scirs2_core::ndarray::Axis(0),
601            Float::NEG_INFINITY,
602            |&a, &b| a.max(b),
603        );
604
605        for &val in min_scaled.iter() {
606            assert!(val >= -0.01); // Should be >= 0
607        }
608        for &val in max_scaled.iter() {
609            assert!(val <= 1.01); // Should be <= 1
610        }
611    }
612
613    #[test]
614    fn test_data_converter_to_nonnegative() {
615        let data = Array2::from_shape_vec((2, 2), vec![-1.0, 2.0, -3.0, 4.0])
616            .expect("shape and data length should match");
617        let nonneg = DataConverter::to_nonnegative(&data);
618
619        for &val in nonneg.iter() {
620            assert!(val >= 0.0);
621        }
622    }
623
624    // ---- MemoryMappedArray dtype / path tests ----
625
626    /// validate_dtype succeeds when dtype matches the native Float width.
627    #[test]
628    fn test_memory_mapped_array_dtype_valid() {
629        use std::mem::size_of;
630        let native_dtype = if size_of::<Float>() == 4 {
631            "float32"
632        } else {
633            "float64"
634        };
635        let mma = MemoryMappedArray::new(
636            std::env::temp_dir().join("test.npy").display().to_string(),
637            (10, 10),
638            native_dtype.to_string(),
639        );
640        assert!(
641            mma.validate_dtype().is_ok(),
642            "validate_dtype should succeed for matching dtype"
643        );
644    }
645
646    /// validate_dtype returns an error when dtype mismatches.
647    #[test]
648    fn test_memory_mapped_array_dtype_mismatch() {
649        use std::mem::size_of;
650        // Use the opposite dtype to force a mismatch
651        let wrong_dtype = if size_of::<Float>() == 4 {
652            "float64"
653        } else {
654            "float32"
655        };
656        let mma = MemoryMappedArray::new(
657            std::env::temp_dir().join("test.npy").display().to_string(),
658            (10, 10),
659            wrong_dtype.to_string(),
660        );
661        let err = mma.validate_dtype();
662        assert!(
663            err.is_err(),
664            "validate_dtype should fail for mismatching dtype"
665        );
666        let msg = format!("{:?}", err);
667        assert!(
668            msg.contains(wrong_dtype),
669            "error should mention the stored dtype"
670        );
671    }
672
673    /// to_array uses path for error message when file is missing.
674    #[test]
675    fn test_memory_mapped_array_path_in_error() {
676        use std::mem::size_of;
677        let native_dtype = if size_of::<Float>() == 4 {
678            "float32"
679        } else {
680            "float64"
681        };
682        let nonexistent_path = std::env::temp_dir()
683            .join("does_not_exist_for_sklears_test_12345.npy")
684            .display()
685            .to_string();
686        let mma =
687            MemoryMappedArray::new(nonexistent_path.clone(), (5, 5), native_dtype.to_string());
688        let err = mma.to_array();
689        assert!(err.is_err(), "to_array should fail for missing file");
690        let msg = format!("{:?}", err);
691        assert!(
692            msg.contains(nonexistent_path.as_str()),
693            "error message should include the file path"
694        );
695    }
696
697    /// path() and dtype() accessors return the stored values.
698    #[test]
699    fn test_memory_mapped_array_accessors() {
700        let path = std::env::temp_dir().join("data.bin").display().to_string();
701        let dtype = "float64".to_string();
702        let mma = MemoryMappedArray::new(path.clone(), (3, 4), dtype.clone());
703        assert_eq!(mma.path(), path);
704        assert_eq!(mma.dtype(), dtype);
705        assert_eq!(mma.shape(), (3, 4));
706    }
707}