Skip to main content

data_beans/sparse_backend/
zarr.rs

1#![allow(dead_code)]
2
3use crate::sparse_io::*;
4use legume_numeric::matrix::common_io::*;
5use log::info;
6use std::ops::Range;
7use std::sync::{Arc, OnceLock};
8use zarrs::array::chunk_cache::ChunkCacheDecodedLruChunkLimit;
9use zarrs::array::{data_type, ArraySubset, DataType};
10use zarrs::filesystem::FilesystemStore;
11use zarrs::storage::ReadableListableStorageTraits as ZReadStorageTraits;
12
13/// Decoded-chunk LRU capacity, per zarr array. Keeps cache memory low
14/// per backend so it scales when many backends are loaded simultaneously
15/// (e.g. multi-sample workloads). With ~1 MiB/chunk, default 4 chunks per
16/// array × 4 arrays = ~16 MiB upper bound per backend. Override with the
17/// `LEGUME_ZARR_CACHE_CAP` env var when total backend count × default
18/// would exceed the memory budget.
19const DEFAULT_CACHE_CHUNK_CAP: u64 = 4;
20
21fn cache_chunk_cap() -> u64 {
22    static CAP: OnceLock<u64> = OnceLock::new();
23    *CAP.get_or_init(|| {
24        std::env::var("LEGUME_ZARR_CACHE_CAP")
25            .ok()
26            .and_then(|s| s.parse().ok())
27            .unwrap_or(DEFAULT_CACHE_CHUNK_CAP)
28    })
29}
30
31const KEY_BY_COLUMN_DATA: &str = "/by_column/data";
32const KEY_BY_COLUMN_INDICES: &str = "/by_column/indices";
33const KEY_BY_ROW_DATA: &str = "/by_row/data";
34const KEY_BY_ROW_INDICES: &str = "/by_row/indices";
35
36use anyhow::anyhow;
37
38use crate::sparse_backend::shared;
39use crate::utilities::io_helpers::{chunk_elems, parse_name_file};
40
41const COMPRESSION_LEVEL: i32 = 5;
42
43/// Block size (in elements) for streaming the CSC value/row-index arrays when
44/// exporting to Matrix Market. ~1M elements ≈ 4 MiB f32 + 8 MiB u64 transient
45/// per block — large enough to amortize per-retrieve overhead, small enough to
46/// bound memory regardless of matrix size.
47const MTX_STREAM_BLOCK: u64 = 1 << 20;
48
49/// 10x-like cell-feature matrix with `zarr` backend (feature x cell)
50///
51/// ```text
52/// (root)
53///     ├── nrow
54///     ├── ncell
55///     ├── by_column
56///     │   ├── data
57///     │   ├── indices (row indices)
58///     │   └── indptr (column pointers)
59///     └── by_row
60///         ├── data
61///         ├── indices (column indices)
62///         └── indptr (row pointers)
63/// ```
64///
65#[derive(Clone)]
66pub struct SparseMtxData {
67    read_store: Arc<dyn ZReadStorageTraits>,
68    write_store: Option<Arc<FilesystemStore>>,
69    file_name: String,
70    max_row_name_idx: usize,
71    max_column_name_idx: usize,
72    by_column_indptr: Vec<u64>,
73    /// Streaming-write cursor: entries appended so far (see `note_streamed_nnz`).
74    streamed_nnz: u64,
75    by_row_indptr: Vec<u64>,
76    by_column_indices: Option<Vec<u64>>,
77    by_column_data: Option<Vec<f32>>,
78    by_row_indices: Option<Vec<u64>>,
79    by_row_data: Option<Vec<f32>>,
80    /// Budget share of the preloaded column arrays, given back with them.
81    column_preload: Option<crate::sparse_io::PreloadReservation>,
82    /// Budget share of the preloaded row arrays, given back with them.
83    row_preload: Option<crate::sparse_io::PreloadReservation>,
84    /// Persistent decoded-chunk LRU caches. `Arc<OnceLock<_>>` so that
85    /// `Clone`s share state, and the underlying `moka::sync::Cache` inside
86    /// `ChunkCacheDecodedLruChunkLimit` is internally thread-safe — no
87    /// external locking needed.
88    by_column_data_cache: Arc<OnceLock<ChunkCacheDecodedLruChunkLimit>>,
89    by_column_indices_cache: Arc<OnceLock<ChunkCacheDecodedLruChunkLimit>>,
90    by_row_data_cache: Arc<OnceLock<ChunkCacheDecodedLruChunkLimit>>,
91    by_row_indices_cache: Arc<OnceLock<ChunkCacheDecodedLruChunkLimit>>,
92}
93
94impl SparseMtxData {
95    /// Get the writable store, or error if this is a read-only backend (e.g. zip archive).
96    fn write_store(&self) -> anyhow::Result<&Arc<FilesystemStore>> {
97        self.write_store
98            .as_ref()
99            .ok_or_else(|| anyhow!("store is read-only (zip archive)"))
100    }
101}
102
103impl SparseMtxData {
104    /// Create an empty new `SparseMtxData` instance with a zarr
105    /// backend file If no `backend_file` is provided, a temporary
106    /// file will be created.
107    ///
108    /// * `backend_file` - Optional zarr backend file
109    pub fn new(zarr_file: Option<&str>) -> anyhow::Result<Self> {
110        Self::create_backend(zarr_file)
111    }
112
113    /// Helper to create a backend file (from provided path or temp file)
114    fn create_backend(zarr_file: Option<&str>) -> anyhow::Result<Self> {
115        match zarr_file {
116            Some(backend_file) => Self::register_backend_file(backend_file),
117            None => {
118                let backend_file = create_temp_dir_file(".zarr")?;
119                let backend_file = backend_file
120                    .to_str()
121                    .ok_or_else(|| anyhow::anyhow!("Failed to convert path to string"))?;
122                Self::register_backend_file(backend_file)
123            }
124        }
125    }
126
127    /// Create `SparseMtxData` instance from an existing zarr backend file
128    /// * `zarr_file` - zarr backend file (directory or `.zarr.zip`)
129    pub fn open(backend_file: &str) -> anyhow::Result<Self> {
130        let (read_store, write_store) = crate::zarr_io::open_zarr_store_rw(backend_file)?;
131
132        if (
133            Self::_num_rows(read_store.clone()),
134            Self::_num_columns(read_store.clone()),
135            Self::_num_nnz(read_store.clone()),
136        ) == (None, None, None)
137        {
138            anyhow::bail!("Couldn't figure out the size of this sparse matrix data");
139        }
140
141        let mut ret = Self {
142            read_store,
143            write_store,
144            file_name: backend_file.to_string(),
145            max_row_name_idx: MAX_ROW_NAME_IDX,
146            max_column_name_idx: MAX_COLUMN_NAME_IDX,
147            by_column_indptr: vec![],
148            streamed_nnz: 0,
149            by_row_indptr: vec![],
150            by_column_indices: None,
151            by_column_data: None,
152            by_row_indices: None,
153            by_row_data: None,
154            column_preload: None,
155            row_preload: None,
156            by_column_data_cache: Arc::new(OnceLock::new()),
157            by_column_indices_cache: Arc::new(OnceLock::new()),
158            by_row_data_cache: Arc::new(OnceLock::new()),
159            by_row_indices_cache: Arc::new(OnceLock::new()),
160        };
161
162        ret.read_column_indptr()?;
163        ret.read_row_indptr()?;
164
165        Ok(ret)
166    }
167
168    /// Create `SparseMtxData` from mtx file with `backend_file` as
169    /// the backend file.  If no `backend_file` is provided, it will
170    /// be the same as `mtx_file` with `.zarr` extension.
171    /// * `mtx_file`: mtx file to be read into zarr backend
172    /// * `backend_file`: zarr file to be associated with
173    /// * `index_by_row`: if true, the matrix will be indexed by row
174    pub fn from_mtx_file(
175        mtx_file: &str,
176        backend_file: Option<&str>,
177        index_by_row: Option<bool>,
178    ) -> anyhow::Result<Self> {
179        let zarr_file = backend_file
180            .map(|s| s.to_string())
181            .unwrap_or_else(|| format!("{}.zarr", mtx_file));
182
183        info!("backend file: {}", zarr_file);
184        let mut ret = Self::register_backend_file(&zarr_file)?;
185
186        ret.import_mtx_file(mtx_file, index_by_row == Some(true))?;
187
188        info!("created sparse backend from {}", mtx_file);
189        Ok(ret)
190    }
191
192    #[cfg(feature = "ndarray")]
193    /// Create a new `SparseMtxData` instance from an `ndarray` array
194    /// * `array` - 2D array to be added to the backend
195    /// * `backend_file` - Optional zarr backend file
196    /// * `index_by_row` - Optional flag to index by row (CSR format)
197    pub fn from_ndarray(
198        array: &Array2<f32>,
199        zarr_file: Option<&str>,
200        index_by_row: Option<bool>,
201    ) -> anyhow::Result<Self> {
202        let mut ret = Self::create_backend(zarr_file)?;
203
204        ret.import_ndarray_by_col(array)?;
205        ret.read_column_indptr()?;
206
207        if index_by_row == Some(true) {
208            ret.import_ndarray_by_row(array)?;
209            ret.read_row_indptr()?;
210        }
211        Ok(ret)
212    }
213
214    /// Create a new `SparseMtxData` instance from an `DMatrix` array
215    /// * `array` - 2D array to be added to the backend
216    /// * `backend_file` - Optional zarr backend file
217    /// * `index_by_row` - Optional flag to index by row (CSR format)
218    pub fn from_dmatrix(
219        matrix: &DMatrix<f32>,
220        zarr_file: Option<&str>,
221        index_by_row: Option<bool>,
222    ) -> anyhow::Result<Self> {
223        let mut ret = Self::create_backend(zarr_file)?;
224
225        ret.import_dmatrix_by_col(matrix)?;
226        ret.read_column_indptr()?;
227
228        if index_by_row == Some(true) {
229            ret.import_dmatrix_by_row(matrix)?;
230            ret.read_row_indptr()?;
231        }
232        Ok(ret)
233    }
234
235    /// Show the hierarchy of the zarr store
236    pub fn print_hierarchy(&self) -> anyhow::Result<()> {
237        use zarrs::config::MetadataRetrieveVersion;
238        let node =
239            zarrs::node::Node::open_opt(&self.read_store, "/", &MetadataRetrieveVersion::Default)?;
240        let tree = node.hierarchy_tree();
241        info!("hierarchy_tree:\n{}", tree);
242        Ok(())
243    }
244
245    /// Helper function to create a new zarr backend file
246    fn register_backend_file(zarr_file: &str) -> anyhow::Result<Self> {
247        use zarrs::group::GroupBuilder;
248        let store = Arc::new(FilesystemStore::new(zarr_file)?);
249        let root = GroupBuilder::new().build(store.clone(), "/")?;
250        root.store_metadata()?;
251
252        Ok(Self {
253            read_store: store.clone(),
254            write_store: Some(store),
255            file_name: zarr_file.to_string(),
256            max_row_name_idx: MAX_ROW_NAME_IDX,
257            max_column_name_idx: MAX_COLUMN_NAME_IDX,
258            by_column_indptr: vec![],
259            streamed_nnz: 0,
260            by_row_indptr: vec![],
261            by_column_indices: None,
262            by_column_data: None,
263            by_row_indices: None,
264            by_row_data: None,
265            column_preload: None,
266            row_preload: None,
267            by_column_data_cache: Arc::new(OnceLock::new()),
268            by_column_indices_cache: Arc::new(OnceLock::new()),
269            by_row_data_cache: Arc::new(OnceLock::new()),
270            by_row_indices_cache: Arc::new(OnceLock::new()),
271        })
272    }
273
274    /////////////////////
275    // backend related //
276    /////////////////////
277
278    /// Helper function to create a filled 1D array with the given
279    /// data type and fill value. This is the most useful function to
280    /// create a vector like data.
281    ///
282    /// * `key` - the key name
283    /// * `dt` - the data type among `DataType`
284    /// * `vec` - the vector to be stored
285    ///
286    fn new_filled_vector<V>(&mut self, key: &str, dt: DataType, vec: &[V]) -> anyhow::Result<()>
287    where
288        V: zarrs::array::Element,
289    {
290        use zarrs::array::codec::ZstdCodec;
291        use zarrs::array::ArrayBuilder;
292        use zarrs::array::FillValue;
293
294        let ws = self.write_store()?;
295
296        let nelem = vec.len();
297        let chunk_size = chunk_elems(nelem, std::mem::size_of::<V>());
298
299        let fill = if dt == data_type::float32() {
300            FillValue::from(zarrs::array::ZARR_NAN_F32)
301        } else if dt == data_type::uint64() {
302            FillValue::from(0u64)
303        } else if dt == data_type::string() {
304            FillValue::from("")
305        } else {
306            FillValue::from(0)
307        };
308
309        let array = ArrayBuilder::new(
310            vec![vec.len() as u64],  // array shape
311            vec![chunk_size as u64], // chunk shape
312            dt,                      // data type
313            fill,                    //
314        )
315        .bytes_to_bytes_codecs(vec![Arc::new(ZstdCodec::new(COMPRESSION_LEVEL, false))])
316        .build(ws.clone(), key)?;
317
318        array.store_metadata()?;
319
320        let subset = Self::create_subset(0..vec.len() as u64);
321        array.store_array_subset(&subset, vec)?;
322
323        Ok(())
324    }
325
326    fn _open_vector(
327        &self,
328        key: &str,
329    ) -> anyhow::Result<zarrs::array::Array<dyn ZReadStorageTraits>> {
330        use zarrs::array::Array as ZArray;
331        let ret = ZArray::open(self.read_store.clone(), key)?;
332        Ok(ret)
333    }
334
335    /// Create an empty fixed-shape 1-D array with the given data type,
336    /// chunk layout, and fill value. No data is written.
337    ///
338    /// Used by the streaming write path to pre-create `/by_column/*` and
339    /// `/by_row/*` arrays at their final size before any triplets land.
340    fn create_shaped_vector(
341        &mut self,
342        key: &str,
343        dt: DataType,
344        elem_bytes: usize,
345        nelem: usize,
346    ) -> anyhow::Result<()> {
347        use zarrs::array::codec::ZstdCodec;
348        use zarrs::array::ArrayBuilder;
349        use zarrs::array::FillValue;
350
351        let ws = self.write_store()?;
352
353        let chunk_size = chunk_elems(nelem, elem_bytes);
354
355        let fill = if dt == data_type::float32() {
356            FillValue::from(zarrs::array::ZARR_NAN_F32)
357        } else if dt == data_type::uint64() {
358            FillValue::from(0u64)
359        } else {
360            FillValue::from(0)
361        };
362
363        let array = ArrayBuilder::new(
364            vec![nelem.max(1) as u64],
365            vec![chunk_size.max(1) as u64],
366            dt,
367            fill,
368        )
369        .bytes_to_bytes_codecs(vec![Arc::new(ZstdCodec::new(COMPRESSION_LEVEL, false))])
370        .build(ws.clone(), key)?;
371
372        array.store_metadata()?;
373        Ok(())
374    }
375
376    /// Open an existing array through the writable filesystem store.
377    /// `_open_vector` uses the read-only handle, so it can't be used
378    /// to stage streaming writes.
379    fn _open_writable_vector(
380        &self,
381        key: &str,
382    ) -> anyhow::Result<zarrs::array::Array<FilesystemStore>> {
383        use zarrs::array::Array as ZArray;
384        let ws = self.write_store()?.clone();
385        let ret = ZArray::open(ws, key)?;
386        Ok(ret)
387    }
388
389    /// Write a `u64` slab at the given offset. The target array must
390    /// already exist (see [`create_shaped_vector`]).
391    fn write_slab_u64(&mut self, key: &str, offset: u64, data: &[u64]) -> anyhow::Result<()> {
392        if data.is_empty() {
393            return Ok(());
394        }
395        let array = self._open_writable_vector(key)?;
396        let subset = Self::create_subset(offset..offset + data.len() as u64);
397        array.store_array_subset(&subset, data)?;
398        Ok(())
399    }
400
401    /// Write an `f32` slab at the given offset.
402    fn write_slab_f32(&mut self, key: &str, offset: u64, data: &[f32]) -> anyhow::Result<()> {
403        if data.is_empty() {
404            return Ok(());
405        }
406        let array = self._open_writable_vector(key)?;
407        let subset = Self::create_subset(offset..offset + data.len() as u64);
408        array.store_array_subset(&subset, data)?;
409        Ok(())
410    }
411
412    #[allow(clippy::type_complexity)]
413    fn open_csc_triplets(
414        &self,
415    ) -> anyhow::Result<(
416        zarrs::array::Array<dyn ZReadStorageTraits>,
417        zarrs::array::Array<dyn ZReadStorageTraits>,
418        zarrs::array::Array<dyn ZReadStorageTraits>,
419    )> {
420        Ok((
421            self._open_vector("/by_column/indptr")?,
422            self._open_vector("/by_column/data")?,
423            self._open_vector("/by_column/indices")?,
424        ))
425    }
426
427    /// Helper to create an ArraySubset from a range
428    #[inline]
429    fn create_subset(range: Range<u64>) -> ArraySubset {
430        ArraySubset::new_with_ranges(&[range])
431    }
432
433    /// Build a decoded-chunk LRU cache for one zarr array. The cache
434    /// short-circuits redundant zstd decompression when the same chunk
435    /// is touched by multiple non-mergeable subset reads, both within
436    /// a single batch and across repeated calls (e.g. minibatch loops
437    /// over the same backend).
438    fn open_chunk_cache(
439        read_store: &Arc<dyn ZReadStorageTraits>,
440        key: &str,
441    ) -> anyhow::Result<ChunkCacheDecodedLruChunkLimit> {
442        use zarrs::array::Array as ZArray;
443        use zarrs::storage::ReadableStorageTraits;
444
445        let storage_readable: Arc<dyn ReadableStorageTraits> = read_store.clone().readable();
446        let arr = ZArray::open(read_store.clone(), key)?;
447        let arr_arc = Arc::new(arr.with_storage(storage_readable));
448        Ok(ChunkCacheDecodedLruChunkLimit::new(
449            arr_arc,
450            cache_chunk_cap(),
451        ))
452    }
453
454    /// Lazily build the persistent decoded-chunk cache. Concurrent
455    /// first-callers may both open the array, but only one `set` wins;
456    /// the loser's cache is dropped and `get` returns the winner.
457    fn cache_for<'a>(
458        &'a self,
459        cell: &'a OnceLock<ChunkCacheDecodedLruChunkLimit>,
460        key: &str,
461    ) -> anyhow::Result<&'a ChunkCacheDecodedLruChunkLimit> {
462        if let Some(cache) = cell.get() {
463            return Ok(cache);
464        }
465        let _ = cell.set(Self::open_chunk_cache(&self.read_store, key)?);
466        Ok(cell.get().expect("OnceLock populated above"))
467    }
468
469    fn _retrieve_vector<V>(&self, key: &str) -> anyhow::Result<Vec<V>>
470    where
471        V: zarrs::array::ElementOwned,
472    {
473        let data = self._open_vector(key)?;
474        let ntot = data.shape()[0];
475        let subset = Self::create_subset(0..ntot);
476        Ok(data.retrieve_array_subset::<Vec<V>>(&subset)?)
477    }
478
479    /////////////////////////////
480    // purely helper functions //
481    /////////////////////////////
482
483    /// Helper function to set an attribute from a group named `group_name`
484    fn _set_group_attr<V>(
485        store: Arc<FilesystemStore>,
486        group_name: &str,
487        attr_name: &str,
488        value: &V,
489    ) -> anyhow::Result<()>
490    where
491        V: serde::Serialize,
492    {
493        use zarrs::group::Group;
494        let mut group = Group::open(store, group_name)?;
495
496        let new_value = serde_json::to_value(value)?;
497        group
498            .attributes_mut()
499            .insert((*attr_name).to_string(), new_value);
500        group.store_metadata()?;
501        Ok(())
502    }
503
504    /// Helper function to get an attribute from a group named `group_name`
505    fn _get_group_attr<V>(
506        store: Arc<dyn ZReadStorageTraits>,
507        group_name: &str,
508        attr_name: &str,
509    ) -> Option<V>
510    where
511        V: serde::de::DeserializeOwned,
512    {
513        zarrs::group::Group::open(store, group_name)
514            .ok()
515            .and_then(|grp| grp.attributes().get(attr_name).cloned())
516            .and_then(|attr| serde_json::from_value(attr).ok())
517    }
518
519    fn _num_nnz(store: Arc<dyn ZReadStorageTraits>) -> Option<usize> {
520        Self::_get_group_attr::<usize>(store, "/", "nnz")
521    }
522
523    fn _num_rows(store: Arc<dyn ZReadStorageTraits>) -> Option<usize> {
524        Self::_get_group_attr::<usize>(store, "/", "nrow")
525    }
526
527    fn _num_columns(store: Arc<dyn ZReadStorageTraits>) -> Option<usize> {
528        Self::_get_group_attr::<usize>(store, "/", "ncol")
529    }
530
531    /// Helper function to add a group in the writable store
532    fn _add_group(&mut self, group_name: &str) -> anyhow::Result<()> {
533        use zarrs::group::Group;
534        let ws = self.write_store()?;
535
536        if Group::open(ws.clone(), group_name).is_err() {
537            let new_group = zarrs::group::GroupBuilder::new().build(ws.clone(), group_name)?;
538            new_group.store_metadata()?;
539        }
540
541        Ok(())
542    }
543}
544
545impl SparseIo for SparseMtxData {
546    type IndexIter = Vec<usize>;
547
548    /// Read row index pointers
549    fn read_row_indptr(&mut self) -> anyhow::Result<()> {
550        use zarrs::array::Array as Zarray;
551        let key = "/by_row/indptr";
552        if let Ok(indptr) = Zarray::open(self.read_store.clone(), key) {
553            let indptr_vec = indptr.retrieve_array_subset::<Vec<u64>>(&indptr.subset_all())?;
554            self.by_row_indptr.clear();
555            self.by_row_indptr.extend(indptr_vec);
556        }
557        Ok(())
558    }
559
560    /// Read column index pointers
561    fn column_indptr(&self) -> &[u64] {
562        &self.by_column_indptr
563    }
564
565    fn row_indptr(&self) -> &[u64] {
566        &self.by_row_indptr
567    }
568
569    fn metadata(&self) -> Metadata {
570        Self::_get_group_attr::<Metadata>(self.read_store.clone(), "/", meta::ATTR)
571            .unwrap_or_default()
572    }
573
574    fn set_metadata(&mut self, values: &Metadata) -> anyhow::Result<()> {
575        let mut root = zarrs::group::Group::open(self.write_store()?.clone(), "/")?;
576        if values.is_empty() {
577            root.attributes_mut().remove(meta::ATTR);
578        } else {
579            root.attributes_mut()
580                .insert(meta::ATTR.to_string(), serde_json::to_value(values)?);
581        }
582        root.store_metadata()?;
583        Ok(())
584    }
585
586    fn reopen_backend(&mut self) -> anyhow::Result<()> {
587        // Path-addressed store: rebuild it, refresh the resident indptrs — and
588        // DROP the decoded-chunk LRU caches, which pin arrays of the store they
589        // were built on. Keeping them served pre-swap chunk contents against
590        // post-swap indptrs: a matrix that read back with row indices past its
591        // own nrow, from a file that was byte-for-byte correct on disk.
592        let store = Arc::new(FilesystemStore::new(&self.file_name)?);
593        self.read_store = store.clone();
594        self.write_store = Some(store);
595        self.by_column_data_cache = Arc::new(OnceLock::new());
596        self.by_column_indices_cache = Arc::new(OnceLock::new());
597        self.by_row_data_cache = Arc::new(OnceLock::new());
598        self.by_row_indices_cache = Arc::new(OnceLock::new());
599        self.streamed_nnz = 0;
600        self.read_column_indptr()?;
601        self.read_row_indptr()?;
602        Ok(())
603    }
604
605    fn note_streamed_nnz(&mut self, n: u64) {
606        self.streamed_nnz += n;
607    }
608
609    fn streamed_nnz(&self) -> u64 {
610        self.streamed_nnz
611    }
612
613    fn reset_streamed_nnz(&mut self) {
614        self.streamed_nnz = 0;
615    }
616
617    fn read_column_indptr(&mut self) -> anyhow::Result<()> {
618        use zarrs::array::Array as ZArray;
619        let key = "/by_column/indptr";
620        if let Ok(indptr) = ZArray::open(self.read_store.clone(), key) {
621            let indptr_vec = indptr.retrieve_array_subset::<Vec<u64>>(&indptr.subset_all())?;
622            self.by_column_indptr.clear();
623            self.by_column_indptr.extend(indptr_vec);
624        }
625        Ok(())
626    }
627
628    fn clean_preloaded_columns(&mut self) {
629        self.by_column_data = None;
630        self.by_column_indices = None;
631        self.column_preload = None;
632    }
633
634    /// preload columns' values and indices
635    fn preload_columns(&mut self) -> anyhow::Result<()> {
636        if self.by_column_data.is_some() && self.by_column_indices.is_some() {
637            return Ok(());
638        }
639        let reservation = match self.num_non_zeros() {
640            Some(nnz) => match crate::sparse_io::reserve_preload(nnz, "column") {
641                Some(r) => Some(r),
642                None => return Ok(()),
643            },
644            None => None,
645        };
646        use zarrs::array::Array as ZArray;
647
648        let key = "/by_column/data";
649        let data = ZArray::open(self.read_store.clone(), key)?;
650        let key = "/by_column/indices";
651        let indices = ZArray::open(self.read_store.clone(), key)?;
652
653        let data = data.retrieve_array_subset::<Vec<f32>>(&data.subset_all())?;
654        let indices = indices.retrieve_array_subset::<Vec<u64>>(&indices.subset_all())?;
655
656        index_audit::check_preload(
657            index_audit::Major::Column,
658            self.num_rows(),
659            self.num_columns(),
660            &self.by_column_indptr,
661            &indices,
662            data.len(),
663        )?;
664
665        self.by_column_indices = Some(indices);
666        self.by_column_data = Some(data);
667        self.column_preload = reservation;
668        Ok(())
669    }
670
671    fn clean_preloaded_rows(&mut self) {
672        self.by_row_data = None;
673        self.by_row_indices = None;
674        self.row_preload = None;
675    }
676
677    /// preload rows' values and indices
678    fn preload_rows(&mut self) -> anyhow::Result<()> {
679        if self.by_row_data.is_some() && self.by_row_indices.is_some() {
680            return Ok(());
681        }
682        let reservation = match self.num_non_zeros() {
683            Some(nnz) => match crate::sparse_io::reserve_preload(nnz, "row") {
684                Some(r) => Some(r),
685                None => return Ok(()),
686            },
687            None => None,
688        };
689        use zarrs::array::Array as ZArray;
690
691        let data = ZArray::open(self.read_store.clone(), KEY_BY_ROW_DATA)?;
692        let indices = ZArray::open(self.read_store.clone(), KEY_BY_ROW_INDICES)?;
693
694        let data = data.retrieve_array_subset::<Vec<f32>>(&data.subset_all())?;
695        let indices = indices.retrieve_array_subset::<Vec<u64>>(&indices.subset_all())?;
696
697        index_audit::check_preload(
698            index_audit::Major::Row,
699            self.num_rows(),
700            self.num_columns(),
701            &self.by_row_indptr,
702            &indices,
703            data.len(),
704        )?;
705
706        self.by_row_indices = Some(indices);
707        self.by_row_data = Some(data);
708        self.row_preload = reservation;
709        Ok(())
710    }
711
712    /// Helper function to keep the matrix shape
713    fn record_mtx_shape(&mut self, mtx_shape: Option<(usize, usize, usize)>) -> anyhow::Result<()> {
714        if let Some((nrow, ncol, nnz)) = mtx_shape {
715            let ws = self.write_store()?;
716            let read_store = self.read_store.clone();
717
718            let check_set_attr = |attr_name: &str, value: usize| -> anyhow::Result<()> {
719                let old_value = Self::_get_group_attr::<usize>(read_store.clone(), "/", attr_name);
720                let new_value = serde_json::to_value(value)?;
721
722                match old_value {
723                    Some(old_value) => {
724                        if old_value != new_value {
725                            return Err(anyhow!("{} mismatch", attr_name));
726                        }
727                    }
728                    _ => {
729                        Self::_set_group_attr(ws.clone(), "/", attr_name, &new_value)?;
730                    }
731                }
732                Ok(())
733            };
734
735            check_set_attr("nrow", nrow)?;
736            check_set_attr("ncol", ncol)?;
737            check_set_attr("nnz", nnz)?;
738        }
739        Ok(())
740    }
741
742    /// Helper function to create a new zarr backend file
743    fn initialize_backend(&mut self) -> anyhow::Result<()> {
744        use zarrs::group::GroupBuilder;
745
746        self.remove_backend_file()?;
747        let zarr_file = &self.file_name;
748        let store = Arc::new(FilesystemStore::new(zarr_file)?);
749        let root = GroupBuilder::new().build(store.clone(), "/")?;
750        root.store_metadata()?;
751
752        self.read_store = store.clone();
753        self.write_store = Some(store);
754        self.file_name = zarr_file.to_string();
755        self.max_column_name_idx = MAX_COLUMN_NAME_IDX;
756        self.max_row_name_idx = MAX_ROW_NAME_IDX;
757        self.by_column_indptr = vec![];
758        self.by_row_indptr = vec![];
759
760        Ok(())
761    }
762
763    /// Clean up the backend file
764    fn remove_backend_file(&self) -> anyhow::Result<()> {
765        let backend = std::path::Path::new(&self.file_name);
766        if backend.exists() {
767            if backend.is_file() {
768                std::fs::remove_file(backend)?;
769            } else {
770                std::fs::remove_dir_all(backend)?;
771            }
772        }
773        Ok(())
774    }
775
776    /// Access file name of the zarr backend
777    fn get_backend_file_name(&self) -> &str {
778        &self.file_name
779    }
780
781    fn backend_type(&self) -> SparseIoBackend {
782        SparseIoBackend::Zarr
783    }
784
785    /// Export the data to a mtx file. This will take time.
786    /// * `mtx_file`: mtx file to be written
787    fn to_mtx_file(&self, mtx_file: &str) -> anyhow::Result<()> {
788        if let (Some(ncol), Some(nrow), Some(nnz)) =
789            (self.num_columns(), self.num_rows(), self.num_non_zeros())
790        {
791            let (nrow, ncol, nnz) = (nrow, ncol, nnz);
792
793            let mut buf = open_buf_writer(mtx_file)?;
794            shared::write_mtx_header(&mut buf, nrow, ncol, nnz)?;
795
796            let (indptr, data, indices) = self.open_csc_triplets()?;
797            let indptr = indptr.retrieve_array_subset::<Vec<u64>>(&indptr.subset_all())?;
798            crate::sparse_io::index_audit::check_indptr("to_mtx_file", &indptr, ncol, nnz)?;
799
800            // Stream the CSC value/row-index arrays in large sequential blocks
801            // (instead of two retrieves per column) and walk `indptr` to map
802            // each stored nonzero back to its column. CSC values are laid out
803            // column-major in column order, so emitting them in storage order
804            // reproduces the same column-by-column output as a per-column scan;
805            // empty columns are skipped by advancing the column pointer.
806            let total_nnz = indptr[ncol];
807            let mut jj = 0usize; // column owning the running nnz position
808            let mut pos = 0u64;
809            while pos < total_nnz {
810                let end = (pos + MTX_STREAM_BLOCK).min(total_nnz);
811                let subset = Self::create_subset(pos..end);
812                let data_block = data.retrieve_array_subset::<Vec<f32>>(&subset)?;
813                let indices_block = indices.retrieve_array_subset::<Vec<u64>>(&subset)?;
814
815                for (k, (&val, &ii)) in data_block.iter().zip(&indices_block).enumerate() {
816                    let global = pos + k as u64;
817                    // advance to the column owning this nonzero (skips empties)
818                    while jj + 1 < indptr.len() && indptr[jj + 1] <= global {
819                        jj += 1;
820                    }
821                    // 1-based indices
822                    writeln!(buf, "{}\t{}\t{}", ii as usize + 1, jj + 1, val)?;
823                }
824                pos = end;
825            }
826            buf.flush()?;
827            Ok(())
828        } else {
829            Err(anyhow!("Unable to figure out the size of the backend data"))
830        }
831    }
832
833    /// Set row names for the matrix
834    /// * `row_name_file`: a file each line contains row name words
835    fn register_row_names_file(&mut self, row_name_file: &str) {
836        let _ = self.register_names_file(
837            "/row_names",
838            row_name_file,
839            0..self.max_row_name_idx,
840            ROW_SEP,
841        );
842    }
843
844    /// Set row names for the matrix
845    /// * `rows`: a vector of row names
846    fn register_row_names_vec(&mut self, rows: &[Box<str>]) {
847        let _ = self.register_names_vec("/row_names", rows);
848    }
849
850    /// Set column names for the matrix
851    /// * `column_name_file`: a file each line contains column name words
852    fn register_column_names_file(&mut self, column_name_file: &str) {
853        let _ = self.register_names_file(
854            "/column_names",
855            column_name_file,
856            0..self.max_column_name_idx,
857            COLUMN_SEP,
858        );
859    }
860
861    /// Set column names for the matrix
862    /// * `columns`: a vector of column names
863    fn register_column_names_vec(&mut self, columns: &[Box<str>]) {
864        let _ = self.register_names_vec("/column_names", columns);
865    }
866
867    /// Number of rows in the matrix
868    fn num_rows(&self) -> Option<usize> {
869        Self::_num_rows(self.read_store.clone())
870    }
871
872    /// Number of columns in the matrix
873    fn num_columns(&self) -> Option<usize> {
874        Self::_num_columns(self.read_store.clone())
875    }
876
877    /// Number of non-zero elements in the matrix
878    fn num_non_zeros(&self) -> Option<usize> {
879        Self::_num_nnz(self.read_store.clone())
880    }
881
882    /// Add arbitrary names (a vector of strings)
883    /// * `group_name`: group name
884    /// * `name_file`: a file each line contains name words
885    /// * `name_columns`: range of columns to be used for name
886    /// * `name_sep`: separator for name columns
887    fn register_names_file(
888        &mut self,
889        key: &str,
890        name_file: &str,
891        name_columns: Range<usize>,
892        name_sep: &str,
893    ) -> anyhow::Result<()> {
894        let names = parse_name_file(name_file, name_columns, name_sep)?;
895        self.new_filled_vector(key, data_type::string(), &names)?;
896        Ok(())
897    }
898
899    /// Add arbitrary names (a vector of strings)
900    /// * `group_name`: group name
901    /// * `names`: a file each line contains name words
902    fn register_names_vec(&mut self, key: &str, names: &[Box<str>]) -> anyhow::Result<()> {
903        let names_vec: Vec<String> = names.iter().map(|x| x.to_string()).collect();
904        self.new_filled_vector(key, data_type::string(), &names_vec)?;
905        Ok(())
906    }
907
908    fn row_names(&self) -> anyhow::Result<Vec<Box<str>>> {
909        self.retrieve_registered_names("/row_names")
910    }
911
912    fn column_names(&self) -> anyhow::Result<Vec<Box<str>>> {
913        self.retrieve_registered_names("/column_names")
914    }
915
916    /// Get back the registered names
917    /// * `key`: key for the registered names
918    fn retrieve_registered_names(&self, key: &str) -> anyhow::Result<Vec<Box<str>>> {
919        Ok(self
920            ._retrieve_vector::<String>(key)?
921            .into_iter()
922            .map(|s| s.into_boxed_str())
923            .collect())
924    }
925
926    /// Read columns within the range and return a vector of triplets (row, col, value)
927    /// * `col` : usize
928    ///
929    fn read_triplets_by_single_column(
930        &self,
931        j_data: usize,
932    ) -> anyhow::Result<(usize, usize, Vec<(u64, u64, f32)>)> {
933        use zarrs::array::Array as ZArray;
934
935        let indptr = &self.by_column_indptr;
936
937        let nrow = self
938            .num_rows()
939            .ok_or(anyhow!("can't figure out the number of rows"))?;
940        index_audit::check_requested_slots("column", [j_data], indptr.len())?;
941
942        if let (Some(data), Some(indices)) = (&self.by_column_data, &self.by_column_indices) {
943            let ncol_out = 1;
944            let jj = 0;
945
946            let mut ret = Vec::new();
947            index_audit::emit_slot(
948                "preloaded column",
949                j_data,
950                indptr,
951                indices,
952                data,
953                nrow,
954                &mut ret,
955                |ii, x| (ii, jj, x),
956            )?;
957
958            Ok((nrow, ncol_out, ret))
959        } else {
960            let key = "/by_column/data";
961            let data = ZArray::open(self.read_store.clone(), key)?;
962            let key = "/by_column/indices";
963            let indices = ZArray::open(self.read_store.clone(), key)?;
964
965            let ncol_out = 1;
966            let jj = 0;
967
968            // [start, end)
969            let start = indptr[j_data];
970            let end = indptr[j_data + 1];
971            anyhow::ensure!(
972                start <= end,
973                "column read: slot {j_data} points at [{start}, {end})"
974            );
975
976            let mut ret: Vec<(u64, u64, f32)> = Vec::with_capacity((end - start) as usize);
977
978            if start < end {
979                let subset = Self::create_subset(start..end);
980                let data_slice = data.retrieve_array_subset::<Vec<f32>>(&subset)?;
981                let indices_slice = indices.retrieve_array_subset::<Vec<u64>>(&subset)?;
982
983                for k in 0..(end - start) {
984                    let x_ij = data_slice[k as usize];
985                    let ii = indices_slice[k as usize];
986                    if ii >= nrow as u64 {
987                        return Err(index_audit::inner_out_of_range(
988                            "column read",
989                            (start + k) as usize,
990                            ii,
991                            nrow,
992                        ));
993                    }
994                    ret.push((ii, jj, x_ij));
995                }
996            }
997
998            Ok((nrow, ncol_out, ret))
999        }
1000    }
1001
1002    /// Read columns within the range and return dense `ndarray::Array2`
1003    /// * `columns` : range e.g., 0..3 -> [0, 1, 2] or vec![0, 1, 2]
1004    ///
1005    fn read_triplets_by_columns(
1006        &self,
1007        columns: Self::IndexIter,
1008    ) -> anyhow::Result<(usize, usize, Vec<(u64, u64, f32)>)> {
1009        let indptr = &self.by_column_indptr;
1010        let columns_vec = columns.into_iter().collect::<Vec<usize>>();
1011
1012        let nrow = self
1013            .num_rows()
1014            .ok_or(anyhow!("can't figure out the number of rows"))?;
1015
1016        let ncol = self
1017            .num_columns()
1018            .ok_or(anyhow!("can't figure out the number of columns"))?;
1019
1020        let ncol_out = columns_vec.len();
1021        index_audit::check_requested_slots(
1022            "column",
1023            columns_vec.iter().copied().filter(|&j| j < ncol),
1024            indptr.len(),
1025        )?;
1026
1027        if let (Some(data), Some(indices)) = (&self.by_column_data, &self.by_column_indices) {
1028            if let Some(&bad) = columns_vec.iter().find(|&&j| j >= ncol) {
1029                anyhow::bail!("read column {bad}: outside {ncol} columns");
1030            }
1031            let min_start = columns_vec
1032                .iter()
1033                .map(|&j_data| indptr[j_data])
1034                .min()
1035                .unwrap_or(0);
1036
1037            let max_end = columns_vec
1038                .iter()
1039                .map(|&j_data| indptr[j_data + 1])
1040                .max()
1041                .unwrap_or(0);
1042
1043            let mut ret: Vec<(u64, u64, f32)> =
1044                Vec::with_capacity(max_end.saturating_sub(min_start) as usize);
1045
1046            for (jj, &j_data) in columns_vec.iter().enumerate() {
1047                let jj = jj as u64;
1048                index_audit::emit_slot(
1049                    "preloaded column",
1050                    j_data,
1051                    indptr,
1052                    indices,
1053                    data,
1054                    nrow,
1055                    &mut ret,
1056                    |ii, x| (ii, jj, x),
1057                )?;
1058            }
1059
1060            Ok((nrow, ncol_out, ret))
1061        } else {
1062            // CSC: tag = output column, inner = row. Sort by indptr.start so
1063            // abutting/overlapping ranges fuse into one retrieve; across-chunk
1064            // redundancy for non-mergeable ranges is caught by the chunk cache.
1065            let mut tagged: Vec<(u64, u64, u64)> = columns_vec
1066                .iter()
1067                .enumerate()
1068                .filter_map(|(jj, &j_data)| {
1069                    if j_data >= ncol {
1070                        return None;
1071                    }
1072                    let start = indptr[j_data];
1073                    let end = indptr[j_data + 1];
1074                    (start < end).then_some((jj as u64, start, end))
1075                })
1076                .collect();
1077            tagged.sort_by_key(|&(_, start, _)| start);
1078
1079            let data_cache = self.cache_for(&self.by_column_data_cache, KEY_BY_COLUMN_DATA)?;
1080            let indices_cache =
1081                self.cache_for(&self.by_column_indices_cache, KEY_BY_COLUMN_INDICES)?;
1082
1083            let opts = zarrs::array::CodecOptions::default();
1084            let ret = shared::coalesce_and_emit(
1085                &tagged,
1086                nrow,
1087                |jj, ii, val| (ii, jj, val),
1088                |s, e| {
1089                    let subset = Self::create_subset(s..e);
1090                    let data_buf =
1091                        <_ as zarrs::array::chunk_cache::ChunkCache>::retrieve_array_subset::<
1092                            Vec<f32>,
1093                        >(data_cache, &subset, &opts)?;
1094                    let indices_buf =
1095                        <_ as zarrs::array::chunk_cache::ChunkCache>::retrieve_array_subset::<
1096                            Vec<u64>,
1097                        >(indices_cache, &subset, &opts)?;
1098                    Ok((data_buf, indices_buf))
1099                },
1100            )?;
1101            Ok((nrow, ncol_out, ret))
1102        }
1103    }
1104
1105    fn csc_column_arrays(&self) -> Option<(&[u64], &[u64], &[f32])> {
1106        match (
1107            self.by_column_data.as_ref(),
1108            self.by_column_indices.as_ref(),
1109        ) {
1110            (Some(data), Some(indices)) if !self.by_column_indptr.is_empty() => Some((
1111                self.by_column_indptr.as_slice(),
1112                indices.as_slice(),
1113                data.as_slice(),
1114            )),
1115            _ => None,
1116        }
1117    }
1118
1119    /// Read rows within the range and return a vector of triplets (row, col, value)
1120    /// * `rows` : range e.g., 0..3 -> [0, 1, 2] or vec![0, 1, 2]
1121    ///
1122    fn read_triplets_by_rows(
1123        &self,
1124        rows: Self::IndexIter,
1125    ) -> anyhow::Result<(usize, usize, Vec<(u64, u64, f32)>)> {
1126        let indptr = &self.by_row_indptr;
1127
1128        let rows_vec = rows.into_iter().collect::<Vec<_>>();
1129
1130        let (nrow, ncol) = match (self.num_rows(), self.num_columns()) {
1131            (Some(nrow), Some(ncol)) => (nrow, ncol),
1132            _ => return Err(anyhow!("Unable to figure out the size of the backend data")),
1133        };
1134        let nrow_out = rows_vec.len();
1135        index_audit::check_requested_slots(
1136            "row",
1137            rows_vec.iter().copied().filter(|&i| i < nrow),
1138            indptr.len(),
1139        )?;
1140
1141        if let (Some(data), Some(indices)) = (&self.by_row_data, &self.by_row_indices) {
1142            let mut nnz_total: usize = 0;
1143            let valid: Vec<(u64, usize)> = rows_vec
1144                .iter()
1145                .enumerate()
1146                .filter_map(|(ii, &i_data)| {
1147                    if i_data >= nrow {
1148                        return None;
1149                    }
1150                    nnz_total += indptr[i_data + 1].saturating_sub(indptr[i_data]) as usize;
1151                    Some((ii as u64, i_data))
1152                })
1153                .collect();
1154
1155            let mut ret: Vec<(u64, u64, f32)> = Vec::with_capacity(nnz_total);
1156            for (ii, i_data) in valid {
1157                index_audit::emit_slot(
1158                    "preloaded row",
1159                    i_data,
1160                    indptr,
1161                    indices,
1162                    data,
1163                    ncol,
1164                    &mut ret,
1165                    |jj, x| (ii, jj, x),
1166                )?;
1167            }
1168            return Ok((nrow_out, ncol, ret));
1169        }
1170
1171        // CSR: tag = output row, inner = column.
1172        let mut tagged: Vec<(u64, u64, u64)> = rows_vec
1173            .iter()
1174            .enumerate()
1175            .filter_map(|(ii, &i_data)| {
1176                if i_data >= nrow {
1177                    return None;
1178                }
1179                let start = indptr[i_data];
1180                let end = indptr[i_data + 1];
1181                (start < end).then_some((ii as u64, start, end))
1182            })
1183            .collect();
1184        tagged.sort_by_key(|&(_, start, _)| start);
1185
1186        let data_cache = self.cache_for(&self.by_row_data_cache, KEY_BY_ROW_DATA)?;
1187        let indices_cache = self.cache_for(&self.by_row_indices_cache, KEY_BY_ROW_INDICES)?;
1188
1189        let opts = zarrs::array::CodecOptions::default();
1190        let ret = shared::coalesce_and_emit(
1191            &tagged,
1192            ncol,
1193            |ii, jj, val| (ii, jj, val),
1194            |s, e| {
1195                let subset = Self::create_subset(s..e);
1196                let data_buf = <_ as zarrs::array::chunk_cache::ChunkCache>::retrieve_array_subset::<
1197                    Vec<f32>,
1198                >(data_cache, &subset, &opts)?;
1199                let indices_buf =
1200                    <_ as zarrs::array::chunk_cache::ChunkCache>::retrieve_array_subset::<Vec<u64>>(
1201                        indices_cache,
1202                        &subset,
1203                        &opts,
1204                    )?;
1205                Ok((data_buf, indices_buf))
1206            },
1207        )?;
1208        Ok((nrow_out, ncol, ret))
1209    }
1210    /// CSR data structure in Zarr backend
1211    ///
1212    /// ```text
1213    ///     └── by_row
1214    ///         ├── data
1215    ///         ├── indices (column indices)
1216    ///         └── isndptr (row pointers)
1217    /// ```
1218    fn record_csr_dataset_backend(
1219        &mut self,
1220        csr_cols: &[u64],
1221        csr_vals: &[f32],
1222        csr_rowptr: &[u64],
1223    ) -> anyhow::Result<()> {
1224        // open or create the group "/by_row"
1225        let key = "/by_row";
1226        self._add_group(key)?;
1227
1228        let key = "/by_row/data";
1229        self.new_filled_vector(key, data_type::float32(), csr_vals)?;
1230        let key = "/by_row/indices";
1231        self.new_filled_vector(key, data_type::uint64(), csr_cols)?;
1232        let key = "/by_row/indptr";
1233        self.new_filled_vector(key, data_type::uint64(), csr_rowptr)?;
1234
1235        Ok(())
1236    }
1237
1238    /// CSC data structure in Zarr backend
1239    ///
1240    /// ```text
1241    /// Helper function to record the CSC dataset
1242    ///     ├── by_column
1243    ///     │   ├── data
1244    ///     │   ├── indices (row indices)
1245    ///     │   └── indptr (column pointers)
1246    /// ```
1247    fn record_csc_dataset_backend(
1248        &mut self,
1249        csc_rows: &[u64],
1250        csc_vals: &[f32],
1251        csc_colptr: &[u64],
1252    ) -> anyhow::Result<()> {
1253        // open or create the group "/by_column"
1254        let key = "/by_column";
1255        self._add_group(key)?;
1256
1257        let key = "/by_column/data";
1258        self.new_filled_vector(key, data_type::float32(), csc_vals)?;
1259        let key = "/by_column/indices";
1260        self.new_filled_vector(key, data_type::uint64(), csc_rows)?;
1261        let key = "/by_column/indptr";
1262        self.new_filled_vector(key, data_type::uint64(), csc_colptr)?;
1263
1264        Ok(())
1265    }
1266
1267    fn cs_create(&mut self, key: CsKey, len: usize) -> anyhow::Result<()> {
1268        let (group, path, dt, elem_bytes) = match key {
1269            CsKey::CscData => (
1270                "/by_column",
1271                "/by_column/data",
1272                data_type::float32(),
1273                std::mem::size_of::<f32>(),
1274            ),
1275            CsKey::CscIndices => (
1276                "/by_column",
1277                "/by_column/indices",
1278                data_type::uint64(),
1279                std::mem::size_of::<u64>(),
1280            ),
1281            CsKey::CscIndptr => (
1282                "/by_column",
1283                "/by_column/indptr",
1284                data_type::uint64(),
1285                std::mem::size_of::<u64>(),
1286            ),
1287            CsKey::CsrData => (
1288                "/by_row",
1289                "/by_row/data",
1290                data_type::float32(),
1291                std::mem::size_of::<f32>(),
1292            ),
1293            CsKey::CsrIndices => (
1294                "/by_row",
1295                "/by_row/indices",
1296                data_type::uint64(),
1297                std::mem::size_of::<u64>(),
1298            ),
1299            CsKey::CsrIndptr => (
1300                "/by_row",
1301                "/by_row/indptr",
1302                data_type::uint64(),
1303                std::mem::size_of::<u64>(),
1304            ),
1305        };
1306        self._add_group(group)?;
1307        self.create_shaped_vector(path, dt, elem_bytes, len)
1308    }
1309
1310    fn cs_write_u64(&mut self, key: CsKey, offset: u64, data: &[u64]) -> anyhow::Result<()> {
1311        let path = match key {
1312            CsKey::CscIndices => "/by_column/indices",
1313            CsKey::CscIndptr => "/by_column/indptr",
1314            CsKey::CsrIndices => "/by_row/indices",
1315            CsKey::CsrIndptr => "/by_row/indptr",
1316            CsKey::CscData | CsKey::CsrData => {
1317                return Err(anyhow!("cs_write_u64 called on f32 slot {:?}", key));
1318            }
1319        };
1320        self.write_slab_u64(path, offset, data)
1321    }
1322
1323    fn cs_write_f32(&mut self, key: CsKey, offset: u64, data: &[f32]) -> anyhow::Result<()> {
1324        let path = match key {
1325            CsKey::CscData => "/by_column/data",
1326            CsKey::CsrData => "/by_row/data",
1327            _ => {
1328                return Err(anyhow!("cs_write_f32 called on u64 slot {:?}", key));
1329            }
1330        };
1331        self.write_slab_f32(path, offset, data)
1332    }
1333}