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