1use super::*;
9
10pub struct PbSampleLayout {
11 pub centroids: DMatrix<f32>,
13 pub cell_counts: Vec<f32>,
15 pub pb_sample_to_batch: Vec<usize>,
17 pub pb_sample_to_group: Vec<usize>,
19 pub bg_to_pbsamp: HashMap<(usize, usize), usize>,
21 pub cell_to_pbsamp: Vec<usize>,
23 pub singleton_col: Vec<Option<usize>>,
30 pub bulk_batches: Vec<usize>,
34 pub pb_sample_to_stratum: Option<Vec<usize>>,
37}
38
39impl PbSampleLayout {
40 #[must_use]
43 pub fn is_bulk(&self, pbsamp: usize) -> bool {
44 self.is_bulk_batch(self.pb_sample_to_batch[pbsamp])
45 }
46
47 #[must_use]
50 pub fn is_bulk_batch(&self, batch: usize) -> bool {
51 self.bulk_batches.contains(&batch)
52 }
53}
54
55pub struct PbSampleCollection {
58 pub layout: PbSampleLayout,
59 pub gene_sums: Vec<Vec<(usize, f32)>>,
61 pub num_genes: usize,
63}
64
65pub(super) struct BatchAccumulator {
67 centroid_sum: Vec<f32>,
68 gene_sum: HashMap<usize, f32>,
69 count: usize,
70}
71
72pub(super) struct PbSampleData {
74 centroid: Vec<f32>,
75 gene_sums: Vec<(usize, f32)>,
76 cell_count: f32,
77 batch: usize,
78 group: usize,
79}
80
81pub(super) fn build_pb_sample_layout(
98 group_to_cols: &[Vec<usize>],
99 col_to_batch: &[usize],
100 proj_kn: &DMatrix<f32>,
101 col_weight: Option<&[f32]>,
102 anchor_batches: &[usize],
103 bulk_batches: &[usize],
104 cell_to_stratum: Option<&[usize]>,
105) -> anyhow::Result<PbSampleLayout> {
106 let proj_dim = proj_kn.nrows();
107
108 struct CentroidAccum {
110 centroid_sum: Vec<f32>,
111 count: f32,
115 }
116 let weight_of = |c: usize| col_weight.map_or(1.0, |w| w[c]);
117
118 type CentroidTuple = (usize, usize, Vec<f32>, f32, Option<usize>);
122 let per_group_results: Vec<Vec<CentroidTuple>> = group_to_cols
123 .par_iter()
124 .enumerate()
125 .map(|(group, cells)| {
126 let mut batch_data: HashMap<usize, CentroidAccum> = HashMap::default();
127 let mut singletons: Vec<CentroidTuple> = Vec::new();
128
129 for &glob_idx in cells {
130 let batch = col_to_batch[glob_idx];
131 let w = weight_of(glob_idx);
132 if anchor_batches.contains(&batch) || bulk_batches.contains(&batch) {
133 let centroid: Vec<f32> =
134 (0..proj_dim).map(|d| proj_kn[(d, glob_idx)]).collect();
135 singletons.push((batch, group, centroid, w, Some(glob_idx)));
136 continue;
137 }
138 let acc = batch_data.entry(batch).or_insert_with(|| CentroidAccum {
139 centroid_sum: vec![0f32; proj_dim],
140 count: 0.0,
141 });
142 for d in 0..proj_dim {
143 acc.centroid_sum[d] += proj_kn[(d, glob_idx)] * w;
144 }
145 acc.count += w;
146 }
147
148 let mut out: Vec<CentroidTuple> = batch_data
149 .into_iter()
150 .filter(|(_, acc)| acc.count > 0.0)
151 .map(|(batch, acc)| {
152 let inv_count = 1.0 / acc.count;
153 let centroid: Vec<f32> =
154 acc.centroid_sum.iter().map(|v| v * inv_count).collect();
155 (batch, group, centroid, acc.count, None)
156 })
157 .collect::<Vec<_>>();
158 out.extend(singletons);
159 out
160 })
161 .collect();
162
163 let all_pbsamp: Vec<_> = per_group_results.into_iter().flatten().collect();
165 let num_pb = all_pbsamp.len();
166
167 if num_pb == 0 {
168 return Err(anyhow::anyhow!("no pb-samples built"));
169 }
170
171 let mut centroids = DMatrix::<f32>::zeros(proj_dim, num_pb);
173 let mut cell_counts = Vec::with_capacity(num_pb);
174 let mut pbsamp_to_batch = Vec::with_capacity(num_pb);
175 let mut pbsamp_to_group = Vec::with_capacity(num_pb);
176 let mut singleton_col = Vec::with_capacity(num_pb);
177 let mut bg_to_pbsamp = HashMap::default();
178
179 let ncols = col_to_batch.len();
180 let mut cell_to_pbsamp = vec![usize::MAX; ncols];
181 for (i, (batch, group, centroid, count, sc)) in all_pbsamp.into_iter().enumerate() {
182 for (d, &v) in centroid.iter().enumerate() {
183 centroids[(d, i)] = v;
184 }
185 cell_counts.push(count);
186 pbsamp_to_batch.push(batch);
187 pbsamp_to_group.push(group);
188 singleton_col.push(sc);
189 match sc {
190 Some(col) => cell_to_pbsamp[col] = i,
194 None => {
195 bg_to_pbsamp.insert((batch, group), i);
196 }
197 }
198 }
199
200 for (group, cells) in group_to_cols.iter().enumerate() {
203 for &c in cells {
204 let b = col_to_batch[c];
205 if cell_to_pbsamp[c] == usize::MAX {
206 if let Some(&pbsamp) = bg_to_pbsamp.get(&(b, group)) {
207 cell_to_pbsamp[c] = pbsamp;
208 }
209 }
210 }
211 }
212
213 let pb_sample_to_stratum = if let Some(strata) = cell_to_stratum {
214 anyhow::ensure!(
215 strata.len() == ncols,
216 "cell_to_stratum has {} entries, layout has {} columns",
217 strata.len(),
218 ncols
219 );
220 let mut out = vec![0usize; num_pb];
221 let mut seen = vec![false; num_pb];
222 for (c, &pbsamp) in cell_to_pbsamp.iter().enumerate() {
223 if pbsamp == usize::MAX {
224 continue;
225 }
226 if !seen[pbsamp] {
227 out[pbsamp] = strata[c];
228 seen[pbsamp] = true;
229 }
230 }
231 Some(out)
232 } else {
233 None
234 };
235
236 Ok(PbSampleLayout {
237 centroids,
238 cell_counts,
239 pb_sample_to_batch: pbsamp_to_batch,
240 pb_sample_to_group: pbsamp_to_group,
241 bg_to_pbsamp,
242 cell_to_pbsamp,
243 singleton_col,
244 bulk_batches: bulk_batches.to_vec(),
245 pb_sample_to_stratum,
246 })
247}
248
249pub(super) fn collect_pb_sample_gene_sums(
254 data_vec: &SparseIoVec,
255 group_to_cols: &[Vec<usize>],
256 cell_to_pbsamp: &[usize],
257 num_pb: usize,
258) -> anyhow::Result<Vec<Vec<(usize, f32)>>> {
259 use indicatif::ParallelProgressIterator;
260 let prog_bar = styled_progress_bar(group_to_cols.len() as u64, "groups (pb-sample gene sums)");
261 let gene_sum_maps: Vec<(usize, HashMap<usize, f32>)> = group_to_cols
262 .par_iter()
263 .progress_with(prog_bar.clone())
264 .flat_map(|cells| {
265 let yy = data_vec
266 .read_columns_csc(cells.iter().cloned())
267 .expect("read_columns_csc");
268
269 let mut per_pbsamp: HashMap<usize, HashMap<usize, f32>> = HashMap::default();
270
271 for (local_idx, y_j) in yy.col_iter().enumerate() {
272 let col = cells[local_idx];
273 let pbsamp = cell_to_pbsamp[col];
274 if pbsamp == usize::MAX {
275 continue;
276 }
277 let w = data_vec.column_multiplicity(col);
281 let gene_map = per_pbsamp.entry(pbsamp).or_default();
282 for (&gene, &val) in y_j.row_indices().iter().zip(y_j.values().iter()) {
283 *gene_map.entry(gene).or_default() += val * w;
284 }
285 }
286
287 per_pbsamp.into_iter().collect::<Vec<_>>()
288 })
289 .collect();
290
291 let mut gene_sums: Vec<Vec<(usize, f32)>> = vec![vec![]; num_pb];
292 for (pbsamp_idx, gene_map) in gene_sum_maps {
293 let mut sorted: Vec<(usize, f32)> = gene_map.into_iter().collect();
294 sorted.sort_unstable_by_key(|&(g, _)| g);
295 gene_sums[pbsamp_idx] = sorted;
296 }
297
298 Ok(gene_sums)
299}
300
301pub(super) fn build_pb_samples(
303 data_vec: &SparseIoVec,
304 proj_kn: &DMatrix<f32>,
305 num_genes: usize,
306 anchor_batches: &[usize],
307 bulk_batches: &[usize],
308 cell_to_stratum: Option<&[usize]>,
309) -> anyhow::Result<PbSampleCollection> {
310 let group_to_cols = data_vec
311 .take_grouped_columns()
312 .ok_or(anyhow::anyhow!("columns not assigned to groups"))?;
313 let col_to_batch: Vec<usize> = (0..proj_kn.ncols())
314 .map(|c| data_vec.get_batch_membership(std::iter::once(c))[0])
315 .collect();
316
317 let weights: Option<Vec<f32>> = data_vec.has_column_multiplicity().then(|| {
318 (0..proj_kn.ncols())
319 .map(|c| data_vec.column_multiplicity(c))
320 .collect()
321 });
322 let layout = build_pb_sample_layout(
323 group_to_cols,
324 &col_to_batch,
325 proj_kn,
326 weights.as_deref(),
327 anchor_batches,
328 bulk_batches,
329 cell_to_stratum,
330 )?;
331 let num_pb = layout.cell_counts.len();
332 let gene_sums =
333 collect_pb_sample_gene_sums(data_vec, group_to_cols, &layout.cell_to_pbsamp, num_pb)?;
334
335 Ok(PbSampleCollection {
336 layout,
337 gene_sums,
338 num_genes,
339 })
340}
341
342pub(crate) fn knn_distinct_pbsamples_in_batch(
354 bknn: &ColumnDict<usize>,
355 query: &[f32],
356 knn: usize,
357 cell_to_pbsamp: &[usize],
358 own_pbsamp: usize,
359 pb_sample_to_stratum: Option<&[usize]>,
360 own_stratum: Option<usize>,
361) -> anyhow::Result<Vec<(usize, f32)>> {
362 let n = bknn.num_points();
363 if n == 0 || knn == 0 {
364 return Ok(Vec::new());
365 }
366 let mut query_k = (knn * 4 + 1).min(n);
370 let mut best: HashMap<usize, f32> = HashMap::default();
371 loop {
372 let (cell_ids, dists) = bknn.search_by_query_data(query, query_k)?;
373 best.clear();
374 for (&c, &d) in cell_ids.iter().zip(dists.iter()) {
375 let other_pbsamp = cell_to_pbsamp[c];
376 if other_pbsamp == usize::MAX || other_pbsamp == own_pbsamp {
377 continue;
378 }
379 if let (Some(st), Some(own_s)) = (pb_sample_to_stratum, own_stratum) {
380 if st[other_pbsamp] != own_s {
381 continue;
382 }
383 }
384 best.entry(other_pbsamp)
385 .and_modify(|old| {
386 if d < *old {
387 *old = d;
388 }
389 })
390 .or_insert(d);
391 }
392 if best.len() >= knn || query_k >= n {
393 break;
394 }
395 query_k = query_k.saturating_mul(4).min(n);
396 }
397 let mut per_batch: Vec<(usize, f32)> = best.drain().collect();
398 per_batch.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
399 per_batch.truncate(knn);
400 Ok(per_batch)
401}
402
403pub(crate) fn bbknn_match_one_pbsamp(
410 layout: &PbSampleLayout,
411 batch_knn_lookup: &[ColumnDict<usize>],
412 knn: usize,
413 pbsamp: usize,
414 anchor_batches: Option<&[usize]>,
415) -> anyhow::Result<Vec<(usize, f32)>> {
416 let pbsamp_batch = layout.pb_sample_to_batch[pbsamp];
424 let centroid: Vec<f32> = layout.centroids.column(pbsamp).iter().copied().collect();
425 let pb_stratum = layout.pb_sample_to_stratum.as_deref();
426 let own_stratum = pb_stratum.map(|s| s[pbsamp]);
427 let mut all_hits: Vec<(usize, f32)> = Vec::new();
428 match anchor_batches {
429 None => {
434 for (b, bknn) in batch_knn_lookup.iter().enumerate() {
435 if b == pbsamp_batch || layout.is_bulk_batch(b) {
436 continue;
437 }
438 let per_batch = knn_distinct_pbsamples_in_batch(
439 bknn,
440 ¢roid,
441 knn,
442 &layout.cell_to_pbsamp,
443 pbsamp,
444 pb_stratum,
445 own_stratum,
446 )?;
447 all_hits.extend(per_batch);
448 }
449 }
450 Some(anchors) => {
456 for &b in anchors {
457 let Some(bknn) = batch_knn_lookup.get(b) else {
458 continue;
459 };
460 let per_batch = knn_distinct_pbsamples_in_batch(
461 bknn,
462 ¢roid,
463 knn,
464 &layout.cell_to_pbsamp,
465 usize::MAX, pb_stratum,
467 own_stratum,
468 )?;
469 all_hits.extend(per_batch);
470 }
471 }
472 }
473 Ok(all_hits)
474}
475
476pub(super) fn per_batch_sc_neighbors(
486 layout: &PbSampleLayout,
487 batch_knn_lookup: &[ColumnDict<usize>],
488 knn: usize,
489 anchor_batches: Option<&[usize]>,
490) -> anyhow::Result<Vec<Vec<(usize, f32)>>> {
491 use indicatif::ParallelProgressIterator;
492 let num_pb = layout.cell_counts.len();
493
494 let prog_bar = styled_progress_bar(num_pb as u64, "pb-samples (BBKNN match)");
495 let result = (0..num_pb)
496 .into_par_iter()
497 .progress_with(prog_bar.clone())
498 .map(|pbsamp| bbknn_match_one_pbsamp(layout, batch_knn_lookup, knn, pbsamp, anchor_batches))
499 .collect();
500 prog_bar.finish_and_clear();
501 result
502}
503
504pub(super) fn build_pb_sample_to_cells(layout: &PbSampleLayout) -> Vec<Vec<usize>> {
516 let num_pb = layout.cell_counts.len();
517 let mut out: Vec<Vec<usize>> = vec![vec![]; num_pb];
518 for (c, &pbsamp) in layout.cell_to_pbsamp.iter().enumerate() {
519 if pbsamp != usize::MAX {
520 out[pbsamp].push(c);
521 }
522 }
523 out
524}
525
526#[cfg(test)]
527#[path = "pb_samples_tests.rs"]
528mod tests;