1use super::*;
25use crate::alg::nb_dispersion::DispersionTrend;
26use nalgebra_sparse::CscMatrix;
27use rand::rngs::SmallRng;
28use rand::{RngExt, SeedableRng};
29use serde::{Deserialize, Serialize};
30
31pub const DEFAULT_RESIDUAL_BITS_SEED: u64 = 0x5245_5349_4442_4954;
37
38const VE_TOP_GENES: usize = 200;
41
42#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
45pub enum BelowEdge {
46 Keep,
49 Stop,
51}
52
53#[derive(Clone, Debug)]
54pub struct PbTreeParams {
55 pub min_cells_to_split: usize,
57 pub min_node_gene_count: f32,
60 pub max_genes: usize,
62 pub edge_margin: f32,
65 pub below_edge: BelowEdge,
66 pub max_cells_exact: usize,
68 pub num_landmarks: usize,
69 pub top_contrast_genes: usize,
71 pub seed: u64,
72 pub reassign_cells: Option<super::ReassignCellsParams>,
75}
76
77impl Default for PbTreeParams {
78 fn default() -> Self {
79 Self {
80 min_cells_to_split: 8,
81 min_node_gene_count: 5.0,
82 max_genes: 2000,
83 edge_margin: 0.10,
84 below_edge: BelowEdge::Keep,
85 max_cells_exact: 512,
86 num_landmarks: 512,
87 top_contrast_genes: 20,
88 seed: DEFAULT_RESIDUAL_BITS_SEED,
89 reassign_cells: None,
90 }
91 }
92}
93
94#[derive(Clone, Debug, Serialize, Deserialize)]
100pub struct ContrastGene {
101 pub gene: usize,
102 pub loading: f32,
103 pub lfc: f32,
104 pub weight: f32,
105}
106
107#[derive(Clone, Debug, Serialize, Deserialize)]
108pub struct SplitRecord {
109 pub root: usize,
111 pub depth: usize,
113 pub path: usize,
115 pub n_cells: usize,
116 pub n_left: usize,
117 pub n_right: usize,
118 pub n_genes: usize,
119 pub n_left_per_batch: Vec<usize>,
120 pub n_right_per_batch: Vec<usize>,
121 pub s1: f32,
122 pub s2: f32,
123 pub sigma: f32,
124 pub mp_edge: f32,
125 pub passes_edge: bool,
126 pub llr_split: f64,
129 pub ve_ratio: f32,
131 pub applied: bool,
133 #[serde(skip)]
135 pub genes: Vec<usize>,
136 #[serde(skip)]
138 pub loadings: Vec<f32>,
139 pub pos: Vec<ContrastGene>,
141 pub neg: Vec<ContrastGene>,
143}
144
145#[derive(Clone, Debug, Serialize, Deserialize)]
146pub struct RootRecord {
147 pub root: usize,
148 pub n_cells: usize,
149 pub n_genes: usize,
150 pub ve_ratio_residual: f32,
153 pub ve_ratio_marginal: f32,
155 pub splits: Vec<SplitRecord>,
156}
157
158#[derive(Clone, Debug, Default, Serialize, Deserialize)]
159pub struct PbTree {
160 pub coarse_bits: usize,
162 pub depth: usize,
163 pub num_cells: usize,
164 pub num_batches: usize,
165 pub edge_margin: f32,
166 pub reassigned_cells: usize,
169 pub roots: Vec<RootRecord>,
170 pub leaf_to_finest_pb: Vec<(usize, Vec<usize>)>,
173}
174
175pub(crate) struct NodeBlock {
182 pub csc: CscMatrix<f32>,
184 pub batch: Vec<usize>,
185 pub depth: Vec<f32>,
186 pub num_batches: usize,
187}
188
189impl NodeBlock {
190 pub(crate) fn new(csc: CscMatrix<f32>, batch: Vec<usize>, num_batches: usize) -> Self {
191 let depth: Vec<f32> = (0..csc.ncols())
192 .map(|j| csc.col(j).values().iter().sum())
193 .collect();
194 Self {
195 csc,
196 batch,
197 depth,
198 num_batches: num_batches.max(1),
199 }
200 }
201
202 fn ngenes(&self) -> usize {
203 self.csc.nrows()
204 }
205}
206
207const FOLD_MIN_CELLS: usize = 256;
212
213pub(crate) struct NodeProfiles {
216 pub pooled: Vec<f64>,
217 per_batch: Vec<Vec<f64>>,
218 pub batch_depth: Vec<f64>,
219 pub node_sum: Vec<f64>,
220 node_sum_sq: Vec<f64>,
221 pub total_depth: f64,
222 pub num_cells: usize,
223}
224
225impl NodeProfiles {
226 pub(crate) fn new(block: &NodeBlock, local: &[usize], min_cells: usize) -> Self {
227 let d = block.ngenes();
228 let nb = block.num_batches;
229 struct Acc {
230 sum_b: Vec<Vec<f64>>,
231 depth_b: Vec<f64>,
232 cells_b: Vec<usize>,
233 sum_sq: Vec<f64>,
234 }
235 let fresh = || Acc {
236 sum_b: vec![vec![0f64; d]; nb],
237 depth_b: vec![0f64; nb],
238 cells_b: vec![0usize; nb],
239 sum_sq: vec![0f64; d],
240 };
241 let acc = local
242 .par_iter()
243 .with_min_len(FOLD_MIN_CELLS)
244 .fold(fresh, |mut a, &c| {
245 let b = block.batch[c];
246 let col = block.csc.col(c);
247 for (&g, &v) in col.row_indices().iter().zip(col.values()) {
248 a.sum_b[b][g] += f64::from(v);
249 a.sum_sq[g] += f64::from(v) * f64::from(v);
250 }
251 a.depth_b[b] += f64::from(block.depth[c]);
252 a.cells_b[b] += 1;
253 a
254 })
255 .reduce(fresh, |mut a, o| {
256 for (x, y) in a.sum_b.iter_mut().zip(&o.sum_b) {
257 for (p, q) in x.iter_mut().zip(y) {
258 *p += q;
259 }
260 }
261 for (x, y) in a.depth_b.iter_mut().zip(&o.depth_b) {
262 *x += y;
263 }
264 for (x, y) in a.cells_b.iter_mut().zip(&o.cells_b) {
265 *x += y;
266 }
267 for (x, y) in a.sum_sq.iter_mut().zip(&o.sum_sq) {
268 *x += y;
269 }
270 a
271 });
272 let Acc {
273 sum_b,
274 depth_b,
275 cells_b,
276 sum_sq: node_sum_sq,
277 } = acc;
278 let node_sum: Vec<f64> = (0..d).map(|g| sum_b.iter().map(|s| s[g]).sum()).collect();
279 let total_depth: f64 = depth_b.iter().sum();
280 let pooled: Vec<f64> = node_sum
281 .iter()
282 .map(|&s| {
283 if total_depth > 0.0 {
284 s / total_depth
285 } else {
286 0.0
287 }
288 })
289 .collect();
290 let per_batch: Vec<Vec<f64>> = (0..nb)
291 .map(|b| {
292 if cells_b[b] >= min_cells && depth_b[b] > 0.0 {
293 sum_b[b].iter().map(|&s| s / depth_b[b]).collect()
294 } else {
295 pooled.clone()
296 }
297 })
298 .collect();
299 Self {
300 pooled,
301 per_batch,
302 batch_depth: depth_b,
303 node_sum,
304 node_sum_sq,
305 total_depth,
306 num_cells: local.len(),
307 }
308 }
309
310 #[inline]
311 fn rate(&self, b: usize, g: usize) -> f64 {
312 self.per_batch[b][g]
313 }
314}
315
316#[inline]
317fn clipped(x: f64, clip: f32) -> f32 {
318 let c = f64::from(clip);
319 x.clamp(-c, c) as f32
320}
321
322pub(crate) fn residual_variance(
326 block: &NodeBlock,
327 local: &[usize],
328 prof: &NodeProfiles,
329 clip: f32,
330) -> Vec<f64> {
331 let d = block.ngenes();
332 let nb = block.num_batches;
333 let fresh = || (vec![0f64; d], vec![vec![0f64; d]; nb]);
334 let (sq, nz_depth) = local
335 .par_iter()
336 .with_min_len(FOLD_MIN_CELLS)
337 .fold(fresh, |(mut sq, mut nz), &c| {
338 let b = block.batch[c];
339 let n_c = f64::from(block.depth[c]);
340 let col = block.csc.col(c);
341 for (&g, &v) in col.row_indices().iter().zip(col.values()) {
342 let mu = n_c * prof.rate(b, g);
343 if mu > 0.0 {
344 let r = f64::from(clipped((f64::from(v) - mu) / mu.sqrt(), clip));
345 sq[g] += r * r;
346 nz[b][g] += n_c;
347 }
348 }
349 (sq, nz)
350 })
351 .reduce(fresh, |(mut sq, mut nz), (sq2, nz2)| {
352 for (x, y) in sq.iter_mut().zip(&sq2) {
353 *x += y;
354 }
355 for (a, b) in nz.iter_mut().zip(&nz2) {
356 for (x, y) in a.iter_mut().zip(b) {
357 *x += y;
358 }
359 }
360 (sq, nz)
361 });
362 let n = local.len().max(1) as f64;
363 (0..d)
364 .map(|g| {
365 let zeros: f64 = (0..nb)
366 .map(|b| prof.rate(b, g) * (prof.batch_depth[b] - nz_depth[b][g]).max(0.0))
367 .sum();
368 (sq[g] + zeros) / n
369 })
370 .collect()
371}
372
373pub(crate) fn residual_dense(
375 block: &NodeBlock,
376 rows: &[usize],
377 genes: &[usize],
378 prof: &NodeProfiles,
379 clip: f32,
380) -> DMatrix<f32> {
381 let d = block.ngenes();
382 let mut gidx = vec![usize::MAX; d];
383 for (j, &g) in genes.iter().enumerate() {
384 gidx[g] = j;
385 }
386 let p = genes.len();
387 let flat: Vec<Vec<f32>> = rows
388 .par_iter()
389 .map(|&c| {
390 let b = block.batch[c];
391 let n_c = f64::from(block.depth[c]);
392 let mut row = vec![0f32; p];
393 for (j, &g) in genes.iter().enumerate() {
394 let mu = n_c * prof.rate(b, g);
395 row[j] = if mu > 0.0 {
396 clipped(-mu.sqrt(), clip)
397 } else {
398 0.0
399 };
400 }
401 let col = block.csc.col(c);
402 for (&g, &v) in col.row_indices().iter().zip(col.values()) {
403 let j = gidx[g];
404 if j == usize::MAX {
405 continue;
406 }
407 let mu = n_c * prof.rate(b, g);
408 row[j] = if mu > 0.0 {
409 clipped((f64::from(v) - mu) / mu.sqrt(), clip)
410 } else {
411 0.0
412 };
413 }
414 row
415 })
416 .collect();
417 DMatrix::<f32>::from_fn(rows.len(), p, |i, j| flat[i][j])
418}
419
420fn residual_scores(
423 block: &NodeBlock,
424 local: &[usize],
425 genes: &[usize],
426 prof: &NodeProfiles,
427 v1: &[f32],
428 clip: f32,
429) -> Vec<f32> {
430 let d = block.ngenes();
431 let nb = block.num_batches;
432 let mut gidx = vec![usize::MAX; d];
433 for (j, &g) in genes.iter().enumerate() {
434 gidx[g] = j;
435 }
436 let s_b: Vec<f64> = (0..nb)
437 .map(|b| {
438 genes
439 .iter()
440 .enumerate()
441 .map(|(j, &g)| prof.rate(b, g).sqrt() * f64::from(v1[j]))
442 .sum()
443 })
444 .collect();
445 local
446 .par_iter()
447 .map(|&c| {
448 let b = block.batch[c];
449 let n_c = f64::from(block.depth[c]);
450 let col = block.csc.col(c);
451 let mut acc = 0f64;
452 for (&g, &v) in col.row_indices().iter().zip(col.values()) {
453 let j = gidx[g];
454 if j == usize::MAX {
455 continue;
456 }
457 let mu = n_c * prof.rate(b, g);
458 if mu > 0.0 {
459 let r = f64::from(clipped((f64::from(v) - mu) / mu.sqrt(), clip));
460 acc += (r + mu.sqrt()) * f64::from(v1[j]);
461 }
462 }
463 (acc - n_c.sqrt() * s_b[b]) as f32
464 })
465 .collect()
466}
467
468pub(crate) fn mp_edge(n: usize, p: usize, sigma2: f64) -> f32 {
474 (sigma2.sqrt() * ((n as f64).sqrt() + (p as f64).sqrt())) as f32
475}
476
477pub(crate) fn two_group_llr(sum_l: &[f64], sum_r: &[f64]) -> f64 {
480 let nl: f64 = sum_l.iter().sum();
481 let nr: f64 = sum_r.iter().sum();
482 if nl <= 0.0 || nr <= 0.0 {
483 return 0.0;
484 }
485 let f = |y: f64, n: f64| if y > 0.0 { y * (y / n).ln() } else { 0.0 };
486 sum_l
487 .iter()
488 .zip(sum_r)
489 .map(|(&l, &r)| f(l, nl) + f(r, nr) - f(l + r, nl + nr))
490 .sum()
491}
492
493pub(crate) fn ve_ratio(r: &DMatrix<f32>, labels: &[usize], top_q: usize) -> f32 {
497 let n = r.nrows();
498 if n < 2 {
499 return 1.0;
500 }
501 let (compact, k) = crate::alg::dc_poisson::compact_labels(labels);
502 if k < 2 {
503 return 1.0;
504 }
505 let expected = (k - 1) as f64 / (n - 1) as f64;
506 let mut counts = vec![0f64; k];
507 for &z in &compact {
508 counts[z] += 1.0;
509 }
510 let mut per_gene: Vec<(f64, f64)> = (0..r.ncols())
511 .into_par_iter()
512 .map(|j| {
513 let col = r.column(j);
514 let mean = col.iter().map(|&x| f64::from(x)).sum::<f64>() / n as f64;
515 let ss_total: f64 = col.iter().map(|&x| (f64::from(x) - mean).powi(2)).sum();
516 let mut gsum = vec![0f64; k];
517 for (i, &x) in col.iter().enumerate() {
518 gsum[compact[i]] += f64::from(x);
519 }
520 let ss_between: f64 = (0..k)
521 .filter(|&z| counts[z] > 0.0)
522 .map(|z| counts[z] * (gsum[z] / counts[z] - mean).powi(2))
523 .sum();
524 (ss_total, ss_between)
525 })
526 .collect();
527 per_gene.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
528 let top: Vec<f64> = per_gene
529 .iter()
530 .take(top_q)
531 .filter(|(t, _)| *t > 0.0)
532 .map(|(t, b)| b / t / expected)
533 .collect();
534 if top.is_empty() {
535 1.0
536 } else {
537 (top.iter().sum::<f64>() / top.len() as f64) as f32
538 }
539}
540
541fn median(v: &mut [f64]) -> f64 {
542 if v.is_empty() {
543 return 0.0;
544 }
545 v.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
546 let m = v.len() / 2;
547 if v.len() % 2 == 1 {
548 v[m]
549 } else {
550 0.5 * (v[m - 1] + v[m])
551 }
552}
553
554fn top_component(r: &DMatrix<f32>) -> Option<(Vec<f32>, f32, f32)> {
558 if r.nrows() < 2 || r.ncols() < 2 {
559 return None;
560 }
561 let (_, s, v) = r.rsvd(2).ok()?;
562 if s.is_empty() || v.ncols() == 0 {
563 return None;
564 }
565 let mut order: Vec<usize> = (0..s.len()).collect();
566 order.sort_by(|&a, &b| s[b].partial_cmp(&s[a]).unwrap_or(std::cmp::Ordering::Equal));
567 let s1 = s[order[0]];
568 let s2 = order.get(1).map_or(0.0, |&i| s[i]);
569 if !s1.is_finite() || s1 <= 0.0 {
570 return None;
571 }
572 let mut v1: Vec<f32> = v.column(order[0]).iter().copied().collect();
573 let argmax = v1
574 .iter()
575 .enumerate()
576 .max_by(|a, b| a.1.abs().partial_cmp(&b.1.abs()).unwrap())
577 .map_or(0, |(i, _)| i);
578 if v1[argmax] < 0.0 {
579 v1.iter_mut().for_each(|x| *x = -*x);
580 }
581 Some((v1, s1, s2))
582}
583
584struct SubNode {
589 prof: NodeProfiles,
590 genes: Vec<usize>,
591 sigma2: f64,
592}
593
594fn prepare(block: &NodeBlock, local: &[usize], params: &PbTreeParams) -> SubNode {
597 let prof = NodeProfiles::new(block, local, params.min_cells_to_split);
598 let clip = (local.len() as f32).sqrt();
599 let var = residual_variance(block, local, &prof, clip);
600 let mut cand: Vec<(usize, f64)> = (0..block.ngenes())
601 .filter(|&g| prof.node_sum[g] >= f64::from(params.min_node_gene_count))
602 .map(|g| (g, var[g]))
603 .collect();
604 cand.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
605 cand.truncate(params.max_genes);
606 cand.sort_by_key(|x| x.0);
607 let mut vars: Vec<f64> = cand.iter().map(|x| x.1).collect();
608 let sigma2 = median(&mut vars).max(f64::EPSILON);
609 SubNode {
610 prof,
611 genes: cand.into_iter().map(|x| x.0).collect(),
612 sigma2,
613 }
614}
615
616fn svd_rows(local: &[usize], params: &PbTreeParams, seed: u64) -> Vec<usize> {
619 if local.len() <= params.max_cells_exact || params.num_landmarks >= local.len() {
620 return local.to_vec();
621 }
622 let mut rng = SmallRng::seed_from_u64(seed);
623 let mut pool = local.to_vec();
624 let k = params.num_landmarks.max(2);
625 for i in 0..k {
626 let j = rng.random_range(i..pool.len());
627 pool.swap(i, j);
628 }
629 pool.truncate(k);
630 pool
631}
632
633fn side_sums(block: &NodeBlock, cells: &[usize], genes: &[usize]) -> Vec<f64> {
634 let d = block.ngenes();
635 let mut gidx = vec![usize::MAX; d];
636 for (j, &g) in genes.iter().enumerate() {
637 gidx[g] = j;
638 }
639 cells
640 .par_iter()
641 .with_min_len(FOLD_MIN_CELLS)
642 .fold(
643 || vec![0f64; genes.len()],
644 |mut acc, &c| {
645 let col = block.csc.col(c);
646 for (&g, &v) in col.row_indices().iter().zip(col.values()) {
647 let j = gidx[g];
648 if j != usize::MAX {
649 acc[j] += f64::from(v);
650 }
651 }
652 acc
653 },
654 )
655 .reduce(
656 || vec![0f64; genes.len()],
657 |mut a, b| {
658 for (x, y) in a.iter_mut().zip(b) {
659 *x += y;
660 }
661 a
662 },
663 )
664}
665
666fn contrast(
670 sub: &SubNode,
671 loadings: &[f32],
672 sum_l: &[f64],
673 sum_r: &[f64],
674 k: usize,
675) -> (Vec<ContrastGene>, Vec<ContrastGene>) {
676 let n = sub.prof.num_cells.max(1) as f64;
677 let means: Vec<f32> = sub
678 .genes
679 .iter()
680 .map(|&g| (sub.prof.node_sum[g] / n) as f32)
681 .collect();
682 let vars: Vec<f32> = sub
683 .genes
684 .iter()
685 .zip(&means)
686 .map(|(&g, &m)| (sub.prof.node_sum_sq[g] / n - f64::from(m) * f64::from(m)).max(0.0) as f32)
687 .collect();
688 let trend = DispersionTrend::fit(&means, &vars);
689 let total: f64 = sub
690 .genes
691 .iter()
692 .map(|&g| sub.prof.node_sum[g])
693 .sum::<f64>()
694 .max(1.0);
695 let avg_s = (sub.prof.total_depth / n) as f32;
696 let nl: f64 = sum_l.iter().sum::<f64>().max(1.0);
697 let nr: f64 = sum_r.iter().sum::<f64>().max(1.0);
698 let make = |j: usize| ContrastGene {
699 gene: sub.genes[j],
700 loading: loadings[j],
701 lfc: (((sum_r[j] + 0.5) / nr).ln() - ((sum_l[j] + 0.5) / nl).ln()) as f32,
702 weight: trend.fisher_weight(
703 (sub.prof.node_sum[sub.genes[j]] / total) as f32,
704 avg_s,
705 means[j],
706 ),
707 };
708 let mut order: Vec<usize> = (0..loadings.len()).collect();
709 order.sort_by(|&a, &b| {
710 loadings[b]
711 .partial_cmp(&loadings[a])
712 .unwrap_or(std::cmp::Ordering::Equal)
713 });
714 let pos: Vec<ContrastGene> = order
715 .iter()
716 .take(k)
717 .filter(|&&j| loadings[j] > 0.0)
718 .map(|&j| make(j))
719 .collect();
720 let neg: Vec<ContrastGene> = order
721 .iter()
722 .rev()
723 .take(k)
724 .filter(|&&j| loadings[j] < 0.0)
725 .map(|&j| make(j))
726 .collect();
727 (pos, neg)
728}
729
730pub(crate) struct RootBlock {
737 pub root: usize,
738 pub cells: Vec<usize>,
739 pub block: NodeBlock,
740}
741
742struct Proposal {
744 left: Vec<usize>,
745 right: Vec<usize>,
746 record: SplitRecord,
747 applicable: bool,
750 gain: f64,
752}
753
754struct Leaf {
755 root_idx: usize,
756 cells: Vec<usize>,
758 depth: usize,
759 path: usize,
760 proposal: Option<Proposal>,
761}
762
763fn propose(root: &RootBlock, leaf: &Leaf, params: &PbTreeParams) -> Option<Proposal> {
766 let block = &root.block;
767 let local = &leaf.cells;
768 if local.len() < params.min_cells_to_split {
769 return None;
770 }
771 let sub = prepare(block, local, params);
772 if sub.genes.len() < 2 {
773 return None;
774 }
775 let clip = (local.len() as f32).sqrt();
776 let seed = params.seed
777 ^ (root.root as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15)
778 ^ ((leaf.depth as u64) << 48)
779 ^ (leaf.path as u64).wrapping_mul(0xC2B2_AE3D_27D4_EB4F);
780 let rows = svd_rows(local, params, seed);
781 let r = residual_dense(block, &rows, &sub.genes, &sub.prof, clip);
782 let (v1, s1, s2) = top_component(&r)?;
783 let scores = residual_scores(block, local, &sub.genes, &sub.prof, &v1, clip);
784 let mut right: Vec<usize> = Vec::new();
785 let mut left: Vec<usize> = Vec::new();
786 for (&c, &s) in local.iter().zip(&scores) {
787 if s > 0.0 {
788 right.push(c);
789 } else {
790 left.push(c);
791 }
792 }
793 let edge = mp_edge(rows.len(), sub.genes.len(), sub.sigma2);
794 let min_side = (params.min_cells_to_split / 2).max(1);
795 let balanced = left.len().min(right.len()) >= min_side;
796 let passes = balanced && s1 > (1.0 + params.edge_margin) * edge;
797 let applicable = balanced && (passes || params.below_edge == BelowEdge::Keep);
798 let sum_l = side_sums(block, &left, &sub.genes);
799 let sum_r = side_sums(block, &right, &sub.genes);
800 let nb = block.num_batches;
801 let per_batch = |cells: &[usize]| {
802 let mut v = vec![0usize; nb];
803 for &c in cells {
804 v[block.batch[c]] += 1;
805 }
806 v
807 };
808 let mut side_of_local = vec![0usize; block.csc.ncols()];
809 for &c in &right {
810 side_of_local[c] = 1;
811 }
812 let row_side: Vec<usize> = rows.iter().map(|&c| side_of_local[c]).collect();
813 let llr = two_group_llr(&sum_l, &sum_r);
814 let (pos, neg) = contrast(&sub, &v1, &sum_l, &sum_r, params.top_contrast_genes);
815 let record = SplitRecord {
816 root: root.root,
817 depth: leaf.depth + 1,
818 path: leaf.path,
819 n_cells: local.len(),
820 n_left: left.len(),
821 n_right: right.len(),
822 n_genes: sub.genes.len(),
823 n_left_per_batch: per_batch(&left),
824 n_right_per_batch: per_batch(&right),
825 s1,
826 s2,
827 sigma: sub.sigma2.sqrt() as f32,
828 mp_edge: edge,
829 passes_edge: passes,
830 llr_split: llr,
831 ve_ratio: ve_ratio(&r, &row_side, VE_TOP_GENES),
832 applied: false,
833 genes: sub.genes,
834 loadings: v1,
835 pos,
836 neg,
837 };
838 Some(Proposal {
839 left,
840 right,
841 record,
842 applicable,
843 gain: llr,
844 })
845}
846
847pub(crate) struct TargetTree {
850 pub labels_per_level: Vec<Vec<usize>>,
853 pub tree: PbTree,
854}
855
856pub(crate) fn bisect_to_target(
866 roots: &[RootBlock],
867 num_cells: usize,
868 level_targets: &[usize],
869 params: &PbTreeParams,
870) -> TargetTree {
871 let mut leaves: Vec<Leaf> = roots
872 .iter()
873 .enumerate()
874 .map(|(i, r)| Leaf {
875 root_idx: i,
876 cells: (0..r.cells.len()).collect(),
877 depth: 0,
878 path: 0,
879 proposal: None,
880 })
881 .collect();
882 let mut label: Vec<usize> = vec![usize::MAX; num_cells];
883 for (i, r) in roots.iter().enumerate() {
884 for &c in &r.cells {
885 label[c] = i;
886 }
887 }
888 leaves.par_iter_mut().for_each(|leaf| {
889 leaf.proposal = propose(&roots[leaf.root_idx], leaf, params);
890 });
891 let mut splits: Vec<Vec<SplitRecord>> = vec![Vec::new(); roots.len()];
892 let mut levels: Vec<Vec<usize>> = Vec::with_capacity(level_targets.len());
893 let mut next = 0usize;
894 let mut alive: Vec<bool> = vec![true; leaves.len()];
895 let final_target = level_targets.last().copied().unwrap_or(0) as u64;
897 let prog_bar = legume_numeric::matrix::progress::new_progress_bar(final_target)
898 .with_message("pb tree leaves");
899 prog_bar.set_position(leaves.len() as u64);
900
901 loop {
902 let n_leaves = alive.iter().filter(|&&a| a).count();
903 while next < level_targets.len() && n_leaves >= level_targets[next] {
904 levels.push(label.clone());
905 next += 1;
906 }
907 if next >= level_targets.len() {
908 break;
909 }
910 let mut cand: Vec<(f64, usize)> = leaves
911 .iter()
912 .enumerate()
913 .filter(|(i, l)| alive[*i] && l.proposal.as_ref().is_some_and(|p| p.applicable))
914 .map(|(i, l)| (l.proposal.as_ref().map_or(0.0, |p| p.gain), i))
915 .collect();
916 if cand.is_empty() {
917 while next < level_targets.len() {
918 levels.push(label.clone());
919 next += 1;
920 }
921 break;
922 }
923 cand.sort_by(|a, b| {
924 b.0.partial_cmp(&a.0)
925 .unwrap_or(std::cmp::Ordering::Equal)
926 .then(a.1.cmp(&b.1))
927 });
928 let budget = level_targets[next] - n_leaves;
929 prog_bar.set_message(format!(
930 "pb tree leaves, level {}/{} toward {}",
931 next + 1,
932 level_targets.len(),
933 level_targets[next]
934 ));
935 let mut fresh: Vec<usize> = Vec::new();
936 for &(_, i) in cand.iter().take(budget) {
937 let Leaf {
938 root_idx,
939 depth,
940 path,
941 proposal,
942 ..
943 } = std::mem::replace(
944 &mut leaves[i],
945 Leaf {
946 root_idx: 0,
947 cells: Vec::new(),
948 depth: 0,
949 path: 0,
950 proposal: None,
951 },
952 );
953 let mut p = proposal.expect("candidate has a proposal");
954 p.record.applied = true;
955 splits[root_idx].push(p.record);
956 alive[i] = false;
957 let root = &roots[root_idx];
958 for (side, cells) in [(0usize, p.left), (1usize, p.right)] {
959 let id = leaves.len();
960 for &c in &cells {
961 label[root.cells[c]] = id;
962 }
963 leaves.push(Leaf {
964 root_idx,
965 cells,
966 depth: depth + 1,
967 path: (path << 1) | side,
968 proposal: None,
969 });
970 alive.push(true);
971 fresh.push(id);
972 }
973 prog_bar.inc(1);
974 }
975 let mut new_leaves: Vec<(usize, Leaf)> = fresh
976 .iter()
977 .map(|&id| {
978 (
979 id,
980 std::mem::replace(
981 &mut leaves[id],
982 Leaf {
983 root_idx: 0,
984 cells: Vec::new(),
985 depth: 0,
986 path: 0,
987 proposal: None,
988 },
989 ),
990 )
991 })
992 .collect();
993 new_leaves.par_iter_mut().for_each(|(_, leaf)| {
994 leaf.proposal = propose(&roots[leaf.root_idx], leaf, params);
995 });
996 for (id, leaf) in new_leaves {
997 leaves[id] = leaf;
998 }
999 }
1000
1001 prog_bar.finish_and_clear();
1002
1003 for (i, leaf) in leaves.iter_mut().enumerate() {
1005 if alive[i] {
1006 if let Some(p) = leaf.proposal.take() {
1007 splits[leaf.root_idx].push(p.record);
1008 }
1009 }
1010 }
1011 let max_depth = splits
1012 .iter()
1013 .flatten()
1014 .filter(|s| s.applied)
1015 .map(|s| s.depth)
1016 .max()
1017 .unwrap_or(0);
1018 let num_batches = roots.first().map_or(1, |r| r.block.num_batches);
1019 let nodes: Vec<RootRecord> = roots
1020 .iter()
1021 .zip(splits)
1022 .map(|(r, mut sp)| {
1023 sp.sort_by_key(|s| (s.depth, s.path));
1024 RootRecord {
1025 root: r.root,
1026 n_cells: r.cells.len(),
1027 n_genes: sp.first().map_or(0, |s| s.n_genes),
1028 ve_ratio_residual: 1.0,
1029 ve_ratio_marginal: 1.0,
1030 splits: sp,
1031 }
1032 })
1033 .collect();
1034 let tree = PbTree {
1035 coarse_bits: 0,
1036 depth: max_depth,
1037 num_cells,
1038 num_batches,
1039 edge_margin: params.edge_margin,
1040 reassigned_cells: 0,
1041 roots: nodes,
1042 leaf_to_finest_pb: Vec::new(),
1043 };
1044 TargetTree {
1045 labels_per_level: levels,
1046 tree,
1047 }
1048}
1049
1050pub(crate) fn node_ve_ratios(
1054 roots: &[RootBlock],
1055 finest: &[usize],
1056 reference: &[usize],
1057 params: &PbTreeParams,
1058 tree: &mut PbTree,
1059) {
1060 let ratios: Vec<(f32, f32)> = roots
1061 .par_iter()
1062 .map(|r| {
1063 if r.cells.len() < params.min_cells_to_split {
1064 return (1.0, 1.0);
1065 }
1066 let local: Vec<usize> = (0..r.cells.len()).collect();
1067 let sub = prepare(&r.block, &local, params);
1068 if sub.genes.len() < 2 {
1069 return (1.0, 1.0);
1070 }
1071 let rows = svd_rows(&local, params, params.seed ^ r.root as u64);
1072 let m = residual_dense(
1073 &r.block,
1074 &rows,
1075 &sub.genes,
1076 &sub.prof,
1077 (local.len() as f32).sqrt(),
1078 );
1079 let a: Vec<usize> = rows.iter().map(|&c| finest[r.cells[c]]).collect();
1080 let b: Vec<usize> = rows.iter().map(|&c| reference[r.cells[c]]).collect();
1081 (
1082 ve_ratio(&m, &a, VE_TOP_GENES),
1083 ve_ratio(&m, &b, VE_TOP_GENES),
1084 )
1085 })
1086 .collect();
1087 for (node, (res, marg)) in tree.roots.iter_mut().zip(ratios) {
1088 node.ve_ratio_residual = res;
1089 node.ve_ratio_marginal = marg;
1090 }
1091}
1092
1093fn bits_for(k: usize) -> usize {
1098 (usize::BITS as usize - k.max(2).saturating_sub(1).leading_zeros() as usize).max(1)
1099}
1100
1101pub(crate) fn pack_levels(levels: &[Vec<usize>]) -> (Vec<usize>, Vec<usize>) {
1107 let n = levels.first().map_or(0, Vec::len);
1108 let mut codes = vec![0usize; n];
1109 let mut widths: Vec<usize> = Vec::with_capacity(levels.len());
1110 let mut shift = 0usize;
1111 let mut parent_codes: Vec<usize> = vec![0; n];
1112 for (li, level) in levels.iter().enumerate() {
1113 let active: Vec<usize> = level
1114 .iter()
1115 .map(|&l| if l == usize::MAX { 0 } else { l })
1116 .collect();
1117 let field: Vec<usize>;
1118 let width: usize;
1119 if li == 0 {
1120 let (compact, k) = crate::alg::dc_poisson::compact_labels(&active);
1121 width = bits_for(k);
1122 field = compact;
1123 } else {
1124 let mut child_index: HashMap<(usize, usize), usize> = HashMap::default();
1126 let mut children_of: HashMap<usize, usize> = HashMap::default();
1127 let mut idx = vec![0usize; n];
1128 for c in 0..n {
1129 let key = (parent_codes[c], active[c]);
1130 let next = children_of.entry(parent_codes[c]).or_insert(0);
1131 let e = child_index.entry(key).or_insert_with(|| {
1132 let v = *next;
1133 *next += 1;
1134 v
1135 });
1136 idx[c] = *e;
1137 }
1138 let max_children = children_of.values().copied().max().unwrap_or(1);
1139 width = bits_for(max_children);
1140 field = idx;
1141 }
1142 for c in 0..n {
1143 codes[c] |= field[c] << shift;
1144 }
1145 shift += width;
1146 widths.push(shift);
1147 parent_codes.copy_from_slice(&codes);
1148 }
1149 widths.reverse();
1150 (codes, widths)
1151}
1152
1153pub(crate) fn build_tree(
1166 data_vec: &SparseIoVec,
1167 csc: &CscMatrix<f32>,
1168 node_of_cell: &[usize],
1169 reference: &[usize],
1170 level_targets: &[usize],
1171 params: &PbTreeParams,
1172) -> anyhow::Result<(Vec<usize>, Vec<usize>, PbTree)> {
1173 let n = data_vec.num_columns();
1174 anyhow::ensure!(
1175 node_of_cell.len() == n,
1176 "node labels for {} of {n} cells",
1177 node_of_cell.len()
1178 );
1179 anyhow::ensure!(csc.ncols() == n, "counts for {} of {n} cells", csc.ncols());
1180 let col_to_batch = data_vec.get_batch_membership(0..n);
1181 let num_batches = data_vec.num_batches().max(1);
1182 let mut by_node: HashMap<usize, Vec<usize>> = HashMap::default();
1183 for (c, &nd) in node_of_cell.iter().enumerate() {
1184 if nd != usize::MAX {
1185 by_node.entry(nd).or_default().push(c);
1186 }
1187 }
1188 let mut nodes: Vec<(usize, Vec<usize>)> = by_node.into_iter().collect();
1189 nodes.sort_by_key(|x| x.0);
1190 let roots: Vec<RootBlock> = nodes
1191 .into_par_iter()
1192 .map(|(root, cells)| {
1193 let block = select_columns(csc, &cells);
1194 let batch: Vec<usize> = cells.iter().map(|&c| col_to_batch[c]).collect();
1195 RootBlock {
1196 root,
1197 cells,
1198 block: NodeBlock::new(block, batch, num_batches),
1199 }
1200 })
1201 .collect();
1202 let mut out = bisect_to_target(&roots, n, level_targets, params);
1203 let finest = out
1204 .labels_per_level
1205 .last()
1206 .cloned()
1207 .unwrap_or_else(|| vec![usize::MAX; n]);
1208 node_ve_ratios(&roots, &finest, reference, params, &mut out.tree);
1209 let (codes, widths) = pack_levels(&out.labels_per_level);
1210 out.tree.coarse_bits = *widths.last().unwrap_or(&0);
1211 log_tree(&out.tree, level_targets, &out.labels_per_level);
1212 Ok((codes, widths, out.tree))
1213}
1214
1215pub(crate) fn select_columns(csc: &CscMatrix<f32>, cols: &[usize]) -> CscMatrix<f32> {
1217 let nnz: usize = cols.iter().map(|&c| csc.col(c).nnz()).sum();
1218 let mut offsets = Vec::with_capacity(cols.len() + 1);
1219 let mut rows = Vec::with_capacity(nnz);
1220 let mut vals = Vec::with_capacity(nnz);
1221 offsets.push(0);
1222 for &c in cols {
1223 let col = csc.col(c);
1224 rows.extend_from_slice(col.row_indices());
1225 vals.extend_from_slice(col.values());
1226 offsets.push(rows.len());
1227 }
1228 CscMatrix::try_from_csc_data(csc.nrows(), cols.len(), offsets, rows, vals)
1229 .expect("columns of a valid CSC form a valid CSC")
1230}
1231
1232fn log_tree(tree: &PbTree, targets: &[usize], levels: &[Vec<usize>]) {
1233 let mut per_depth: Vec<(usize, usize)> = vec![(0, 0); tree.depth];
1234 let mut ratios: Vec<f64> = Vec::new();
1235 let mut ve_res: Vec<f64> = Vec::new();
1236 let mut ve_marg: Vec<f64> = Vec::new();
1237 let mut applied = 0usize;
1238 let mut total = 0usize;
1239 for node in &tree.roots {
1240 ve_res.push(f64::from(node.ve_ratio_residual));
1241 ve_marg.push(f64::from(node.ve_ratio_marginal));
1242 for s in &node.splits {
1243 total += 1;
1244 applied += usize::from(s.applied);
1245 if s.applied && s.depth >= 1 && s.depth <= tree.depth {
1246 per_depth[s.depth - 1].1 += 1;
1247 per_depth[s.depth - 1].0 += usize::from(s.passes_edge);
1248 }
1249 if s.mp_edge > 0.0 {
1250 ratios.push(f64::from(s.s1 / s.mp_edge));
1251 }
1252 }
1253 }
1254 let by_depth: Vec<String> = per_depth
1255 .iter()
1256 .enumerate()
1257 .map(|(d, (a, n))| format!("d{} {}/{}", d + 1, a, n))
1258 .collect();
1259 let reached: Vec<usize> = levels
1260 .iter()
1261 .map(|l| {
1262 l.iter()
1263 .filter(|&&x| x != usize::MAX)
1264 .collect::<std::collections::HashSet<_>>()
1265 .len()
1266 })
1267 .collect();
1268 info!(
1269 "pb tree: {} roots, max depth {}, {} splits proposed, {} applied; leaves per level {:?} for targets {:?}; \
1270 applied splits above the noise edge by depth: {}; median s1/edge {:.2}; \
1271 variance-explained ratio residual {:.2} vs marginal {:.2} (median over roots, 1.0 = random)",
1272 tree.roots.len(),
1273 tree.depth,
1274 total,
1275 applied,
1276 reached,
1277 targets,
1278 by_depth.join(", "),
1279 median(&mut ratios),
1280 median(&mut ve_res),
1281 median(&mut ve_marg),
1282 );
1283}
1284
1285#[cfg(test)]
1286#[path = "pb_tree_tests.rs"]
1287mod tests;