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 metadata(&self) -> Metadata {
566        Self::_get_group_attr::<Metadata>(self.read_store.clone(), "/", meta::ATTR)
567            .unwrap_or_default()
568    }
569
570    fn set_metadata(&mut self, values: &Metadata) -> anyhow::Result<()> {
571        let mut root = zarrs::group::Group::open(self.write_store()?.clone(), "/")?;
572        if values.is_empty() {
573            root.attributes_mut().remove(meta::ATTR);
574        } else {
575            root.attributes_mut()
576                .insert(meta::ATTR.to_string(), serde_json::to_value(values)?);
577        }
578        root.store_metadata()?;
579        Ok(())
580    }
581
582    fn reopen_backend(&mut self) -> anyhow::Result<()> {
583        // Path-addressed store: rebuild it, refresh the resident indptrs — and
584        // DROP the decoded-chunk LRU caches, which pin arrays of the store they
585        // were built on. Keeping them served pre-swap chunk contents against
586        // post-swap indptrs: a matrix that read back with row indices past its
587        // own nrow, from a file that was byte-for-byte correct on disk.
588        let store = Arc::new(FilesystemStore::new(&self.file_name)?);
589        self.read_store = store.clone();
590        self.write_store = Some(store);
591        self.by_column_data_cache = Arc::new(OnceLock::new());
592        self.by_column_indices_cache = Arc::new(OnceLock::new());
593        self.by_row_data_cache = Arc::new(OnceLock::new());
594        self.by_row_indices_cache = Arc::new(OnceLock::new());
595        self.streamed_nnz = 0;
596        self.read_column_indptr()?;
597        self.read_row_indptr()?;
598        Ok(())
599    }
600
601    fn note_streamed_nnz(&mut self, n: u64) {
602        self.streamed_nnz += n;
603    }
604
605    fn streamed_nnz(&self) -> u64 {
606        self.streamed_nnz
607    }
608
609    fn reset_streamed_nnz(&mut self) {
610        self.streamed_nnz = 0;
611    }
612
613    fn read_column_indptr(&mut self) -> anyhow::Result<()> {
614        use zarrs::array::Array as ZArray;
615        let key = "/by_column/indptr";
616        if let Ok(indptr) = ZArray::open(self.read_store.clone(), key) {
617            let indptr_vec = indptr.retrieve_array_subset::<Vec<u64>>(&indptr.subset_all())?;
618            self.by_column_indptr.clear();
619            self.by_column_indptr.extend(indptr_vec);
620        }
621        Ok(())
622    }
623
624    fn clean_preloaded_columns(&mut self) {
625        self.by_column_data = None;
626        self.by_column_indices = None;
627        self.column_preload = None;
628    }
629
630    /// preload columns' values and indices
631    fn preload_columns(&mut self) -> anyhow::Result<()> {
632        if self.by_column_data.is_some() && self.by_column_indices.is_some() {
633            return Ok(());
634        }
635        let reservation = match self.num_non_zeros() {
636            Some(nnz) => match crate::sparse_io::reserve_preload(nnz, "column") {
637                Some(r) => Some(r),
638                None => return Ok(()),
639            },
640            None => None,
641        };
642        use zarrs::array::Array as ZArray;
643
644        let key = "/by_column/data";
645        let data = ZArray::open(self.read_store.clone(), key)?;
646        let key = "/by_column/indices";
647        let indices = ZArray::open(self.read_store.clone(), key)?;
648
649        let data = data.retrieve_array_subset::<Vec<f32>>(&data.subset_all())?;
650        let indices = indices.retrieve_array_subset::<Vec<u64>>(&indices.subset_all())?;
651
652        self.by_column_indices = Some(indices);
653        self.by_column_data = Some(data);
654        self.column_preload = reservation;
655        Ok(())
656    }
657
658    fn clean_preloaded_rows(&mut self) {
659        self.by_row_data = None;
660        self.by_row_indices = None;
661        self.row_preload = None;
662    }
663
664    /// preload rows' values and indices
665    fn preload_rows(&mut self) -> anyhow::Result<()> {
666        if self.by_row_data.is_some() && self.by_row_indices.is_some() {
667            return Ok(());
668        }
669        let reservation = match self.num_non_zeros() {
670            Some(nnz) => match crate::sparse_io::reserve_preload(nnz, "row") {
671                Some(r) => Some(r),
672                None => return Ok(()),
673            },
674            None => None,
675        };
676        use zarrs::array::Array as ZArray;
677
678        let data = ZArray::open(self.read_store.clone(), KEY_BY_ROW_DATA)?;
679        let indices = ZArray::open(self.read_store.clone(), KEY_BY_ROW_INDICES)?;
680
681        let data = data.retrieve_array_subset::<Vec<f32>>(&data.subset_all())?;
682        let indices = indices.retrieve_array_subset::<Vec<u64>>(&indices.subset_all())?;
683
684        self.by_row_indices = Some(indices);
685        self.by_row_data = Some(data);
686        self.row_preload = reservation;
687        Ok(())
688    }
689
690    /// Helper function to keep the matrix shape
691    fn record_mtx_shape(&mut self, mtx_shape: Option<(usize, usize, usize)>) -> anyhow::Result<()> {
692        if let Some((nrow, ncol, nnz)) = mtx_shape {
693            let ws = self.write_store()?;
694            let read_store = self.read_store.clone();
695
696            let check_set_attr = |attr_name: &str, value: usize| -> anyhow::Result<()> {
697                let old_value = Self::_get_group_attr::<usize>(read_store.clone(), "/", attr_name);
698                let new_value = serde_json::to_value(value)?;
699
700                match old_value {
701                    Some(old_value) => {
702                        if old_value != new_value {
703                            return Err(anyhow!("{} mismatch", attr_name));
704                        }
705                    }
706                    _ => {
707                        Self::_set_group_attr(ws.clone(), "/", attr_name, &new_value)?;
708                    }
709                }
710                Ok(())
711            };
712
713            check_set_attr("nrow", nrow)?;
714            check_set_attr("ncol", ncol)?;
715            check_set_attr("nnz", nnz)?;
716        }
717        Ok(())
718    }
719
720    /// Helper function to create a new zarr backend file
721    fn initialize_backend(&mut self) -> anyhow::Result<()> {
722        use zarrs::group::GroupBuilder;
723
724        self.remove_backend_file()?;
725        let zarr_file = &self.file_name;
726        let store = Arc::new(FilesystemStore::new(zarr_file)?);
727        let root = GroupBuilder::new().build(store.clone(), "/")?;
728        root.store_metadata()?;
729
730        self.read_store = store.clone();
731        self.write_store = Some(store);
732        self.file_name = zarr_file.to_string();
733        self.max_column_name_idx = MAX_COLUMN_NAME_IDX;
734        self.max_row_name_idx = MAX_ROW_NAME_IDX;
735        self.by_column_indptr = vec![];
736        self.by_row_indptr = vec![];
737
738        Ok(())
739    }
740
741    /// Clean up the backend file
742    fn remove_backend_file(&self) -> anyhow::Result<()> {
743        let backend = std::path::Path::new(&self.file_name);
744        if backend.exists() {
745            if backend.is_file() {
746                std::fs::remove_file(backend)?;
747            } else {
748                std::fs::remove_dir_all(backend)?;
749            }
750        }
751        Ok(())
752    }
753
754    /// Access file name of the zarr backend
755    fn get_backend_file_name(&self) -> &str {
756        &self.file_name
757    }
758
759    fn backend_type(&self) -> SparseIoBackend {
760        SparseIoBackend::Zarr
761    }
762
763    /// Export the data to a mtx file. This will take time.
764    /// * `mtx_file`: mtx file to be written
765    fn to_mtx_file(&self, mtx_file: &str) -> anyhow::Result<()> {
766        if let (Some(ncol), Some(nrow), Some(nnz)) =
767            (self.num_columns(), self.num_rows(), self.num_non_zeros())
768        {
769            let (nrow, ncol, nnz) = (nrow, ncol, nnz);
770
771            let mut buf = open_buf_writer(mtx_file)?;
772            shared::write_mtx_header(&mut buf, nrow, ncol, nnz)?;
773
774            let (indptr, data, indices) = self.open_csc_triplets()?;
775            let indptr = indptr.retrieve_array_subset::<Vec<u64>>(&indptr.subset_all())?;
776            debug_assert!(indptr.len() == ncol + 1);
777
778            // Stream the CSC value/row-index arrays in large sequential blocks
779            // (instead of two retrieves per column) and walk `indptr` to map
780            // each stored nonzero back to its column. CSC values are laid out
781            // column-major in column order, so emitting them in storage order
782            // reproduces the same column-by-column output as a per-column scan;
783            // empty columns are skipped by advancing the column pointer.
784            let total_nnz = indptr[ncol];
785            let mut jj = 0usize; // column owning the running nnz position
786            let mut pos = 0u64;
787            while pos < total_nnz {
788                let end = (pos + MTX_STREAM_BLOCK).min(total_nnz);
789                let subset = Self::create_subset(pos..end);
790                let data_block = data.retrieve_array_subset::<Vec<f32>>(&subset)?;
791                let indices_block = indices.retrieve_array_subset::<Vec<u64>>(&subset)?;
792
793                for (k, (&val, &ii)) in data_block.iter().zip(&indices_block).enumerate() {
794                    let global = pos + k as u64;
795                    // advance to the column owning this nonzero (skips empties)
796                    while jj + 1 < indptr.len() && indptr[jj + 1] <= global {
797                        jj += 1;
798                    }
799                    // 1-based indices
800                    writeln!(buf, "{}\t{}\t{}", ii as usize + 1, jj + 1, val)?;
801                }
802                pos = end;
803            }
804            buf.flush()?;
805            Ok(())
806        } else {
807            Err(anyhow!("Unable to figure out the size of the backend data"))
808        }
809    }
810
811    /// Set row names for the matrix
812    /// * `row_name_file`: a file each line contains row name words
813    fn register_row_names_file(&mut self, row_name_file: &str) {
814        let _ = self.register_names_file(
815            "/row_names",
816            row_name_file,
817            0..self.max_row_name_idx,
818            ROW_SEP,
819        );
820    }
821
822    /// Set row names for the matrix
823    /// * `rows`: a vector of row names
824    fn register_row_names_vec(&mut self, rows: &[Box<str>]) {
825        let _ = self.register_names_vec("/row_names", rows);
826    }
827
828    /// Set column names for the matrix
829    /// * `column_name_file`: a file each line contains column name words
830    fn register_column_names_file(&mut self, column_name_file: &str) {
831        let _ = self.register_names_file(
832            "/column_names",
833            column_name_file,
834            0..self.max_column_name_idx,
835            COLUMN_SEP,
836        );
837    }
838
839    /// Set column names for the matrix
840    /// * `columns`: a vector of column names
841    fn register_column_names_vec(&mut self, columns: &[Box<str>]) {
842        let _ = self.register_names_vec("/column_names", columns);
843    }
844
845    /// Number of rows in the matrix
846    fn num_rows(&self) -> Option<usize> {
847        Self::_num_rows(self.read_store.clone())
848    }
849
850    /// Number of columns in the matrix
851    fn num_columns(&self) -> Option<usize> {
852        Self::_num_columns(self.read_store.clone())
853    }
854
855    /// Number of non-zero elements in the matrix
856    fn num_non_zeros(&self) -> Option<usize> {
857        Self::_num_nnz(self.read_store.clone())
858    }
859
860    /// Add arbitrary names (a vector of strings)
861    /// * `group_name`: group name
862    /// * `name_file`: a file each line contains name words
863    /// * `name_columns`: range of columns to be used for name
864    /// * `name_sep`: separator for name columns
865    fn register_names_file(
866        &mut self,
867        key: &str,
868        name_file: &str,
869        name_columns: Range<usize>,
870        name_sep: &str,
871    ) -> anyhow::Result<()> {
872        let names = parse_name_file(name_file, name_columns, name_sep)?;
873        self.new_filled_vector(key, data_type::string(), &names)?;
874        Ok(())
875    }
876
877    /// Add arbitrary names (a vector of strings)
878    /// * `group_name`: group name
879    /// * `names`: a file each line contains name words
880    fn register_names_vec(&mut self, key: &str, names: &[Box<str>]) -> anyhow::Result<()> {
881        let names_vec: Vec<String> = names.iter().map(|x| x.to_string()).collect();
882        self.new_filled_vector(key, data_type::string(), &names_vec)?;
883        Ok(())
884    }
885
886    fn row_names(&self) -> anyhow::Result<Vec<Box<str>>> {
887        self.retrieve_registered_names("/row_names")
888    }
889
890    fn column_names(&self) -> anyhow::Result<Vec<Box<str>>> {
891        self.retrieve_registered_names("/column_names")
892    }
893
894    /// Get back the registered names
895    /// * `key`: key for the registered names
896    fn retrieve_registered_names(&self, key: &str) -> anyhow::Result<Vec<Box<str>>> {
897        Ok(self
898            ._retrieve_vector::<String>(key)?
899            .into_iter()
900            .map(|s| s.into_boxed_str())
901            .collect())
902    }
903
904    /// Read columns within the range and return a vector of triplets (row, col, value)
905    /// * `col` : usize
906    ///
907    fn read_triplets_by_single_column(
908        &self,
909        j_data: usize,
910    ) -> anyhow::Result<(usize, usize, Vec<(u64, u64, f32)>)> {
911        use zarrs::array::Array as ZArray;
912
913        debug_assert!(!self.by_column_indptr.is_empty()); // pre-loaded
914        debug_assert!(j_data < self.num_columns().unwrap_or(0)); //
915
916        let indptr = &self.by_column_indptr;
917
918        debug_assert!((j_data + 1) < indptr.len());
919        debug_assert!(indptr.len() > self.num_columns().unwrap_or(0));
920
921        let nrow = self
922            .num_rows()
923            .ok_or(anyhow!("can't figure out the number of rows"))?;
924
925        if let (Some(data), Some(indices)) = (&self.by_column_data, &self.by_column_indices) {
926            let ncol_out = 1;
927            let jj = 0;
928
929            // [start, end)
930            let start = indptr[j_data] as usize;
931            let end = indptr[j_data + 1] as usize;
932            let ret: Vec<(u64, u64, f32)> = indices[start..end]
933                .iter()
934                .zip(data[start..end].iter())
935                .map(|(&ii, &x_ij)| (ii, jj, x_ij))
936                .collect();
937
938            Ok((nrow, ncol_out, ret))
939        } else {
940            let key = "/by_column/data";
941            let data = ZArray::open(self.read_store.clone(), key)?;
942            let key = "/by_column/indices";
943            let indices = ZArray::open(self.read_store.clone(), key)?;
944
945            let ncol_out = 1;
946            let jj = 0;
947
948            // [start, end)
949            let start = indptr[j_data];
950            let end = indptr[j_data + 1];
951
952            let mut ret: Vec<(u64, u64, f32)> = Vec::with_capacity((end - start) as usize);
953
954            if start < end {
955                let subset = Self::create_subset(start..end);
956                let data_slice = data.retrieve_array_subset::<Vec<f32>>(&subset)?;
957                let indices_slice = indices.retrieve_array_subset::<Vec<u64>>(&subset)?;
958
959                for k in 0..(end - start) {
960                    let x_ij = data_slice[k as usize];
961                    let ii = indices_slice[k as usize];
962                    debug_assert!((ii as usize) < nrow);
963                    ret.push((ii, jj, x_ij));
964                }
965            }
966
967            Ok((nrow, ncol_out, ret))
968        }
969    }
970
971    /// Read columns within the range and return dense `ndarray::Array2`
972    /// * `columns` : range e.g., 0..3 -> [0, 1, 2] or vec![0, 1, 2]
973    ///
974    fn read_triplets_by_columns(
975        &self,
976        columns: Self::IndexIter,
977    ) -> anyhow::Result<(usize, usize, Vec<(u64, u64, f32)>)> {
978        debug_assert!(!self.by_column_indptr.is_empty());
979        let indptr = &self.by_column_indptr;
980        let columns_vec = columns.into_iter().collect::<Vec<usize>>();
981
982        debug_assert!(indptr.len() > self.num_columns().unwrap_or(0));
983
984        let nrow = self
985            .num_rows()
986            .ok_or(anyhow!("can't figure out the number of rows"))?;
987
988        let ncol = self
989            .num_columns()
990            .ok_or(anyhow!("can't figure out the number of columns"))?;
991
992        let ncol_out = columns_vec.len();
993
994        if let (Some(data), Some(indices)) = (&self.by_column_data, &self.by_column_indices) {
995            let min_start = columns_vec
996                .iter()
997                .map(|&j_data| indptr[j_data])
998                .min()
999                .unwrap_or(0);
1000
1001            let max_end = columns_vec
1002                .iter()
1003                .map(|&j_data| indptr[j_data + 1])
1004                .max()
1005                .unwrap_or(0);
1006
1007            let mut ret: Vec<(u64, u64, f32)> = Vec::with_capacity((max_end - min_start) as usize);
1008
1009            for (jj, &j_data) in columns_vec.iter().enumerate() {
1010                let jj = jj as u64;
1011                let start = indptr[j_data] as usize;
1012                let end = indptr[j_data + 1] as usize;
1013                for (&ii, &x_ij) in indices[start..end].iter().zip(data[start..end].iter()) {
1014                    ret.push((ii, jj, x_ij));
1015                }
1016            }
1017
1018            Ok((nrow, ncol_out, ret))
1019        } else {
1020            // CSC: tag = output column, inner = row. Sort by indptr.start so
1021            // abutting/overlapping ranges fuse into one retrieve; across-chunk
1022            // redundancy for non-mergeable ranges is caught by the chunk cache.
1023            let mut tagged: Vec<(u64, u64, u64)> = columns_vec
1024                .iter()
1025                .enumerate()
1026                .filter_map(|(jj, &j_data)| {
1027                    if j_data >= ncol {
1028                        return None;
1029                    }
1030                    let start = indptr[j_data];
1031                    let end = indptr[j_data + 1];
1032                    (start < end).then_some((jj as u64, start, end))
1033                })
1034                .collect();
1035            tagged.sort_by_key(|&(_, start, _)| start);
1036
1037            let data_cache = self.cache_for(&self.by_column_data_cache, KEY_BY_COLUMN_DATA)?;
1038            let indices_cache =
1039                self.cache_for(&self.by_column_indices_cache, KEY_BY_COLUMN_INDICES)?;
1040
1041            let opts = zarrs::array::CodecOptions::default();
1042            let ret = shared::coalesce_and_emit(
1043                &tagged,
1044                nrow,
1045                |jj, ii, val| (ii, jj, val),
1046                |s, e| {
1047                    let subset = Self::create_subset(s..e);
1048                    let data_buf =
1049                        <_ as zarrs::array::chunk_cache::ChunkCache>::retrieve_array_subset::<
1050                            Vec<f32>,
1051                        >(data_cache, &subset, &opts)?;
1052                    let indices_buf =
1053                        <_ as zarrs::array::chunk_cache::ChunkCache>::retrieve_array_subset::<
1054                            Vec<u64>,
1055                        >(indices_cache, &subset, &opts)?;
1056                    Ok((data_buf, indices_buf))
1057                },
1058            )?;
1059            Ok((nrow, ncol_out, ret))
1060        }
1061    }
1062
1063    fn csc_column_arrays(&self) -> Option<(&[u64], &[u64], &[f32])> {
1064        match (
1065            self.by_column_data.as_ref(),
1066            self.by_column_indices.as_ref(),
1067        ) {
1068            (Some(data), Some(indices)) if !self.by_column_indptr.is_empty() => Some((
1069                self.by_column_indptr.as_slice(),
1070                indices.as_slice(),
1071                data.as_slice(),
1072            )),
1073            _ => None,
1074        }
1075    }
1076
1077    /// Read rows within the range and return a vector of triplets (row, col, value)
1078    /// * `rows` : range e.g., 0..3 -> [0, 1, 2] or vec![0, 1, 2]
1079    ///
1080    fn read_triplets_by_rows(
1081        &self,
1082        rows: Self::IndexIter,
1083    ) -> anyhow::Result<(usize, usize, Vec<(u64, u64, f32)>)> {
1084        debug_assert!(!self.by_row_indptr.is_empty());
1085        let indptr = &self.by_row_indptr;
1086        debug_assert!(indptr.len() > self.num_rows().unwrap_or(0));
1087
1088        let rows_vec = rows.into_iter().collect::<Vec<_>>();
1089
1090        let (nrow, ncol) = match (self.num_rows(), self.num_columns()) {
1091            (Some(nrow), Some(ncol)) => (nrow, ncol),
1092            _ => return Err(anyhow!("Unable to figure out the size of the backend data")),
1093        };
1094        let nrow_out = rows_vec.len();
1095
1096        if let (Some(data), Some(indices)) = (&self.by_row_data, &self.by_row_indices) {
1097            let mut nnz_total: usize = 0;
1098            let valid: Vec<(u64, usize)> = rows_vec
1099                .iter()
1100                .enumerate()
1101                .filter_map(|(ii, &i_data)| {
1102                    if i_data >= nrow {
1103                        return None;
1104                    }
1105                    nnz_total += (indptr[i_data + 1] - indptr[i_data]) as usize;
1106                    Some((ii as u64, i_data))
1107                })
1108                .collect();
1109
1110            let mut ret: Vec<(u64, u64, f32)> = Vec::with_capacity(nnz_total);
1111            for (ii, i_data) in valid {
1112                let start = indptr[i_data] as usize;
1113                let end = indptr[i_data + 1] as usize;
1114                for (&jj, &x_ij) in indices[start..end].iter().zip(data[start..end].iter()) {
1115                    ret.push((ii, jj, x_ij));
1116                }
1117            }
1118            return Ok((nrow_out, ncol, ret));
1119        }
1120
1121        // CSR: tag = output row, inner = column.
1122        let mut tagged: Vec<(u64, u64, u64)> = rows_vec
1123            .iter()
1124            .enumerate()
1125            .filter_map(|(ii, &i_data)| {
1126                if i_data >= nrow {
1127                    return None;
1128                }
1129                debug_assert!((i_data + 1) < indptr.len());
1130                let start = indptr[i_data];
1131                let end = indptr[i_data + 1];
1132                (start < end).then_some((ii as u64, start, end))
1133            })
1134            .collect();
1135        tagged.sort_by_key(|&(_, start, _)| start);
1136
1137        let data_cache = self.cache_for(&self.by_row_data_cache, KEY_BY_ROW_DATA)?;
1138        let indices_cache = self.cache_for(&self.by_row_indices_cache, KEY_BY_ROW_INDICES)?;
1139
1140        let opts = zarrs::array::CodecOptions::default();
1141        let ret = shared::coalesce_and_emit(
1142            &tagged,
1143            ncol,
1144            |ii, jj, val| (ii, jj, val),
1145            |s, e| {
1146                let subset = Self::create_subset(s..e);
1147                let data_buf = <_ as zarrs::array::chunk_cache::ChunkCache>::retrieve_array_subset::<
1148                    Vec<f32>,
1149                >(data_cache, &subset, &opts)?;
1150                let indices_buf =
1151                    <_ as zarrs::array::chunk_cache::ChunkCache>::retrieve_array_subset::<Vec<u64>>(
1152                        indices_cache,
1153                        &subset,
1154                        &opts,
1155                    )?;
1156                Ok((data_buf, indices_buf))
1157            },
1158        )?;
1159        Ok((nrow_out, ncol, ret))
1160    }
1161    /// CSR data structure in Zarr backend
1162    ///
1163    /// ```text
1164    ///     └── by_row
1165    ///         ├── data
1166    ///         ├── indices (column indices)
1167    ///         └── isndptr (row pointers)
1168    /// ```
1169    fn record_csr_dataset_backend(
1170        &mut self,
1171        csr_cols: &[u64],
1172        csr_vals: &[f32],
1173        csr_rowptr: &[u64],
1174    ) -> anyhow::Result<()> {
1175        // open or create the group "/by_row"
1176        let key = "/by_row";
1177        self._add_group(key)?;
1178
1179        let key = "/by_row/data";
1180        self.new_filled_vector(key, data_type::float32(), csr_vals)?;
1181        let key = "/by_row/indices";
1182        self.new_filled_vector(key, data_type::uint64(), csr_cols)?;
1183        let key = "/by_row/indptr";
1184        self.new_filled_vector(key, data_type::uint64(), csr_rowptr)?;
1185
1186        Ok(())
1187    }
1188
1189    /// CSC data structure in Zarr backend
1190    ///
1191    /// ```text
1192    /// Helper function to record the CSC dataset
1193    ///     ├── by_column
1194    ///     │   ├── data
1195    ///     │   ├── indices (row indices)
1196    ///     │   └── indptr (column pointers)
1197    /// ```
1198    fn record_csc_dataset_backend(
1199        &mut self,
1200        csc_rows: &[u64],
1201        csc_vals: &[f32],
1202        csc_colptr: &[u64],
1203    ) -> anyhow::Result<()> {
1204        // open or create the group "/by_column"
1205        let key = "/by_column";
1206        self._add_group(key)?;
1207
1208        let key = "/by_column/data";
1209        self.new_filled_vector(key, data_type::float32(), csc_vals)?;
1210        let key = "/by_column/indices";
1211        self.new_filled_vector(key, data_type::uint64(), csc_rows)?;
1212        let key = "/by_column/indptr";
1213        self.new_filled_vector(key, data_type::uint64(), csc_colptr)?;
1214
1215        Ok(())
1216    }
1217
1218    fn cs_create(&mut self, key: CsKey, len: usize) -> anyhow::Result<()> {
1219        let (group, path, dt, elem_bytes) = match key {
1220            CsKey::CscData => (
1221                "/by_column",
1222                "/by_column/data",
1223                data_type::float32(),
1224                std::mem::size_of::<f32>(),
1225            ),
1226            CsKey::CscIndices => (
1227                "/by_column",
1228                "/by_column/indices",
1229                data_type::uint64(),
1230                std::mem::size_of::<u64>(),
1231            ),
1232            CsKey::CscIndptr => (
1233                "/by_column",
1234                "/by_column/indptr",
1235                data_type::uint64(),
1236                std::mem::size_of::<u64>(),
1237            ),
1238            CsKey::CsrData => (
1239                "/by_row",
1240                "/by_row/data",
1241                data_type::float32(),
1242                std::mem::size_of::<f32>(),
1243            ),
1244            CsKey::CsrIndices => (
1245                "/by_row",
1246                "/by_row/indices",
1247                data_type::uint64(),
1248                std::mem::size_of::<u64>(),
1249            ),
1250            CsKey::CsrIndptr => (
1251                "/by_row",
1252                "/by_row/indptr",
1253                data_type::uint64(),
1254                std::mem::size_of::<u64>(),
1255            ),
1256        };
1257        self._add_group(group)?;
1258        self.create_shaped_vector(path, dt, elem_bytes, len)
1259    }
1260
1261    fn cs_write_u64(&mut self, key: CsKey, offset: u64, data: &[u64]) -> anyhow::Result<()> {
1262        let path = match key {
1263            CsKey::CscIndices => "/by_column/indices",
1264            CsKey::CscIndptr => "/by_column/indptr",
1265            CsKey::CsrIndices => "/by_row/indices",
1266            CsKey::CsrIndptr => "/by_row/indptr",
1267            CsKey::CscData | CsKey::CsrData => {
1268                return Err(anyhow!("cs_write_u64 called on f32 slot {:?}", key));
1269            }
1270        };
1271        self.write_slab_u64(path, offset, data)
1272    }
1273
1274    fn cs_write_f32(&mut self, key: CsKey, offset: u64, data: &[f32]) -> anyhow::Result<()> {
1275        let path = match key {
1276            CsKey::CscData => "/by_column/data",
1277            CsKey::CsrData => "/by_row/data",
1278            _ => {
1279                return Err(anyhow!("cs_write_f32 called on u64 slot {:?}", key));
1280            }
1281        };
1282        self.write_slab_f32(path, offset, data)
1283    }
1284}