Skip to main content

data_beans/sparse_io_vector/
read.rs

1#![allow(dead_code)]
2
3use super::*;
4
5impl SparseIoVec {
6    ////////////////////
7    // access columns //
8    ////////////////////
9
10    /// Read a single column by global index and append offset triplets.
11    /// Under [`ColumnAlignment::Union`] one global column can be backed
12    /// by multiple `(didx, loc)` pairs — their triplets all land in the
13    /// same output column and `col_offset` advances by 1.
14    ///
15    /// `pub(super)` so the matched/neighbour read paths in `matched.rs` (a
16    /// sibling module) can reuse it.
17    pub(super) fn read_column_offset(
18        &self,
19        glob: usize,
20        col_offset: &mut usize,
21        triplets: &mut Vec<(u64, u64, f32)>,
22    ) -> anyhow::Result<()> {
23        let off = *col_offset as u64;
24        let g2c = self.global_to_compact_row.as_slice();
25        for source in self.col_to_data[glob].iter() {
26            let didx = source.backend as usize;
27            let loc = source.local_col as usize;
28            let (_, _, loc_triplets) = self.data_vec[didx].read_triplets_by_single_column(loc)?;
29            let l2g = self.data_local_to_global_row[didx].as_slice();
30            for (i, _j, v) in loc_triplets {
31                if let Some(c) = g2c[l2g[i as usize]] {
32                    triplets.push((c as u64, off, v));
33                }
34            }
35        }
36        *col_offset += 1;
37        Ok(())
38    }
39
40    /// Stream every nonzero of the selected `cells` (global column
41    /// indices) through `f(row, col, val)` **without** materializing the
42    /// full triplet vector. `row` is a compact-row index and `col` runs
43    /// over `0..ncol` in the iteration order of `cells` — exactly the
44    /// indices [`Self::columns_triplets`] would emit.
45    ///
46    /// Columns are processed in slabs of at most `chunk_cols`, so the
47    /// only transient allocation is one backend slab's worth of
48    /// `(u64, u64, f32)` triplets: peak memory is bounded by `chunk_cols`,
49    /// not by the total nnz. Callers can therefore build a compact edge
50    /// list (e.g. 12-byte triplets) directly and never pay for the wide
51    /// intermediate. Returns the `(nrow, ncol)` dimensions.
52    pub fn for_each_triplet<I, F>(
53        &self,
54        cells: I,
55        chunk_cols: usize,
56        mut f: F,
57    ) -> anyhow::Result<(usize, usize)>
58    where
59        I: Iterator<Item = usize>,
60        F: FnMut(u64, u64, f32),
61    {
62        let nrow = self.num_rows();
63        let cells: Vec<usize> = cells.collect();
64        let ncol = cells.len();
65        let chunk = chunk_cols.max(1);
66        let g2c = self.global_to_compact_row.as_slice();
67
68        for slab_start in (0..ncol).step_by(chunk) {
69            let slab_end = (slab_start + chunk).min(ncol);
70
71            // Group this slab's cells by backend, tracking (local_col,
72            // out_col) where out_col is the index into the *full* `cells`
73            // sequence. Under `ColumnAlignment::Union` one global cell can
74            // contribute entries to multiple backend groups (one per
75            // backend that observed it); all those reads target the same
76            // out_col.
77            let mut backend_groups: HashMap<usize, Vec<(usize, usize)>> = HashMap::default();
78            for (k, &glob) in cells[slab_start..slab_end].iter().enumerate() {
79                let out_col = slab_start + k;
80                for source in self.col_to_data[glob].iter() {
81                    backend_groups
82                        .entry(source.backend as usize)
83                        .or_default()
84                        .push((source.local_col as usize, out_col));
85                }
86            }
87
88            for (&didx, group) in &backend_groups {
89                let local_cols: Vec<usize> = group.iter().map(|&(loc, _)| loc).collect();
90                let (_, _, group_triplets) =
91                    self.data_vec[didx].read_triplets_by_columns(local_cols)?;
92
93                let l2g = self.data_local_to_global_row[didx].as_slice();
94                if self.data_has_intra_row_merges[didx] {
95                    // Canonicalizer collapsed >=2 local rows in this dataset
96                    // to the same global. Sum into a HashMap so downstream
97                    // consumers don't see duplicate (row, col) entries. Every
98                    // entry for a given out_col lives in this slab, so
99                    // per-slab accumulation is exact.
100                    let mut acc: HashMap<(u64, u64), f32> = HashMap::default();
101                    for (i, j, v) in group_triplets {
102                        if let Some(c) = g2c[l2g[i as usize]] {
103                            let out_col = group[j as usize].1 as u64;
104                            *acc.entry((c as u64, out_col)).or_insert(0.0) += v;
105                        }
106                    }
107                    for ((r, c), v) in acc {
108                        f(r, c, v);
109                    }
110                } else {
111                    for (i, j, v) in group_triplets {
112                        if let Some(c) = g2c[l2g[i as usize]] {
113                            let out_col = group[j as usize].1 as u64;
114                            f(c as u64, out_col, v);
115                        }
116                    }
117                }
118            }
119        }
120
121        Ok((nrow, ncol))
122    }
123
124    /// Collect all nonzeros of the selected `cells` into one triplet
125    /// vector. Thin wrapper over [`Self::for_each_triplet`] with a single
126    /// slab spanning every column (identical one-pass behavior); prefer
127    /// `for_each_triplet` when the result is consumed once, to avoid the
128    /// full-width intermediate.
129    #[allow(clippy::type_complexity)]
130    pub fn columns_triplets<I>(
131        &self,
132        cells: I,
133    ) -> anyhow::Result<((usize, usize), Vec<(u64, u64, f32)>)>
134    where
135        I: Iterator<Item = usize>,
136    {
137        let cells: Vec<usize> = cells.collect();
138        let one_slab = cells.len().max(1);
139        let mut triplets = Vec::new();
140        let dims = self.for_each_triplet(cells.into_iter(), one_slab, |r, c, v| {
141            triplets.push((r, c, v));
142        })?;
143        Ok((dims, triplets))
144    }
145
146    pub fn read_columns_ndarray<I>(&self, cells: I) -> anyhow::Result<ndarray::Array2<f32>>
147    where
148        I: Iterator<Item = usize>,
149    {
150        let ((nrow, ncol), triplets) = self.columns_triplets(cells)?;
151        ndarray::Array2::<f32>::from_nonzero_triplets(nrow, ncol, &triplets)
152    }
153
154    pub fn read_columns_dmatrix<I>(&self, cells: I) -> anyhow::Result<nalgebra::DMatrix<f32>>
155    where
156        I: Iterator<Item = usize>,
157    {
158        let ((nrow, ncol), triplets) = self.columns_triplets(cells)?;
159        DMatrix::<f32>::from_nonzero_triplets(nrow, ncol, &triplets)
160    }
161
162    /// Direct-slice CSC read: bypasses the `triplet → COO → CSC` roundtrip
163    /// when the underlying backends have preloaded column arrays.
164    ///
165    /// For each cell, we slice `(indices, values)` straight out of the
166    /// backend's preloaded `by_column_indices` / `by_column_data`, remap
167    /// row indices through `l2g` then `g2c` once, drop entries that fall
168    /// outside the row intersection, and assemble final CSC arrays in one
169    /// pass per column. Backends that aren't preloaded fall back to
170    /// per-column triplet reads, which still avoids the global triplet
171    /// vec and the column-major sort inside `CscMatrix::from(&coo)`.
172    pub fn read_columns_csc<I>(&self, cells: I) -> anyhow::Result<CscMatrix<f32>>
173    where
174        I: Iterator<Item = usize>,
175    {
176        let cells: Vec<usize> = cells.collect();
177        let nrow = self.num_rows();
178        let ncol = cells.len();
179
180        // Group cells by backend, carrying the output column index so we
181        // can scatter directly into the final per-column buckets. Under
182        // `ColumnAlignment::Union` one global cell can be observed by
183        // multiple backends — the loop visits every `(didx, loc)` for
184        // the same `out_col`, so the bucket accumulates contributions
185        // from all observing backends.
186        let mut backend_groups: Vec<Vec<(usize, usize)>> =
187            (0..self.data_vec.len()).map(|_| Vec::new()).collect();
188        for (out_col, &glob) in cells.iter().enumerate() {
189            for source in self.col_to_data[glob].iter() {
190                backend_groups[source.backend as usize].push((source.local_col as usize, out_col));
191            }
192        }
193
194        // Per-output-column buckets of (compact_row, value).
195        let mut buckets: Vec<Vec<(u32, f32)>> = (0..ncol).map(|_| Vec::new()).collect();
196        let g2c = self.global_to_compact_row.as_slice();
197
198        for (didx, group) in backend_groups.iter().enumerate() {
199            if group.is_empty() {
200                continue;
201            }
202            let l2g = self.data_local_to_global_row[didx].as_slice();
203
204            if let Some((indptr, indices, values)) = self.data_vec[didx].csc_column_arrays() {
205                // Fast path: zero-copy slicing into preloaded arrays.
206                for &(loc, out_col) in group {
207                    if loc + 1 >= indptr.len() {
208                        continue;
209                    }
210                    let s = indptr[loc] as usize;
211                    let e = indptr[loc + 1] as usize;
212                    let bucket = &mut buckets[out_col];
213                    bucket.reserve(e - s);
214                    for k in s..e {
215                        let local_row = indices[k] as usize;
216                        if let Some(c) = g2c[l2g[local_row]] {
217                            bucket.push((c as u32, values[k]));
218                        }
219                    }
220                }
221            } else {
222                // Cold path: route through `read_triplets_by_columns` so the
223                // backend can coalesce abutting indptr ranges into a single
224                // zarr/hdf5 retrieval (zarr uses `coalesce_and_emit` + chunk
225                // LRU cache). For a contiguous block of N cells this becomes
226                // ONE retrieval instead of N — the dominant win on cold reads
227                // from `.zarr.zip` over a slow disk.
228                let cols: Vec<usize> = group.iter().map(|&(loc, _)| loc).collect();
229                let (_, _, trip) = self.data_vec[didx].read_triplets_by_columns(cols)?;
230                for (i, jj, v) in trip {
231                    let (_, out_col) = group[jj as usize];
232                    if let Some(c) = g2c[l2g[i as usize]] {
233                        buckets[out_col].push((c as u32, v));
234                    }
235                }
236            }
237        }
238
239        // Assemble final CSC arrays in one pass.
240        let total_nnz: usize = buckets.iter().map(|b| b.len()).sum();
241        let mut col_offsets: Vec<usize> = Vec::with_capacity(ncol + 1);
242        let mut row_indices: Vec<usize> = Vec::with_capacity(total_nnz);
243        let mut values: Vec<f32> = Vec::with_capacity(total_nnz);
244        col_offsets.push(0);
245
246        for bucket in &mut buckets {
247            // Canonical CSC requires within-column row indices sorted
248            // ascending AND unique. On-disk indices are sorted by local
249            // row, but two sources can land on the same compact row:
250            //   (a) a row canonicalizer that maps two local rows in the
251            //       same backend to one global row;
252            //   (b) `ColumnAlignment::Union` where multiple backends
253            //       contribute to one output column.
254            // The composition `l2g[g2c[..]]` is monotonic in the
255            // single-source, no-canonicalizer case (the historical
256            // fast path) — detect it cheaply and skip the rebuild;
257            // otherwise sort and fold duplicates by summing.
258            let strictly_sorted_unique = bucket.windows(2).all(|w| w[0].0 < w[1].0);
259            if !strictly_sorted_unique {
260                bucket.sort_by_key(|&(r, _)| r);
261                // Compact duplicates in-place: sum values for equal rows.
262                let mut write = 0usize;
263                let mut read = 0usize;
264                while read < bucket.len() {
265                    let (r, mut v) = bucket[read];
266                    read += 1;
267                    while read < bucket.len() && bucket[read].0 == r {
268                        v += bucket[read].1;
269                        read += 1;
270                    }
271                    bucket[write] = (r, v);
272                    write += 1;
273                }
274                bucket.truncate(write);
275            }
276            for &(r, v) in bucket.iter() {
277                row_indices.push(r as usize);
278                values.push(v);
279            }
280            col_offsets.push(row_indices.len());
281        }
282
283        CscMatrix::try_from_csc_data(nrow, ncol, col_offsets, row_indices, values)
284            .map_err(|e| anyhow::anyhow!("CSC construction failed: {:?}", e))
285    }
286
287    pub fn read_columns_csr<I>(&self, cells: I) -> anyhow::Result<CsrMatrix<f32>>
288    where
289        I: Iterator<Item = usize>,
290    {
291        let ((nrow, ncol), triplets) = self.columns_triplets(cells)?;
292        nalgebra_sparse::CsrMatrix::<f32>::from_nonzero_triplets(nrow, ncol, &triplets)
293    }
294
295    pub fn read_columns_tensor<I>(&self, cells: I) -> anyhow::Result<Tensor>
296    where
297        I: Iterator<Item = usize>,
298    {
299        let ((nrow, ncol), triplets) = self.columns_triplets(cells)?;
300        Tensor::from_nonzero_triplets(nrow, ncol, &triplets)
301    }
302
303    /// Build (shape, triplets) for the requested compact rows across
304    /// all backends. Output column index is the SparseIoVec-global
305    /// column (concatenation of backends in push order); output row
306    /// index is the position in `rows`.
307    #[allow(clippy::type_complexity)]
308    pub fn rows_triplets<I>(
309        &self,
310        rows: I,
311    ) -> anyhow::Result<((usize, usize), Vec<(u64, u64, f32)>)>
312    where
313        I: Iterator<Item = usize>,
314    {
315        let rows_compact: Vec<usize> = rows.collect();
316        let nrow_out = rows_compact.len();
317        let ncol_out = self.num_columns();
318
319        let n_compact = self.cached_num_rows;
320        let compact_to_global = self.compact_to_global_row.as_slice();
321
322        let mut triplets: Vec<(u64, u64, f32)> = Vec::new();
323        let mut local_to_out: Vec<usize> = Vec::with_capacity(rows_compact.len());
324        for didx in 0..self.data_vec.len() {
325            let g2l = &self.data_global_to_local_row[didx];
326
327            local_to_out.clear();
328            let mut local_rows: Vec<usize> = Vec::with_capacity(rows_compact.len());
329            for (out_row, &c) in rows_compact.iter().enumerate() {
330                if c >= n_compact {
331                    continue;
332                }
333                let g = compact_to_global[c];
334                if let Some(&l) = g2l.get(&g) {
335                    local_rows.push(l);
336                    local_to_out.push(out_row);
337                }
338            }
339            if local_rows.is_empty() {
340                continue;
341            }
342
343            let (_, _, group_triplets) = self.data_vec[didx].read_triplets_by_rows(local_rows)?;
344            let cols_map = self
345                .data_to_cols
346                .get(&didx)
347                .ok_or_else(|| anyhow::anyhow!("missing data_to_cols entry for didx {}", didx))?;
348            triplets.reserve(group_triplets.len());
349            for (i, j, v) in group_triplets {
350                // `usize::MAX` marks a cell dropped by `mask_columns`.
351                let mapped = cols_map[j as usize];
352                if mapped == usize::MAX {
353                    continue;
354                }
355                let out_row = local_to_out[i as usize] as u64;
356                let out_col = mapped as u64;
357                triplets.push((out_row, out_col, v));
358            }
359        }
360
361        Ok(((nrow_out, ncol_out), triplets))
362    }
363
364    pub fn read_rows_ndarray<I>(&self, rows: I) -> anyhow::Result<ndarray::Array2<f32>>
365    where
366        I: Iterator<Item = usize>,
367    {
368        let ((nrow, ncol), triplets) = self.rows_triplets(rows)?;
369        ndarray::Array2::<f32>::from_nonzero_triplets(nrow, ncol, &triplets)
370    }
371
372    pub fn read_rows_dmatrix<I>(&self, rows: I) -> anyhow::Result<nalgebra::DMatrix<f32>>
373    where
374        I: Iterator<Item = usize>,
375    {
376        let ((nrow, ncol), triplets) = self.rows_triplets(rows)?;
377        DMatrix::<f32>::from_nonzero_triplets(nrow, ncol, &triplets)
378    }
379
380    pub fn read_rows_csc<I>(&self, rows: I) -> anyhow::Result<CscMatrix<f32>>
381    where
382        I: Iterator<Item = usize>,
383    {
384        let ((nrow, ncol), triplets) = self.rows_triplets(rows)?;
385        nalgebra_sparse::CscMatrix::<f32>::from_nonzero_triplets(nrow, ncol, &triplets)
386    }
387
388    pub fn read_rows_csr<I>(&self, rows: I) -> anyhow::Result<CsrMatrix<f32>>
389    where
390        I: Iterator<Item = usize>,
391    {
392        let ((nrow, ncol), triplets) = self.rows_triplets(rows)?;
393        nalgebra_sparse::CsrMatrix::<f32>::from_nonzero_triplets(nrow, ncol, &triplets)
394    }
395
396    pub fn read_rows_tensor<I>(&self, rows: I) -> anyhow::Result<Tensor>
397    where
398        I: Iterator<Item = usize>,
399    {
400        let ((nrow, ncol), triplets) = self.rows_triplets(rows)?;
401        Tensor::from_nonzero_triplets(nrow, ncol, &triplets)
402    }
403}