1#[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};
26use std::collections::HashMap;
30use std::path::Path;
31
32#[derive(Debug, Clone)]
34pub struct FormatConfig {
35 pub compression_level: u8,
37 pub chunk_size: Option<(usize, usize)>,
39 pub enable_checksums: bool,
41 pub max_memory_mb: Option<usize>,
43 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum SparseFormat {
62 COO,
64 CSR,
66 CSC,
68}
69
70#[cfg(feature = "hdf5-support")]
72#[derive(Default)]
73pub struct HDF5Support {
74 config: FormatConfig,
75}
76
77#[cfg(feature = "hdf5-support")]
78impl HDF5Support {
79 pub fn new() -> Self {
81 Self::default()
82 }
83
84 pub fn with_config(config: FormatConfig) -> Self {
86 Self { config }
87 }
88
89 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 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 self.write_metadata(&dataset, matrix)?;
129
130 Ok(())
131 }
132
133 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 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 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 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 self.write_decomposition_metadata(&group, results)?;
225
226 Ok(())
227 }
228
229 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 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 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 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 results.metadata = self.read_decomposition_metadata(&group)?;
284
285 Ok(results)
286 }
287
288 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 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 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 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 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 let mut metadata = HashMap::new();
398 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 Ok(())
411 }
412}
413
414#[cfg(feature = "sparse")]
416#[derive(Default)]
417pub struct SparseMatrixSupport {
418 config: FormatConfig,
419}
420
421#[cfg(feature = "sparse")]
422impl SparseMatrixSupport {
423 pub fn new() -> Self {
425 Self::default()
426 }
427
428 pub fn with_config(config: FormatConfig) -> Self {
430 Self { config }
431 }
432
433 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 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 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 let _result_rows = a.shape.0;
491 let _result_cols = b.shape.1;
492
493 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 self.dense_to_sparse(&dense_result, 1e-12)
500 }
501
502 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 let _dense_matrix = self.sparse_to_dense(sparse)?;
514
515 let u = Array2::<Float>::eye(m);
517 let s = Array1::<Float>::ones(min_dim);
518 let vt = Array2::<Float>::eye(n);
519
520 for _iter in 0..max_iter {
522 }
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 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#[derive(Debug, Clone)]
549pub struct SparseMatrix {
550 pub format: SparseFormat,
551 pub shape: (usize, usize),
552 pub nnz: usize, pub sparsity: Float, pub row_indices: Vec<usize>,
555 pub col_indices: Vec<usize>,
556 pub values: Vec<Float>,
557}
558
559impl SparseMatrix {
560 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 pub fn density(&self) -> Float {
570 1.0 - self.sparsity
571 }
572}
573
574#[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#[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#[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 pub fn new() -> Self {
608 Self::default()
609 }
610
611 pub fn with_metadata(mut self, key: String, value: String) -> Self {
613 self.metadata.insert(key, value);
614 self
615 }
616
617 pub fn with_algorithm(self, algorithm: &str) -> Self {
619 self.with_metadata("algorithm".to_string(), algorithm.to_string())
620 }
621
622 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 pub fn has_pca(&self) -> bool {
629 self.components.is_some() && self.eigenvalues.is_some()
630 }
631}
632
633pub struct MemoryMappedMatrix {
635 file_path: std::path::PathBuf,
636 shape: (usize, usize),
637 mmap: memmap2::Mmap,
638}
639
640impl MemoryMappedMatrix {
641 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 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 pub fn file_path(&self) -> &std::path::Path {
680 &self.file_path
681 }
682
683 pub fn shape(&self) -> (usize, usize) {
685 self.shape
686 }
687
688 pub fn as_slice(&self) -> &[u8] {
690 &self.mmap
691 }
692
693 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 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 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 let sparse = sparse_support
769 .dense_to_sparse(&dense, 0.5)
770 .expect("parsing should succeed");
771 assert_eq!(sparse.nnz, 4); assert!(sparse.sparsity > 0.0);
773
774 let reconstructed = sparse_support
776 .sparse_to_dense(&sparse)
777 .expect("parsing should succeed");
778 assert_eq!(reconstructed.shape(), dense.shape());
779
780 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}