Skip to main content

sklears_decomposition/
memory_efficiency.rs

1//! Memory efficiency improvements for decomposition algorithms
2//!
3//! This module provides memory-efficient implementations including:
4//! - Out-of-core decomposition methods for datasets larger than memory
5//! - Memory-mapped matrix operations for efficient disk access
6//! - Chunked processing for large matrices
7//! - Compression techniques for decomposed matrices
8//! - Lazy evaluation for decomposition chains
9
10use 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
16/// Type alias for complex operation function type
17type DecompositionOperation<T> = Box<dyn Fn(&T) -> SklearsResult<T>>;
18
19/// Out-of-core matrix decomposition for datasets larger than memory
20pub 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    /// Perform incremental SVD on large matrices using out-of-core processing
45    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        // Create temporary directory
52        std::fs::create_dir_all(&self.temp_dir).map_err(|e| {
53            SklearsError::InvalidInput(format!("Failed to create temp directory: {e}"))
54        })?;
55
56        // Initialize running SVD
57        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        // Process matrix in chunks
62        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); // Assuming f64
72        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            // Extract chunk from memory-mapped file
80            let chunk = self.extract_chunk(&mmap, start_row, actual_chunk_rows, n_cols)?;
81
82            // Update running SVD with this chunk
83            let (u_chunk, s_chunk, vt_chunk) = self.chunk_svd(&chunk)?;
84
85            // Merge with running decomposition
86            (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; // f64 = 8 bytes
105
106        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        // For now, use a simplified version - in a real implementation, you'd use proper SVD
129        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        // Create placeholder matrices - replace with actual SVD computation
133        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        // Simplified merge - in practice would use more sophisticated techniques
154        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
180/// Memory-mapped matrix operations for efficient disk access
181pub 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
254/// Chunked processing for large matrices
255pub 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/// Compression techniques for decomposed matrices
324#[derive(Debug, Clone)]
325pub enum CompressionMethod {
326    /// Store only significant singular values above threshold
327    SingularValueThreshold(f64),
328    /// Store only top-k components
329    TopK(usize),
330    /// Quantize values to reduce precision
331    Quantized(u8), // bits per value
332    /// Block-wise compression
333    BlockCompression(usize), // block size
334}
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; // f64 = 8 bytes
351
352        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        // Extract diagonal values from s matrix
384        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        // Quantize matrices to reduce precision
439        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        // Create diagonal matrix from singular values
493        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/// Compressed matrix representation
506#[derive(Debug, Clone)]
507pub enum CompressedMatrix {
508    Dense(Array2<f64>),
509    Sparse(Vec<(usize, usize, f64)>, (usize, usize)), // (row, col, val), shape
510    Quantized(Array2<f64>, u8),                       // quantized data, bits per value
511    BlockCompressed(Vec<Array2<f64>>, (usize, usize), usize), // blocks, shape, block_size
512}
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), // row, col, val indices
548            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
593/// Lazy evaluation for decomposition chains
594pub 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; // Invalidate cache
613        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(&current)?;
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); // (100 + 20 - 1) / 20 = 5 chunks
658    }
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); // ((3 * 2) + 1)^2 = 7^2 = 49
685
686        // Test caching
687        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}