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 num_genes = data_vec.num_rows();
261 let prog_bar = styled_progress_bar(group_to_cols.len() as u64, "groups (pb-sample gene sums)");
262 let per_group: Vec<Vec<_>> = group_to_cols
265 .par_iter()
266 .progress_with(prog_bar.clone())
267 .map_init(
268 || (vec![0f32; num_genes], vec![false; num_genes], Vec::new()),
269 |(sums, seen, touched), cells| -> anyhow::Result<_> {
270 let yy = data_vec.read_columns_csc(cells.iter().cloned())?;
271
272 let mut by_pbsamp: Vec<(usize, usize)> = cells
275 .iter()
276 .enumerate()
277 .map(|(local_idx, &col)| (cell_to_pbsamp[col], local_idx))
278 .filter(|&(pbsamp, _)| pbsamp != usize::MAX)
279 .collect();
280 by_pbsamp.sort_by_key(|&(pbsamp, _)| pbsamp);
281
282 let mut out: Vec<(usize, Vec<(usize, f32)>)> = Vec::new();
283 for run in by_pbsamp.chunk_by(|a, b| a.0 == b.0) {
284 for &(_, local_idx) in run {
285 let w = data_vec.column_multiplicity(cells[local_idx]);
291 let y_j = yy.col(local_idx);
292 for (&gene, &val) in y_j.row_indices().iter().zip(y_j.values().iter()) {
293 if !seen[gene] {
294 seen[gene] = true;
295 touched.push(gene);
296 }
297 sums[gene] += val * w;
298 }
299 }
300 touched.sort_unstable();
301 let sorted: Vec<(usize, f32)> = touched
302 .drain(..)
303 .map(|gene| {
304 seen[gene] = false;
305 (gene, std::mem::take(&mut sums[gene]))
306 })
307 .collect();
308 out.push((run[0].0, sorted));
309 }
310 Ok(out)
311 },
312 )
313 .collect::<anyhow::Result<_>>()?;
314 prog_bar.finish_and_clear();
315
316 let mut gene_sums: Vec<Vec<(usize, f32)>> = vec![vec![]; num_pb];
317 for (pbsamp_idx, sorted) in per_group.into_iter().flatten() {
318 gene_sums[pbsamp_idx] = sorted;
319 }
320
321 Ok(gene_sums)
322}
323
324pub(super) fn build_pb_samples(
326 data_vec: &SparseIoVec,
327 proj_kn: &DMatrix<f32>,
328 num_genes: usize,
329 anchor_batches: &[usize],
330 bulk_batches: &[usize],
331 cell_to_stratum: Option<&[usize]>,
332) -> anyhow::Result<PbSampleCollection> {
333 let group_to_cols = data_vec
334 .take_grouped_columns()
335 .ok_or(anyhow::anyhow!("columns not assigned to groups"))?;
336 let col_to_batch: Vec<usize> = (0..proj_kn.ncols())
337 .map(|c| data_vec.get_batch_membership(std::iter::once(c))[0])
338 .collect();
339
340 let weights: Option<Vec<f32>> = data_vec.has_column_multiplicity().then(|| {
341 (0..proj_kn.ncols())
342 .map(|c| data_vec.column_multiplicity(c))
343 .collect()
344 });
345 let layout = build_pb_sample_layout(
346 group_to_cols,
347 &col_to_batch,
348 proj_kn,
349 weights.as_deref(),
350 anchor_batches,
351 bulk_batches,
352 cell_to_stratum,
353 )?;
354 let num_pb = layout.cell_counts.len();
355 let gene_sums =
356 collect_pb_sample_gene_sums(data_vec, group_to_cols, &layout.cell_to_pbsamp, num_pb)?;
357
358 Ok(PbSampleCollection {
359 layout,
360 gene_sums,
361 num_genes,
362 })
363}
364
365pub(crate) struct MatchablePbSamples {
370 per_batch: Vec<usize>,
371 per_batch_stratum: Option<Vec<HashMap<usize, usize>>>,
372}
373
374impl MatchablePbSamples {
375 pub(crate) fn new(layout: &PbSampleLayout, batch_knn_lookup: &[ColumnDict<usize>]) -> Self {
376 let strata = layout.pb_sample_to_stratum.as_deref();
377 let (per_batch, by_stratum): (Vec<usize>, Vec<HashMap<usize, usize>>) = batch_knn_lookup
378 .par_iter()
379 .map(|bknn| {
380 let mut pbsamps: Vec<usize> = bknn
381 .names()
382 .iter()
383 .map(|&c| layout.cell_to_pbsamp[c])
384 .filter(|&p| p != usize::MAX)
385 .collect();
386 pbsamps.sort_unstable();
387 pbsamps.dedup();
388 let mut by_stratum: HashMap<usize, usize> = HashMap::default();
389 if let Some(st) = strata {
390 for &p in &pbsamps {
391 *by_stratum.entry(st[p]).or_default() += 1;
392 }
393 }
394 (pbsamps.len(), by_stratum)
395 })
396 .unzip();
397 Self {
398 per_batch,
399 per_batch_stratum: strata.map(|_| by_stratum),
400 }
401 }
402
403 fn count(&self, batch: usize, stratum: Option<usize>) -> usize {
406 match (self.per_batch_stratum.as_ref(), stratum) {
407 (Some(by_stratum), Some(s)) => by_stratum[batch].get(&s).copied().unwrap_or(0),
408 _ => self.per_batch[batch],
409 }
410 }
411}
412
413pub(crate) fn knn_distinct_pbsamples_in_batch(
432 bknn: &ColumnDict<usize>,
433 query: &[f32],
434 knn: usize,
435 available: usize,
436 cell_to_pbsamp: &[usize],
437 own_pbsamp: usize,
438 stratum: Option<(&[usize], usize)>,
439) -> anyhow::Result<Vec<(usize, f32)>> {
440 let n = bknn.num_points();
441 let wanted = knn.min(available);
442 if n == 0 || wanted == 0 {
443 return Ok(Vec::new());
444 }
445 let mut query_k = (knn * 4 + 1).min(n);
449 let mut best: HashMap<usize, f32> = HashMap::default();
450 loop {
451 let (cell_ids, dists) = bknn.search_by_query_data(query, query_k)?;
452 best.clear();
453 for (&c, &d) in cell_ids.iter().zip(dists.iter()) {
454 let other_pbsamp = cell_to_pbsamp[c];
455 if other_pbsamp == usize::MAX || other_pbsamp == own_pbsamp {
456 continue;
457 }
458 if stratum.is_some_and(|(st, own_s)| st[other_pbsamp] != own_s) {
459 continue;
460 }
461 best.entry(other_pbsamp)
462 .and_modify(|old| {
463 if d < *old {
464 *old = d;
465 }
466 })
467 .or_insert(d);
468 }
469 if best.len() >= wanted || query_k >= n {
470 break;
471 }
472 query_k = query_k.saturating_mul(4).min(n);
473 }
474 let mut per_batch: Vec<(usize, f32)> = best.drain().collect();
475 per_batch.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
476 per_batch.truncate(knn);
477 Ok(per_batch)
478}
479
480fn bbknn_match_one_pbsamp(
487 layout: &PbSampleLayout,
488 batch_knn_lookup: &[ColumnDict<usize>],
489 matchable: &MatchablePbSamples,
490 knn: usize,
491 pbsamp: usize,
492 anchor_batches: Option<&[usize]>,
493) -> anyhow::Result<Vec<(usize, f32)>> {
494 let pbsamp_batch = layout.pb_sample_to_batch[pbsamp];
502 let centroid: Vec<f32> = layout.centroids.column(pbsamp).iter().copied().collect();
503 let stratum = layout
504 .pb_sample_to_stratum
505 .as_deref()
506 .map(|st| (st, st[pbsamp]));
507 let own_stratum = stratum.map(|(_, own_s)| own_s);
508 let mut all_hits: Vec<(usize, f32)> = Vec::new();
509 match anchor_batches {
510 None => {
515 for (b, bknn) in batch_knn_lookup.iter().enumerate() {
516 if b == pbsamp_batch || layout.is_bulk_batch(b) {
517 continue;
518 }
519 let per_batch = knn_distinct_pbsamples_in_batch(
520 bknn,
521 ¢roid,
522 knn,
523 matchable.count(b, own_stratum),
524 &layout.cell_to_pbsamp,
525 pbsamp,
526 stratum,
527 )?;
528 all_hits.extend(per_batch);
529 }
530 }
531 Some(anchors) => {
537 for &b in anchors {
538 let Some(bknn) = batch_knn_lookup.get(b) else {
539 continue;
540 };
541 let per_batch = knn_distinct_pbsamples_in_batch(
542 bknn,
543 ¢roid,
544 knn,
545 matchable.count(b, own_stratum),
546 &layout.cell_to_pbsamp,
547 usize::MAX, stratum,
549 )?;
550 all_hits.extend(per_batch);
551 }
552 }
553 }
554 Ok(all_hits)
555}
556
557pub(crate) fn per_batch_sc_neighbors(
570 layout: &PbSampleLayout,
571 batch_knn_lookup: &[ColumnDict<usize>],
572 knn: usize,
573 anchor_batches: Option<&[usize]>,
574) -> anyhow::Result<Vec<Vec<(usize, f32)>>> {
575 use indicatif::ParallelProgressIterator;
576 let num_pb = layout.cell_counts.len();
577 let matchable = MatchablePbSamples::new(layout, batch_knn_lookup);
578
579 let prog_bar = styled_progress_bar(num_pb as u64, "pb-samples (BBKNN match)");
580 let result = (0..num_pb)
581 .into_par_iter()
582 .progress_with(prog_bar.clone())
583 .map(|pbsamp| {
584 bbknn_match_one_pbsamp(
585 layout,
586 batch_knn_lookup,
587 &matchable,
588 knn,
589 pbsamp,
590 anchor_batches,
591 )
592 })
593 .collect();
594 prog_bar.finish_and_clear();
595 result
596}
597
598pub(super) fn build_pb_sample_to_cells(layout: &PbSampleLayout) -> Vec<Vec<usize>> {
610 let num_pb = layout.cell_counts.len();
611 let mut out: Vec<Vec<usize>> = vec![vec![]; num_pb];
612 for (c, &pbsamp) in layout.cell_to_pbsamp.iter().enumerate() {
613 if pbsamp != usize::MAX {
614 out[pbsamp].push(c);
615 }
616 }
617 out
618}
619
620#[cfg(test)]
621#[path = "pb_samples_tests.rs"]
622mod tests;