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