Skip to main content

data_beans/sparse_io_vector/
push.rs

1#![allow(dead_code)]
2
3use super::*;
4
5impl SparseIoVec {
6    /// Add a backend's columns to the vector.
7    ///
8    /// * `data`: `Arc` to the backend [`SparseData`].
9    /// * `data_name`: under [`ColumnAlignment::Disjoint`], appended as the
10    ///   `@<data_name>` display disambiguator for barcodes shared across
11    ///   files. **Ignored under [`ColumnAlignment::Union`]**, where cells
12    ///   glue by raw barcode — use [`push_with_barcode_suffix`] to attach a
13    ///   per-cell sample tag that participates in the Union merge.
14    ///
15    /// [`push_with_barcode_suffix`]: Self::push_with_barcode_suffix
16    pub fn push(
17        &mut self,
18        data: Arc<SparseData>,
19        data_name: Option<Box<str>>,
20    ) -> anyhow::Result<()> {
21        self.push_with_barcode_suffix(data, data_name, None)
22    }
23
24    /// Like [`push`](Self::push) but, under [`ColumnAlignment::Union`], tags
25    /// every barcode of this backend with `{COLUMN_SEP}{barcode_suffix}`
26    /// **before** the canonical-merge step. Two backends that share a barcode
27    /// merge into one global cell only if they carry the SAME suffix, so
28    /// callers can encode per-file sample identity (e.g. `rep1_wt`):
29    /// same-sample modalities merge, different samples stay distinct. The
30    /// tagged name also becomes the displayed column name. `None` reproduces
31    /// [`push`](Self::push) exactly. The `data_name` Disjoint disambiguator is
32    /// orthogonal: it still applies under `Disjoint`, and `barcode_suffix`
33    /// only under `Union` (the two alignments are mutually exclusive).
34    pub fn push_with_barcode_suffix(
35        &mut self,
36        data: Arc<SparseData>,
37        data_name: Option<Box<str>>,
38        barcode_suffix: Option<&str>,
39    ) -> anyhow::Result<()> {
40        anyhow::ensure!(
41            self.column_scales.is_none(),
42            "push after set_column_scales: the new columns would have no scale"
43        );
44        let Some(ncol_data) = data.num_columns() else {
45            return Err(anyhow::anyhow!("data file has no columns"));
46        };
47        debug_assert_eq!(self.col_to_data.len(), self.offset);
48        let didx = self.data_vec.len();
49        let didx_u32: u32 = didx
50            .try_into()
51            .map_err(|_| anyhow::anyhow!("backend count overflows u32"))?;
52
53        // Everything that can fail runs before the first change to `self`, so
54        // a refused backend leaves the vector exactly as it was and the next
55        // push starts from a consistent state.
56        let raw_col_names = data.column_names()?;
57        debug_assert_eq!(raw_col_names.len(), ncol_data);
58        anyhow::ensure!(u32::try_from(ncol_data).is_ok(), "local col overflows u32");
59        // `Union` keys its global columns by `u32`.
60        anyhow::ensure!(
61            self.column_alignment == ColumnAlignment::Disjoint
62                || u32::try_from(self.offset + ncol_data).is_ok(),
63            "global col overflows u32"
64        );
65
66        // Per-backend modality namespacing: `/{suffix}` goes after
67        // canonicalization so the gene/locus rule still applies to the
68        // bare name and only the modality tag distinguishes the row.
69        let row_suffix = match self.per_backend_row_suffix.as_ref() {
70            Some(suffixes) => Some(suffixes.get(didx).ok_or_else(|| {
71                anyhow::anyhow!(
72                    "per_backend_row_suffix has {} entries but backend index is {}",
73                    suffixes.len(),
74                    didx,
75                )
76            })?),
77            None => None,
78        };
79        let row_keys: Vec<Box<str>> = data
80            .row_names()?
81            .iter()
82            .map(|row| {
83                let key: Box<str> = match self.row_canonicalizer.as_ref() {
84                    Some(canon) => canon(row),
85                    None => row.clone(),
86                };
87                match row_suffix {
88                    Some(suffix) => format!("{key}/{suffix}").into_boxed_str(),
89                    None => key,
90                }
91            })
92            .collect();
93
94        // `Union` only: each column's (merge key, displayed name), refused
95        // when two columns of this backend share a key — that would put two
96        // entries from the same backend on one global col.
97        let union_keys: Vec<(Box<str>, Box<str>)> = match self.column_alignment {
98            ColumnAlignment::Disjoint => Vec::new(),
99            ColumnAlignment::Union => {
100                let mut seen_in_this_push: HashMap<Box<str>, usize> =
101                    HashMap::with_capacity_and_hasher(ncol_data, Default::default());
102                let mut keys = Vec::with_capacity(ncol_data);
103                for (loc, raw) in raw_col_names.iter().enumerate() {
104                    // Tag the barcode with the per-file suffix (sample id)
105                    // before canonicalization so the merge key — and the
106                    // displayed name — carry sample identity. Same suffix
107                    // across backends ⇒ merge; different ⇒ distinct cells.
108                    let tagged: Box<str> = match barcode_suffix {
109                        Some(s) => format!("{raw}{COLUMN_SEP}{s}").into_boxed_str(),
110                        None => raw.clone(),
111                    };
112                    let canon: Box<str> = match self.column_canonicalizer.as_ref() {
113                        Some(c) => c(&tagged),
114                        None => tagged.clone(),
115                    };
116                    if let Some(prev_loc) = seen_in_this_push.insert(canon.clone(), loc) {
117                        return Err(anyhow::anyhow!(
118                            "ColumnAlignment::Union: backend {} has duplicate canonical \
119                             barcode `{}` (local cols {} and {}) — Union cannot fold a \
120                             cell with itself within one backend",
121                            didx,
122                            canon,
123                            prev_loc,
124                            loc,
125                        ));
126                    }
127                    keys.push((canon, tagged));
128                }
129                keys
130            }
131        };
132
133        // `Disjoint`: every push appends fresh global columns + the
134        // `@<basename>` disambiguator. `Union`: match each pushed
135        // barcode against the existing global cell pool by canonical
136        // name so matched barcodes share a global column across
137        // backends (reads merge their nonzeros into one output col).
138        let data_to_cells_loc_to_glob: Vec<usize> = match self.column_alignment {
139            ColumnAlignment::Disjoint => {
140                let local_to_glob: Vec<usize> = (self.offset..self.offset + ncol_data).collect();
141                self.col_to_data
142                    .extend((0..ncol_data as u32).map(|local_col| {
143                        vec![BackendLocation {
144                            backend: didx_u32,
145                            local_col,
146                        }]
147                    }));
148                self.column_names_with_data_tag.extend(
149                    raw_col_names
150                        .iter()
151                        .map(|x| tag_column_name(x, data_name.as_deref())),
152                );
153                self.offset += ncol_data;
154                local_to_glob
155            }
156            ColumnAlignment::Union => {
157                let mut local_to_glob = Vec::with_capacity(ncol_data);
158                for (loc, (canon, tagged)) in union_keys.into_iter().enumerate() {
159                    let here = BackendLocation {
160                        backend: didx_u32,
161                        local_col: loc as u32,
162                    };
163                    let glob = match self.col_name_position.get(&canon).copied() {
164                        Some(g) => {
165                            self.col_to_data[g as usize].push(here);
166                            g as usize
167                        }
168                        None => {
169                            let new_g = self.offset;
170                            self.col_name_position.insert(canon, new_g as u32);
171                            self.col_to_data.push(vec![here]);
172                            // Tagged barcode (`raw` when no suffix) becomes
173                            // the displayed name; subsequent backends
174                            // contributing to this cell don't relabel it.
175                            self.column_names_with_data_tag.push(tagged);
176                            self.offset += 1;
177                            new_g
178                        }
179                    };
180                    local_to_glob.push(glob);
181                }
182                local_to_glob
183            }
184        };
185
186        // `data_to_cols[didx][loc] = glob`. Stored separately from
187        // `col_to_data` because callers (e.g. `rows_triplets`) need the
188        // forward map per backend.
189        self.data_to_cols.insert(didx, data_to_cells_loc_to_glob);
190
191        let mut local_to_global = Vec::with_capacity(row_keys.len());
192        for key in row_keys {
193            let glob_row = match self.row_name_position.get(&key) {
194                Some(&g) => g,
195                None => {
196                    let next_global = self.row_names_by_global.len();
197                    self.row_name_position.insert(key.clone(), next_global);
198                    self.row_names_by_global.push(key);
199                    self.row_count_by_global.push(0);
200                    next_global
201                }
202            };
203            local_to_global.push(glob_row);
204        }
205        // Count per-dataset *presence*, not per-local-row hits: when the
206        // row canonicalizer collapses several local rows in one file to
207        // the same global key, this dataset should still contribute
208        // exactly 1 to that global's count. Otherwise the
209        // `RowAlignment::Intersect` admit-rule (`count >= n_datasets`)
210        // silently excludes every collapsed row.
211        let mut counted: HashSet<usize> = HashSet::default();
212        for &g in &local_to_global {
213            if counted.insert(g) {
214                self.row_count_by_global[g] += 1;
215            }
216        }
217        // Flag canonicalizer-induced intra-file row merges so the read
218        // paths can sum duplicate (row, col) entries instead of emitting
219        // them twice (which breaks `from_nonzero_triplets` strictness).
220        let has_intra_merges = counted.len() != local_to_global.len();
221        self.data_has_intra_row_merges.push(has_intra_merges);
222        let mut g2l: Vec<(usize, usize)> = local_to_global
223            .iter()
224            .enumerate()
225            .map(|(l, &g)| (g, l))
226            .collect();
227        g2l.sort_unstable();
228        self.data_global_to_local_row.push(g2l);
229        self.data_local_to_global_row.push(local_to_global);
230
231        self.data_vec.push(data.clone());
232
233        self.cached_num_columns = self.offset;
234        self.recompute_row_mapping();
235
236        info!(
237            "Added {} columns ({} total); row {} = {}",
238            ncol_data,
239            self.offset,
240            match self.row_alignment {
241                RowAlignment::Intersect => "intersection",
242                RowAlignment::Union => "union",
243            },
244            self.cached_num_rows
245        );
246        Ok(())
247    }
248
249    /// Recompute the global → compact row mapping under the current
250    /// [`RowAlignment`] mode. Intersection keeps only rows present in
251    /// every backend; Union keeps every row that any backend contains.
252    /// Surviving rows get a compact index in raw-global order;
253    /// everything else maps to `None`.
254    fn recompute_row_mapping(&mut self) {
255        let n_datasets = self.data_local_to_global_row.len();
256        let n_global = self.row_names_by_global.len();
257        let min_count = match self.row_alignment {
258            RowAlignment::Intersect => n_datasets,
259            RowAlignment::Union => 1,
260        };
261
262        self.global_to_compact_row.clear();
263        self.global_to_compact_row.resize(n_global, None);
264        self.compact_to_global_row.clear();
265
266        let mut next_compact = 0usize;
267        for g in 0..n_global {
268            if self.row_count_by_global[g] >= min_count {
269                self.global_to_compact_row[g] = Some(next_compact);
270                self.compact_to_global_row.push(g);
271                next_compact += 1;
272            }
273        }
274        self.cached_num_rows = next_compact;
275    }
276
277    pub fn num_columns_by_data(&self) -> anyhow::Result<Vec<usize>> {
278        Ok(self
279            .data_vec
280            .iter()
281            .map(|d| d.num_columns().unwrap_or(0_usize))
282            .collect())
283    }
284
285    pub fn remove_backend_file(&mut self) -> anyhow::Result<()> {
286        for dat in self.data_vec.iter() {
287            dat.remove_backend_file()?;
288        }
289        Ok(())
290    }
291}