1use super::*;
19use nalgebra::DVector;
20
21pub(super) struct KnnParams<'a> {
22 pub(super) knn_batches: usize,
23 pub(super) knn_cells: usize,
24 pub(super) reference_indices: Option<&'a [usize]>,
25}
26
27pub(super) fn collect_matched_stat_visitor(
28 sample: usize,
29 cells: &[usize],
30 data_vec: &SparseIoVec,
31 knn_params: &KnnParams,
32 arc_stat: Arc<Mutex<&mut CollapsedStat>>,
33) -> anyhow::Result<()> {
34 let knn_batches = knn_params.knn_batches;
35 let knn_cells = knn_params.knn_cells;
36
37 let (y0_matched, source_columns, matched_columns, euclidean_distances) =
38 match knn_params.reference_indices {
39 Some(target_indices) => data_vec.read_matched_columns_with_targets_csc(
40 cells.iter().cloned(),
41 target_indices,
42 knn_cells,
43 true,
44 )?,
45 None => data_vec.read_neighbouring_columns_csc(
46 cells.iter().cloned(),
47 knn_batches,
48 knn_cells,
49 true,
50 None,
51 )?,
52 };
53
54 let y1_pos: HashMap<_, _> = cells
55 .iter()
56 .cloned()
57 .enumerate()
58 .map(|(i, p)| (p, i))
59 .collect();
60
61 let neg_distance_triplets = source_columns
62 .iter()
63 .zip(euclidean_distances.iter())
64 .enumerate()
65 .map(|(t, (&s, &d))| (t, y1_pos[&s], -d))
66 .collect::<Vec<_>>();
67
68 let ww = CscMat::from_nonzero_triplets(
77 y0_matched.ncols(),
78 cells.len(),
79 neg_distance_triplets.as_ref(),
80 )?
81 .normalize_exp_logits_columns();
82
83 let y1_hat = &y0_matched * &ww;
84 let matched_batches = data_vec.get_batch_membership(matched_columns.iter().cloned());
85
86 let mut stat = arc_stat.lock().expect("lock stat");
87
88 for w_j in ww.col_iter() {
92 for (&k, &w) in w_j.row_indices().iter().zip(w_j.values().iter()) {
93 stat.matched_bs[(matched_batches[k], sample)] += w;
94 }
95 }
96
97 for y_j in y1_hat.col_iter() {
98 let rows = y_j.row_indices();
99 let vals = y_j.values();
100 for (&gene, &y) in rows.iter().zip(vals.iter()) {
101 stat.imputed_sum_ds[(gene, sample)] += y;
102 }
103 }
104
105 Ok(())
106}
107
108pub(super) fn collect_basic_stat_visitor(
109 sample: usize,
110 cells: &[usize],
111 data_vec: &SparseIoVec,
112 _: &EmptyArg,
113 arc_stat: Arc<Mutex<&mut CollapsedStat>>,
114) -> anyhow::Result<()> {
115 let yy = data_vec.read_columns_csc(cells.iter().cloned())?;
116
117 let mut stat = arc_stat.lock().expect("lock stat");
118
119 for (y_j, &col) in yy.col_iter().zip(cells.iter()) {
123 let w = data_vec.column_multiplicity(col);
124 let rows = y_j.row_indices();
125 let vals = y_j.values();
126 for (&gene, &y) in rows.iter().zip(vals.iter()) {
127 stat.observed_sum_ds[(gene, sample)] += y * w;
128 }
129 stat.size_s[sample] += w;
130 }
131 Ok(())
132}
133
134pub(super) fn collect_batch_stat_visitor(
135 sample: usize,
136 cells_in_sample: &[usize],
137 data_vec: &SparseIoVec,
138 _: &EmptyArg,
139 arc_stat: Arc<Mutex<&mut CollapsedStat>>,
140) -> anyhow::Result<()> {
141 let yy = data_vec.read_columns_csc(cells_in_sample.iter().cloned())?;
142
143 let batches = data_vec.get_batch_membership(cells_in_sample.iter().cloned());
146
147 let mut stat = arc_stat.lock().expect("lock stat");
148
149 yy.col_iter()
150 .zip(batches.iter())
151 .zip(cells_in_sample.iter())
152 .for_each(|((y_j, &b), &col)| {
153 let w = data_vec.column_multiplicity(col);
154 let rows = y_j.row_indices();
155 let vals = y_j.values();
156 for (&gene, &y) in rows.iter().zip(vals.iter()) {
157 stat.observed_sum_db[(gene, b)] += y * w;
158 }
159 stat.n_bs[(b, sample)] += w;
160 });
161 Ok(())
162}
163
164fn add_effective_size(denom_ds: &mut nalgebra::DMatrix<f32>, stat: &CollapsedStat) {
175 match stat.size_ds.as_ref() {
176 Some(size_ds) => {
177 debug_assert_eq!(denom_ds.shape(), size_ds.shape());
178 *denom_ds += size_ds;
179 }
180 None => {
181 for s in 0..denom_ds.ncols() {
182 denom_ds.column_mut(s).add_scalar_mut(stat.size_s[s]);
183 }
184 }
185 }
186}
187
188fn pin_delta_scale(
193 num_db: &DMatrix<f32>,
194 den_db: &DMatrix<f32>,
195 (a0, b0): (f32, f32),
196 frame_w: &DVector<f32>,
197 mask_db: Option<&DMatrix<f32>>,
198) -> DMatrix<f32> {
199 let (ng, nb) = num_db.shape();
200 let mut out = den_db.clone();
201 for g in 0..ng {
202 let (mut lsum, mut wsum) = (0f64, 0f64);
203 for b in 0..nb {
204 let w = f64::from(frame_w[b]) * mask_db.map_or(1.0, |m| f64::from(m[(g, b)]));
205 if w > 0.0 {
206 let m = f64::from(a0 + num_db[(g, b)]) / f64::from(b0 + den_db[(g, b)]);
207 lsum += w * m.max(f64::MIN_POSITIVE).ln();
208 wsum += w;
209 }
210 }
211 if wsum > 0.0 {
212 let gm = (lsum / wsum).exp() as f32;
213 for b in 0..nb {
214 if mask_db.is_some_and(|m| m[(g, b)] == 0.0) {
216 continue;
217 }
218 out[(g, b)] = ((b0 + den_db[(g, b)]) * gm - b0).max(0.0);
222 }
223 }
224 }
225 out
226}
227
228fn optimize_block(
229 stat: &CollapsedStat,
230 hyper: (f32, f32),
231 num_iter: usize,
232 out_target: CalibrateTarget,
233 prog: Option<&indicatif::ProgressBar>,
234) -> anyhow::Result<CollapsedOut> {
235 let (a0, b0) = hyper;
236 let num_genes = stat.num_genes();
237 let num_samples = stat.num_samples();
238 let num_batches = stat.num_batches();
239 let mut mu_param = GammaMatrix::new((num_genes, num_samples), a0, b0);
240
241 if num_batches > 1 {
242 let n_bs = &stat.n_bs;
256 let w_bs = &stat.matched_bs;
257 let own_plus_src = n_bs + w_bs; let (obs_db_for_delta, own_plus_src_for_delta, n_bs_d) = if stat
263 .exclude_unmatched_from_delta
264 {
265 let mut obs = stat.observed_sum_db.clone();
266 let mut n_bs_d = n_bs.clone();
267 for s in 0..num_samples {
268 if w_bs.column(s).sum() > 0.0 {
269 continue;
270 }
271 let size = stat.size_s[s];
272 if size <= 0.0 {
273 continue;
274 }
275 for b in 0..num_batches {
276 let n = n_bs[(b, s)];
277 if n <= 0.0 {
278 continue;
279 }
280 let frac = n / size;
285 for g in 0..num_genes {
286 obs[(g, b)] = (obs[(g, b)] - stat.observed_sum_ds[(g, s)] * frac).max(0.0);
287 }
288 n_bs_d[(b, s)] = 0.0;
289 }
290 }
291 let own_plus = &n_bs_d + w_bs;
292 (obs, own_plus, n_bs_d)
293 } else {
294 (
295 stat.observed_sum_db.clone(),
296 own_plus_src.clone(),
297 n_bs.clone(),
298 )
299 };
300 let obs_plus_imp = &stat.observed_sum_ds + &stat.imputed_sum_ds;
301 let obs_frac: Option<DMatrix<f32>> = stat.size_ds.as_ref().map(|size_ds| {
304 DMatrix::from_fn(num_genes, num_samples, |g, s| {
305 let n = stat.size_s[s];
306 if n > 0.0 {
307 size_ds[(g, s)] / n
308 } else {
309 0.0
310 }
311 })
312 });
313 let frame_w = if stat.exclude_unmatched_from_delta && stat.anchor_batches.is_empty() {
314 DVector::from_fn(num_batches, |b, _| n_bs_d.row(b).sum())
317 } else {
318 stat.frame_weights()
319 };
320 for b in 0..num_batches {
321 if frame_w[b] <= 0.0 {
322 warn!(
323 "batch {b} has no matched mass for δ (frame weight 0); \
324 δ_·{b} stays on prior ≈ 1 (e.g. blast-only / clone-only donor)"
325 );
326 }
327 }
328 let own_plus_src_delta_t = own_plus_src_for_delta.transpose();
329 let w_bs_t = w_bs.transpose();
330
331 let mut mu_adj_param = GammaMatrix::new((num_genes, num_samples), a0, b0);
332 let mut delta_param = GammaMatrix::new((num_genes, num_batches), a0, b0);
333 let mut delta_gb = DMatrix::<f32>::from_element(num_genes, num_batches, 1.0);
334
335 let update_mu = |mu_adj_param: &mut GammaMatrix, delta_gb: &DMatrix<f32>| {
337 let mut denom_ds = delta_gb * &own_plus_src;
338 if let Some(f) = obs_frac.as_ref() {
339 denom_ds.component_mul_assign(f);
340 }
341 mu_adj_param.update_stat(&obs_plus_imp, &denom_ds);
342 mu_adj_param.calibrate_with(CalibrateTarget::MeanOnly);
343 };
344
345 let mut imp_share = DMatrix::<f32>::zeros(num_genes, num_samples);
348 let mut mu_frac = DMatrix::<f32>::zeros(num_genes, num_samples);
349 for _opt_iter in 0..num_iter {
350 #[cfg(debug_assertions)]
351 {
352 debug!("iteration: {}", _opt_iter);
353 }
354
355 update_mu(&mut mu_adj_param, &delta_gb);
356 let mu_ds = mu_adj_param.posterior_mean();
357
358 imp_share.gemm(1.0, &delta_gb, w_bs, 0.0); imp_share.zip_apply(&stat.imputed_sum_ds, |z, x| {
366 *z = if *z > 0.0 { x / *z } else { 0.0 };
367 });
368 let mut num_db = &obs_db_for_delta + (&imp_share * &w_bs_t).component_mul(&delta_gb);
369 let mu_frac_ref: &DMatrix<f32> = match obs_frac.as_ref() {
370 Some(f) => {
371 mu_frac.copy_from(mu_ds);
372 mu_frac.component_mul_assign(f);
373 &mu_frac
374 }
375 None => mu_ds,
376 };
377 let mut den_db = mu_frac_ref * &own_plus_src_delta_t; if let Some(mask) = stat.obs_mask_db.as_ref() {
379 num_db.component_mul_assign(mask);
383 den_db.component_mul_assign(mask);
384 }
385 let den_db = pin_delta_scale(
389 &num_db,
390 &den_db,
391 (a0, b0),
392 &frame_w,
393 stat.obs_mask_db.as_ref(),
394 );
395 delta_param.update_stat(&num_db, &den_db);
396 delta_param.calibrate_with(CalibrateTarget::MeanOnly);
397 delta_gb.copy_from(delta_param.posterior_mean());
398
399 if let Some(p) = prog {
400 p.inc(1);
401 }
402 }
403 update_mu(&mut mu_adj_param, &delta_gb);
405 mu_adj_param.calibrate_with(out_target);
406 delta_param.calibrate_with(out_target);
407
408 let mu_ds = mu_adj_param.posterior_mean();
414 let mass_times_mu = |per_sample: &dyn Fn(usize) -> f32| {
415 let mut m = mu_ds.clone();
416 for s in 0..num_samples {
417 m.column_mut(s).scale_mut(per_sample(s));
418 }
419 if let Some(f) = obs_frac.as_ref() {
420 m.component_mul_assign(f);
421 }
422 m
423 };
424 let resid_denom = mass_times_mu(&|s| stat.size_s[s]);
425 let mut mu_resid_param = GammaMatrix::new((num_genes, num_samples), a0, b0);
426 mu_resid_param.update_stat(&stat.observed_sum_ds, &resid_denom);
427 mu_resid_param.calibrate_with(out_target);
428
429 let src_mass_s: Vec<f32> = (0..num_samples).map(|s| w_bs.column(s).sum()).collect();
430 let gamma_denom = mass_times_mu(&|s| src_mass_s[s]);
431 let mut gamma_param = GammaMatrix::new((num_genes, num_samples), a0, b0);
432 gamma_param.update_stat(&stat.imputed_sum_ds, &gamma_denom);
433 gamma_param.calibrate_with(out_target);
434
435 {
437 let mut own_mass = DMatrix::<f32>::zeros(num_genes, num_samples);
438 add_effective_size(&mut own_mass, stat);
439 mu_param.update_stat(&stat.observed_sum_ds, &own_mass);
440 mu_param.calibrate_with(out_target);
441 };
442
443 if matches!(out_target, CalibrateTarget::MeanOnly) {
448 mu_param.sparsify_mean_to_support(&stat.observed_sum_ds);
449 mu_adj_param.sparsify_mean_to_support(&obs_plus_imp);
450 gamma_param.sparsify_mean_to_support(&stat.imputed_sum_ds);
451 mu_resid_param.sparsify_mean_to_support(&stat.observed_sum_ds);
452 }
453
454 Ok(CollapsedOut {
455 mu_observed: mu_param,
456 mu_adjusted: Some(mu_adj_param),
457 mu_residual: Some(mu_resid_param),
458 gamma: Some(gamma_param),
459 delta: Some(delta_param),
460 stats_kept: true,
461 })
462 } else {
463 let mut denom_ds = DMatrix::<f32>::zeros(num_genes, num_samples);
464 add_effective_size(&mut denom_ds, stat);
465 mu_param.update_stat(&stat.observed_sum_ds, &denom_ds);
466 mu_param.calibrate_with(out_target);
467 if matches!(out_target, CalibrateTarget::MeanOnly) {
468 mu_param.sparsify_mean_to_support(&stat.observed_sum_ds);
469 }
470 Ok(CollapsedOut {
471 mu_observed: mu_param,
472 mu_adjusted: None,
473 mu_residual: None,
474 gamma: None,
475 delta: None,
476 stats_kept: true,
477 })
478 }
479}
480
481pub(super) fn optimize(
490 stat: &CollapsedStat,
491 hyper: (f32, f32),
492 num_iter: usize,
493 label: &str,
494 out_target: CalibrateTarget,
495 keep_stats: bool,
498) -> anyhow::Result<CollapsedOut> {
499 let num_genes = stat.num_genes();
500 let num_samples = stat.num_samples();
501 let num_batches = stat.num_batches();
502
503 const BLOCK_ELEMS: usize = 2_000_000;
514 let block_rows = (BLOCK_ELEMS / num_samples.max(1)).clamp(1, num_genes.max(1));
515 let jobs = create_jobs(num_genes, num_samples, Some(block_rows));
518 let n_blocks = jobs.len();
519
520 let dims = format!("{num_genes} genes × {num_samples} samples");
521
522 let keep_stats = keep_stats || matches!(out_target, CalibrateTarget::All);
526
527 let batched = num_batches > 1;
534 let total = if batched {
535 n_blocks * num_iter
536 } else {
537 n_blocks
538 };
539 let msg = if batched {
540 format!("{label} opt-iters · {dims} · {n_blocks} blocks")
541 } else {
542 format!("{label} gene-blocks · {dims}")
543 };
544
545 if n_blocks <= 1 {
550 if batched {
551 let prog = styled_progress_bar(total as u64, &msg);
552 let mut out = optimize_block(stat, hyper, num_iter, out_target, Some(&prog))?;
553 prog.finish_and_clear();
554 if !keep_stats {
555 out.release_stats();
556 }
557 return Ok(out);
558 }
559 let spin =
560 legume_numeric::matrix::progress::new_spinner("{spinner} [{elapsed_precise}] {msg}")
561 .with_message(format!("{label} single gene-block · {dims}"));
562 let mut out = optimize_block(stat, hyper, num_iter, out_target, None)?;
563 spin.finish_and_clear();
564 if !keep_stats {
565 out.release_stats();
566 }
567 return Ok(out);
568 }
569
570 let prog = styled_progress_bar(total as u64, &msg);
576
577 let outs = jobs
581 .par_iter()
582 .map(|&(lb, ub)| -> anyhow::Result<CollapsedOut> {
583 let sub = stat.select_rows(lb, ub - lb);
584 let mut out_b =
585 optimize_block(&sub, hyper, num_iter, out_target, batched.then_some(&prog))?;
586 if !keep_stats {
587 out_b.release_stats();
590 }
591 if !batched {
592 prog.inc(1);
593 }
594 Ok(out_b)
595 })
596 .collect::<anyhow::Result<Vec<_>>>()?;
597 prog.finish_and_clear();
598
599 let mut mu_obs: Vec<GammaMatrix> = Vec::with_capacity(n_blocks);
600 let mut mu_adj: Vec<GammaMatrix> = Vec::new();
601 let mut mu_res: Vec<GammaMatrix> = Vec::new();
602 let mut gam: Vec<GammaMatrix> = Vec::new();
603 let mut del: Vec<GammaMatrix> = Vec::new();
604 for out_b in outs {
605 mu_obs.push(out_b.mu_observed);
606 if let Some(x) = out_b.mu_adjusted {
607 mu_adj.push(x);
608 }
609 if let Some(x) = out_b.mu_residual {
610 mu_res.push(x);
611 }
612 if let Some(x) = out_b.gamma {
613 gam.push(x);
614 }
615 if let Some(x) = out_b.delta {
616 del.push(x);
617 }
618 }
619
620 let join = |v: Vec<GammaMatrix>| -> Option<GammaMatrix> {
621 (!v.is_empty()).then(|| GammaMatrix::vconcat(v, keep_stats))
622 };
623 Ok(CollapsedOut {
624 mu_observed: GammaMatrix::vconcat(mu_obs, keep_stats),
625 mu_adjusted: join(mu_adj),
626 mu_residual: join(mu_res),
627 gamma: join(gam),
628 delta: join(del),
629 stats_kept: keep_stats,
630 })
631}
632
633#[derive(Debug, Clone)]
635pub struct CollapsedOut {
636 pub mu_observed: GammaMatrix,
637 pub mu_adjusted: Option<GammaMatrix>,
638 pub mu_residual: Option<GammaMatrix>,
639 pub gamma: Option<GammaMatrix>,
640 pub delta: Option<GammaMatrix>,
641 pub stats_kept: bool,
645}
646
647impl CollapsedOut {
648 fn release_stats(&mut self) {
653 self.mu_observed.release_stats();
654 for p in [
655 &mut self.mu_adjusted,
656 &mut self.mu_residual,
657 &mut self.gamma,
658 &mut self.delta,
659 ] {
660 if let Some(g) = p.as_mut() {
661 g.release_stats();
662 }
663 }
664 self.stats_kept = false;
665 }
666
667 pub fn observed_counts(
680 &self,
681 cell_to_pb: &[usize],
682 ) -> anyhow::Result<(DMatrix<f32>, Vec<f32>)> {
683 anyhow::ensure!(
684 self.stats_kept,
685 "CollapsedOut::observed_counts needs kept sufficient statistics; \
686 set MultilevelParams::keep_finest_stats"
687 );
688 let param = &self.mu_observed;
689 let (d, n_pb) = (param.nrows(), param.ncols());
690 let mut sizes = vec![0f32; n_pb];
691 let mut n_oob = 0usize;
692 for &pb in cell_to_pb {
693 if pb < n_pb {
694 sizes[pb] += 1.0;
695 } else {
696 n_oob += 1;
697 }
698 }
699 anyhow::ensure!(
700 n_oob == 0,
701 "CollapsedOut::observed_counts: {n_oob} cell→pseudobulk id(s) \
702 are out of range for {n_pb} pseudobulk(s)"
703 );
704 let counts = DMatrix::<f32>::from_fn(d, n_pb, |g, s| param.evidence_mean(g, s) * sizes[s]);
705 Ok((counts, sizes))
706 }
707}
708
709#[derive(Debug, Clone)]
711pub struct CollapsedStat {
712 pub observed_sum_ds: nalgebra::DMatrix<f32>, pub imputed_sum_ds: nalgebra::DMatrix<f32>, pub size_s: nalgebra::DVector<f32>, pub observed_sum_db: nalgebra::DMatrix<f32>, pub n_bs: nalgebra::DMatrix<f32>, pub matched_bs: nalgebra::DMatrix<f32>,
723 pub anchor_batches: Vec<usize>,
726 pub size_ds: Option<nalgebra::DMatrix<f32>>,
736 pub obs_mask_db: Option<nalgebra::DMatrix<f32>>,
741 pub exclude_unmatched_from_delta: bool,
746}
747
748impl CollapsedStat {
749 pub fn new(ngene: usize, nsample: usize, nbatch: usize) -> Self {
750 Self {
751 observed_sum_ds: nalgebra::DMatrix::<f32>::zeros(ngene, nsample),
752 imputed_sum_ds: nalgebra::DMatrix::<f32>::zeros(ngene, nsample),
753 size_s: nalgebra::DVector::<f32>::zeros(nsample),
754 observed_sum_db: nalgebra::DMatrix::<f32>::zeros(ngene, nbatch),
755 n_bs: nalgebra::DMatrix::<f32>::zeros(nbatch, nsample),
756 matched_bs: nalgebra::DMatrix::<f32>::zeros(nbatch, nsample),
757 anchor_batches: Vec::new(),
758 size_ds: None,
759 obs_mask_db: None,
760 exclude_unmatched_from_delta: false,
761 }
762 }
763
764 pub fn num_genes(&self) -> usize {
765 self.observed_sum_ds.nrows()
766 }
767
768 pub fn num_samples(&self) -> usize {
769 self.observed_sum_ds.ncols()
770 }
771
772 pub fn num_batches(&self) -> usize {
773 self.observed_sum_db.ncols()
774 }
775
776 pub fn frame_weights(&self) -> DVector<f32> {
780 let nb = self.num_batches();
781 if self.anchor_batches.is_empty() {
782 DVector::from_fn(nb, |b, _| self.n_bs.row(b).sum())
783 } else {
784 let mut w = DVector::<f32>::zeros(nb);
785 for &b in &self.anchor_batches {
786 w[b] = 1.0;
787 }
788 w
789 }
790 }
791
792 pub fn clear(&mut self) {
793 self.observed_sum_ds.fill(0_f32);
794 self.imputed_sum_ds.fill(0_f32);
795 self.observed_sum_db.fill(0_f32);
796 self.size_s.fill(0_f32);
797 self.n_bs.fill(0_f32);
798 self.matched_bs.fill(0_f32);
799 self.anchor_batches.clear();
800 self.size_ds = None;
801 self.obs_mask_db = None;
802 }
803
804 pub fn select_columns(&self, indices: &[usize]) -> Self {
806 let n_new = indices.len();
807 let ng = self.num_genes();
808 let nb = self.num_batches();
809 let mut out = Self::new(ng, n_new, nb);
810 for (new_col, &old_col) in indices.iter().enumerate() {
811 out.observed_sum_ds
812 .column_mut(new_col)
813 .copy_from(&self.observed_sum_ds.column(old_col));
814 out.imputed_sum_ds
815 .column_mut(new_col)
816 .copy_from(&self.imputed_sum_ds.column(old_col));
817 out.size_s[new_col] = self.size_s[old_col];
818 for b in 0..nb {
819 out.n_bs[(b, new_col)] = self.n_bs[(b, old_col)];
820 out.matched_bs[(b, new_col)] = self.matched_bs[(b, old_col)];
821 }
822 }
823 if let Some(size_ds) = self.size_ds.as_ref() {
824 out.size_ds = Some(nalgebra::DMatrix::from_fn(ng, n_new, |g, new_col| {
825 size_ds[(g, indices[new_col])]
826 }));
827 }
828 out.obs_mask_db = self.obs_mask_db.clone();
829 out.observed_sum_db.copy_from(&self.observed_sum_db);
830 out.anchor_batches = self.anchor_batches.clone();
831 out.exclude_unmatched_from_delta = self.exclude_unmatched_from_delta;
832 out
833 }
834
835 pub fn select_rows(&self, r0: usize, nrows: usize) -> Self {
840 Self {
841 observed_sum_ds: self.observed_sum_ds.rows(r0, nrows).into_owned(),
842 imputed_sum_ds: self.imputed_sum_ds.rows(r0, nrows).into_owned(),
843 size_s: self.size_s.clone(),
844 observed_sum_db: self.observed_sum_db.rows(r0, nrows).into_owned(),
845 n_bs: self.n_bs.clone(),
846 matched_bs: self.matched_bs.clone(),
847 anchor_batches: self.anchor_batches.clone(),
848 size_ds: self
849 .size_ds
850 .as_ref()
851 .map(|m| m.rows(r0, nrows).into_owned()),
852 obs_mask_db: self
853 .obs_mask_db
854 .as_ref()
855 .map(|m| m.rows(r0, nrows).into_owned()),
856 exclude_unmatched_from_delta: self.exclude_unmatched_from_delta,
857 }
858 }
859}
860
861pub fn resample_and_optimize(
864 stat: &CollapsedStat,
865 rng: &mut impl rand::Rng,
866 opt_iter: usize,
867) -> anyhow::Result<CollapsedOut> {
868 use rand::seq::SliceRandom;
869 let n = stat.num_samples();
870 let target = n / 2;
871 let mut indices: Vec<usize> = (0..n).collect();
872 indices.shuffle(rng);
873 indices.truncate(target);
874 indices.sort_unstable();
875 let sub_stat = stat.select_columns(&indices);
876 optimize(
877 &sub_stat,
878 (1.0, 1.0),
879 opt_iter,
880 "Optimizing",
881 CalibrateTarget::All,
882 false,
883 )
884}
885
886pub(super) const DEFAULT_NUM_LEVELS: usize = 2;
891pub(super) const DEFAULT_COARSEST_SORT_DIM: usize = 7;
892
893fn pbsamps_by_group(pbsamp_to_group: &[usize], num_groups: usize) -> Vec<Vec<usize>> {
896 let mut out: Vec<Vec<usize>> = vec![Vec::new(); num_groups];
897 for (pbsamp, &g) in pbsamp_to_group.iter().enumerate() {
898 out[g].push(pbsamp);
899 }
900 out
901}
902
903pub(super) fn collect_observed_stat_from_pb_samples(
911 data_vec: &SparseIoVec,
912 layout: &PbSampleLayout,
913 gene_sums: &[Vec<(usize, f32)>],
914 pbsamp_to_group: &[usize],
915 stat: &mut CollapsedStat,
916) -> anyhow::Result<()> {
917 let num_pb = layout.cell_counts.len();
918 let num_genes = stat.num_genes();
919 let num_groups = stat.num_samples();
920 let num_batches = stat.num_batches();
921 anyhow::ensure!(
922 gene_sums.len() == num_pb && pbsamp_to_group.len() == num_pb,
923 "{} gene sums and {} group labels for {num_pb} pb-samples",
924 gene_sums.len(),
925 pbsamp_to_group.len(),
926 );
927 let with_batches = num_batches >= 2;
928
929 for (c, &pbsamp) in layout.cell_to_pbsamp.iter().enumerate() {
931 if pbsamp == usize::MAX {
932 continue;
933 }
934 let w = data_vec.column_multiplicity(c);
935 let g = pbsamp_to_group[pbsamp];
936 stat.size_s[g] += w;
937 if with_batches {
938 stat.n_bs[(layout.pb_sample_to_batch[pbsamp], g)] += w;
939 }
940 }
941 if num_genes == 0 {
942 return Ok(());
943 }
944
945 let add_gene_sums = |column: &mut [f32], pbsamps: &Vec<usize>| {
946 for &pbsamp in pbsamps {
947 for &(gene, val) in &gene_sums[pbsamp] {
948 column[gene] += val;
949 }
950 }
951 };
952 stat.observed_sum_ds
954 .as_mut_slice()
955 .par_chunks_mut(num_genes)
956 .zip(pbsamps_by_group(pbsamp_to_group, num_groups).par_iter())
957 .for_each(|(column, pbsamps)| add_gene_sums(column, pbsamps));
958 if with_batches {
959 stat.observed_sum_db
960 .as_mut_slice()
961 .par_chunks_mut(num_genes)
962 .zip(pbsamps_by_group(&layout.pb_sample_to_batch, num_batches).par_iter())
963 .for_each(|(column, pbsamps)| add_gene_sums(column, pbsamps));
964 }
965 Ok(())
966}
967
968pub(super) fn collect_matched_stat_coarse(
975 layout: &PbSampleLayout,
976 gene_sums: &[Vec<(usize, f32)>],
977 pbsamp_to_group: &[usize],
978 matches: &[Vec<(usize, f32)>],
979 anchor_batches: Option<&[usize]>,
980 stat: &mut CollapsedStat,
981) -> anyhow::Result<()> {
982 let num_pb = layout.cell_counts.len();
983 debug_assert_eq!(pbsamp_to_group.len(), num_pb);
984 anyhow::ensure!(
985 matches.len() == num_pb,
986 "{} match lists for {num_pb} pb-samples",
987 matches.len(),
988 );
989
990 stat.anchor_batches = anchor_batches.map(<[usize]>::to_vec).unwrap_or_default();
993
994 let num_genes = stat.num_genes();
995 let num_batches = stat.num_batches();
996 if num_genes == 0 || num_batches == 0 {
997 return Ok(());
998 }
999 let group_to_pbsamps = pbsamps_by_group(pbsamp_to_group, stat.num_samples());
1000
1001 let prog_bar = styled_progress_bar(num_pb as u64, "pb-samples (matched stats)");
1002
1003 stat.imputed_sum_ds
1008 .as_mut_slice()
1009 .par_chunks_mut(num_genes)
1010 .zip(stat.matched_bs.as_mut_slice().par_chunks_mut(num_batches))
1011 .zip(group_to_pbsamps.par_iter())
1012 .for_each_init(
1013 || (vec![0f32; num_genes], Vec::<usize>::new()),
1014 |(y_hat, touched), ((imputed_s, matched_s), pbsamps)| {
1015 for &pbsamp_idx in pbsamps {
1016 let sc_count = layout.cell_counts[pbsamp_idx];
1017 let filtered = &matches[pbsamp_idx];
1018 if sc_count < 1.0 || filtered.is_empty() {
1019 continue;
1020 }
1021
1022 let max_neg_d = filtered
1024 .iter()
1025 .map(|(_, d)| -d)
1026 .fold(f32::NEG_INFINITY, f32::max);
1027 let mut weights: Vec<f32> = filtered
1028 .iter()
1029 .map(|(_, d)| (-d - max_neg_d).exp())
1030 .collect();
1031 let w_sum: f32 = weights.iter().sum();
1032 if w_sum > 0.0 {
1033 weights.iter_mut().for_each(|w| *w /= w_sum);
1034 }
1035
1036 for ((matched_sc, _), &w) in filtered.iter().zip(weights.iter()) {
1040 let matched_count = layout.cell_counts[*matched_sc];
1041 if matched_count < 1.0 {
1042 continue;
1043 }
1044 let inv_count = 1.0 / matched_count;
1045 for &(gene, val) in &gene_sums[*matched_sc] {
1046 if y_hat[gene] == 0.0 {
1049 touched.push(gene);
1050 }
1051 y_hat[gene] += w * val * inv_count;
1052 }
1053 }
1054
1055 for gene in touched.drain(..) {
1057 imputed_s[gene] += sc_count * std::mem::take(&mut y_hat[gene]);
1058 }
1059
1060 for ((matched_sc, _), &w) in filtered.iter().zip(weights.iter()) {
1064 if layout.cell_counts[*matched_sc] < 1.0 {
1065 continue;
1066 }
1067 matched_s[layout.pb_sample_to_batch[*matched_sc]] += sc_count * w;
1068 }
1069 }
1070 prog_bar.inc(pbsamps.len() as u64);
1071 },
1072 );
1073
1074 prog_bar.finish_and_clear();
1075 Ok(())
1076}
1077
1078pub(super) fn merge_stat(
1083 fine_stat: &CollapsedStat,
1084 fine_to_coarse: &[usize],
1085 num_coarse_groups: usize,
1086) -> CollapsedStat {
1087 let num_genes = fine_stat.num_genes();
1088 let num_batches = fine_stat.num_batches();
1089 let mut coarse = CollapsedStat::new(num_genes, num_coarse_groups, num_batches);
1090
1091 for (fine_g, &coarse_g) in fine_to_coarse.iter().enumerate() {
1092 coarse
1093 .observed_sum_ds
1094 .column_mut(coarse_g)
1095 .add_assign(&fine_stat.observed_sum_ds.column(fine_g));
1096 coarse
1097 .imputed_sum_ds
1098 .column_mut(coarse_g)
1099 .add_assign(&fine_stat.imputed_sum_ds.column(fine_g));
1100 coarse.size_s[coarse_g] += fine_stat.size_s[fine_g];
1101 for b in 0..num_batches {
1102 coarse.n_bs[(b, coarse_g)] += fine_stat.n_bs[(b, fine_g)];
1103 coarse.matched_bs[(b, coarse_g)] += fine_stat.matched_bs[(b, fine_g)];
1104 }
1105 }
1106
1107 if let Some(fine_size_ds) = fine_stat.size_ds.as_ref() {
1110 let mut size_ds = nalgebra::DMatrix::<f32>::zeros(num_genes, num_coarse_groups);
1111 for (fine_g, &coarse_g) in fine_to_coarse.iter().enumerate() {
1112 size_ds
1113 .column_mut(coarse_g)
1114 .add_assign(&fine_size_ds.column(fine_g));
1115 }
1116 coarse.size_ds = Some(size_ds);
1117 }
1118 coarse.obs_mask_db = fine_stat.obs_mask_db.clone();
1119 coarse.anchor_batches = fine_stat.anchor_batches.clone();
1120 coarse.exclude_unmatched_from_delta = fine_stat.exclude_unmatched_from_delta;
1121
1122 coarse.observed_sum_db.copy_from(&fine_stat.observed_sum_db);
1123 coarse
1124}
1125
1126#[cfg(test)]
1127#[path = "stats_tests.rs"]
1128mod gene_block_tests;