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, euclidean_distances) = match knn_params.reference_indices {
38 Some(target_indices) => data_vec.read_matched_columns_csc(
39 cells.iter().cloned(),
40 target_indices,
41 knn_cells,
42 true,
43 )?,
44 None => {
45 let (mat, src, _matched, dist) = data_vec.read_neighbouring_columns_csc(
46 cells.iter().cloned(),
47 knn_batches,
48 knn_cells,
49 true,
50 None,
51 )?;
52 (mat, src, dist)
53 }
54 };
55
56 let y1_pos: HashMap<_, _> = cells
57 .iter()
58 .cloned()
59 .enumerate()
60 .map(|(i, p)| (p, i))
61 .collect();
62
63 let neg_distance_triplets = source_columns
64 .iter()
65 .zip(euclidean_distances.iter())
66 .enumerate()
67 .map(|(t, (&s, &d))| (t, y1_pos[&s], -d))
68 .collect::<Vec<_>>();
69
70 let ww = CscMat::from_nonzero_triplets(
79 y0_matched.ncols(),
80 cells.len(),
81 neg_distance_triplets.as_ref(),
82 )?
83 .normalize_exp_logits_columns();
84
85 let y1_hat = &y0_matched * &ww;
86 let source_batches = data_vec.get_batch_membership(source_columns.iter().cloned());
87
88 let mut stat = arc_stat.lock().expect("lock stat");
89
90 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[(source_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) =
263 if stat.exclude_unmatched_from_delta {
264 let mut obs = stat.observed_sum_db.clone();
265 let mut n_bs_d = n_bs.clone();
266 for s in 0..num_samples {
267 if w_bs.column(s).sum() > 0.0 {
268 continue;
269 }
270 let size = stat.size_s[s];
271 if size <= 0.0 {
272 continue;
273 }
274 for b in 0..num_batches {
275 let n = n_bs[(b, s)];
276 if n <= 0.0 {
277 continue;
278 }
279 let frac = n / size;
280 for g in 0..num_genes {
281 obs[(g, b)] -= stat.observed_sum_ds[(g, s)] * frac;
282 }
283 n_bs_d[(b, s)] = 0.0;
284 }
285 }
286 let own_plus = &n_bs_d + w_bs;
287 (obs, own_plus, n_bs_d)
288 } else {
289 (
290 stat.observed_sum_db.clone(),
291 own_plus_src.clone(),
292 n_bs.clone(),
293 )
294 };
295 let obs_plus_imp = &stat.observed_sum_ds + &stat.imputed_sum_ds;
296 let obs_frac: Option<DMatrix<f32>> = stat.size_ds.as_ref().map(|size_ds| {
299 DMatrix::from_fn(num_genes, num_samples, |g, s| {
300 let n = stat.size_s[s];
301 if n > 0.0 {
302 size_ds[(g, s)] / n
303 } else {
304 0.0
305 }
306 })
307 });
308 let frame_w = if stat.exclude_unmatched_from_delta && stat.anchor_batches.is_empty() {
309 DVector::from_fn(num_batches, |b, _| n_bs_d.row(b).sum())
312 } else {
313 stat.frame_weights()
314 };
315 for b in 0..num_batches {
316 if frame_w[b] <= 0.0 {
317 warn!(
318 "batch {b} has no matched mass for δ (frame weight 0); \
319 δ_·{b} stays on prior ≈ 1 (e.g. blast-only / clone-only donor)"
320 );
321 }
322 }
323 let own_plus_src_delta_t = own_plus_src_for_delta.transpose();
324 let w_bs_t = w_bs.transpose();
325
326 let mut mu_adj_param = GammaMatrix::new((num_genes, num_samples), a0, b0);
327 let mut delta_param = GammaMatrix::new((num_genes, num_batches), a0, b0);
328 let mut delta_gb = DMatrix::<f32>::from_element(num_genes, num_batches, 1.0);
329
330 let update_mu = |mu_adj_param: &mut GammaMatrix, delta_gb: &DMatrix<f32>| {
332 let mut denom_ds = delta_gb * &own_plus_src;
333 if let Some(f) = obs_frac.as_ref() {
334 denom_ds.component_mul_assign(f);
335 }
336 mu_adj_param.update_stat(&obs_plus_imp, &denom_ds);
337 mu_adj_param.calibrate_with(CalibrateTarget::MeanOnly);
338 };
339
340 let mut imp_share = DMatrix::<f32>::zeros(num_genes, num_samples);
343 let mut mu_frac = DMatrix::<f32>::zeros(num_genes, num_samples);
344 for _opt_iter in 0..num_iter {
345 #[cfg(debug_assertions)]
346 {
347 debug!("iteration: {}", _opt_iter);
348 }
349
350 update_mu(&mut mu_adj_param, &delta_gb);
351 let mu_ds = mu_adj_param.posterior_mean();
352
353 imp_share.gemm(1.0, &delta_gb, w_bs, 0.0); imp_share.zip_apply(&stat.imputed_sum_ds, |z, x| {
361 *z = if *z > 0.0 { x / *z } else { 0.0 };
362 });
363 let mut num_db = &obs_db_for_delta + (&imp_share * &w_bs_t).component_mul(&delta_gb);
364 let mu_frac_ref: &DMatrix<f32> = match obs_frac.as_ref() {
365 Some(f) => {
366 mu_frac.copy_from(mu_ds);
367 mu_frac.component_mul_assign(f);
368 &mu_frac
369 }
370 None => mu_ds,
371 };
372 let mut den_db = mu_frac_ref * &own_plus_src_delta_t; if let Some(mask) = stat.obs_mask_db.as_ref() {
374 num_db.component_mul_assign(mask);
378 den_db.component_mul_assign(mask);
379 }
380 let den_db = pin_delta_scale(
384 &num_db,
385 &den_db,
386 (a0, b0),
387 &frame_w,
388 stat.obs_mask_db.as_ref(),
389 );
390 delta_param.update_stat(&num_db, &den_db);
391 delta_param.calibrate_with(CalibrateTarget::MeanOnly);
392 delta_gb.copy_from(delta_param.posterior_mean());
393
394 if let Some(p) = prog {
395 p.inc(1);
396 }
397 }
398 update_mu(&mut mu_adj_param, &delta_gb);
400 mu_adj_param.calibrate_with(out_target);
401 delta_param.calibrate_with(out_target);
402
403 let mu_ds = mu_adj_param.posterior_mean();
409 let mass_times_mu = |per_sample: &dyn Fn(usize) -> f32| {
410 let mut m = mu_ds.clone();
411 for s in 0..num_samples {
412 m.column_mut(s).scale_mut(per_sample(s));
413 }
414 if let Some(f) = obs_frac.as_ref() {
415 m.component_mul_assign(f);
416 }
417 m
418 };
419 let resid_denom = mass_times_mu(&|s| stat.size_s[s]);
420 let mut mu_resid_param = GammaMatrix::new((num_genes, num_samples), a0, b0);
421 mu_resid_param.update_stat(&stat.observed_sum_ds, &resid_denom);
422 mu_resid_param.calibrate_with(out_target);
423
424 let src_mass_s: Vec<f32> = (0..num_samples).map(|s| w_bs.column(s).sum()).collect();
425 let gamma_denom = mass_times_mu(&|s| src_mass_s[s]);
426 let mut gamma_param = GammaMatrix::new((num_genes, num_samples), a0, b0);
427 gamma_param.update_stat(&stat.imputed_sum_ds, &gamma_denom);
428 gamma_param.calibrate_with(out_target);
429
430 {
432 let mut own_mass = DMatrix::<f32>::zeros(num_genes, num_samples);
433 add_effective_size(&mut own_mass, stat);
434 mu_param.update_stat(&stat.observed_sum_ds, &own_mass);
435 mu_param.calibrate_with(out_target);
436 };
437
438 if matches!(out_target, CalibrateTarget::MeanOnly) {
443 mu_param.sparsify_mean_to_support(&stat.observed_sum_ds);
444 mu_adj_param.sparsify_mean_to_support(&obs_plus_imp);
445 gamma_param.sparsify_mean_to_support(&stat.imputed_sum_ds);
446 mu_resid_param.sparsify_mean_to_support(&stat.observed_sum_ds);
447 }
448
449 Ok(CollapsedOut {
450 mu_observed: mu_param,
451 mu_adjusted: Some(mu_adj_param),
452 mu_residual: Some(mu_resid_param),
453 gamma: Some(gamma_param),
454 delta: Some(delta_param),
455 stats_kept: true,
456 })
457 } else {
458 let mut denom_ds = DMatrix::<f32>::zeros(num_genes, num_samples);
459 add_effective_size(&mut denom_ds, stat);
460 mu_param.update_stat(&stat.observed_sum_ds, &denom_ds);
461 mu_param.calibrate_with(out_target);
462 if matches!(out_target, CalibrateTarget::MeanOnly) {
463 mu_param.sparsify_mean_to_support(&stat.observed_sum_ds);
464 }
465 Ok(CollapsedOut {
466 mu_observed: mu_param,
467 mu_adjusted: None,
468 mu_residual: None,
469 gamma: None,
470 delta: None,
471 stats_kept: true,
472 })
473 }
474}
475
476pub(super) fn optimize(
485 stat: &CollapsedStat,
486 hyper: (f32, f32),
487 num_iter: usize,
488 label: &str,
489 out_target: CalibrateTarget,
490 keep_stats: bool,
493) -> anyhow::Result<CollapsedOut> {
494 let num_genes = stat.num_genes();
495 let num_samples = stat.num_samples();
496 let num_batches = stat.num_batches();
497
498 const BLOCK_ELEMS: usize = 2_000_000;
509 let block_rows = (BLOCK_ELEMS / num_samples.max(1)).clamp(1, num_genes.max(1));
510 let jobs = create_jobs(num_genes, num_samples, Some(block_rows));
513 let n_blocks = jobs.len();
514
515 let dims = format!("{num_genes} genes × {num_samples} samples");
516
517 let keep_stats = keep_stats || matches!(out_target, CalibrateTarget::All);
521
522 let batched = num_batches > 1;
529 let total = if batched {
530 n_blocks * num_iter
531 } else {
532 n_blocks
533 };
534 let msg = if batched {
535 format!("{label} opt-iters · {dims} · {n_blocks} blocks")
536 } else {
537 format!("{label} gene-blocks · {dims}")
538 };
539
540 if n_blocks <= 1 {
545 if batched {
546 let prog = styled_progress_bar(total as u64, &msg);
547 let mut out = optimize_block(stat, hyper, num_iter, out_target, Some(&prog))?;
548 prog.finish_and_clear();
549 if !keep_stats {
550 out.release_stats();
551 }
552 return Ok(out);
553 }
554 let spin =
555 legume_numeric::matrix::progress::new_spinner("{spinner} [{elapsed_precise}] {msg}")
556 .with_message(format!("{label} single gene-block · {dims}"));
557 let mut out = optimize_block(stat, hyper, num_iter, out_target, None)?;
558 spin.finish_and_clear();
559 if !keep_stats {
560 out.release_stats();
561 }
562 return Ok(out);
563 }
564
565 let prog = styled_progress_bar(total as u64, &msg);
571
572 let outs = jobs
576 .par_iter()
577 .map(|&(lb, ub)| -> anyhow::Result<CollapsedOut> {
578 let sub = stat.select_rows(lb, ub - lb);
579 let mut out_b =
580 optimize_block(&sub, hyper, num_iter, out_target, batched.then_some(&prog))?;
581 if !keep_stats {
582 out_b.release_stats();
585 }
586 if !batched {
587 prog.inc(1);
588 }
589 Ok(out_b)
590 })
591 .collect::<anyhow::Result<Vec<_>>>()?;
592 prog.finish_and_clear();
593
594 let mut mu_obs: Vec<GammaMatrix> = Vec::with_capacity(n_blocks);
595 let mut mu_adj: Vec<GammaMatrix> = Vec::new();
596 let mut mu_res: Vec<GammaMatrix> = Vec::new();
597 let mut gam: Vec<GammaMatrix> = Vec::new();
598 let mut del: Vec<GammaMatrix> = Vec::new();
599 for out_b in outs {
600 mu_obs.push(out_b.mu_observed);
601 if let Some(x) = out_b.mu_adjusted {
602 mu_adj.push(x);
603 }
604 if let Some(x) = out_b.mu_residual {
605 mu_res.push(x);
606 }
607 if let Some(x) = out_b.gamma {
608 gam.push(x);
609 }
610 if let Some(x) = out_b.delta {
611 del.push(x);
612 }
613 }
614
615 let join = |v: Vec<GammaMatrix>| -> Option<GammaMatrix> {
616 (!v.is_empty()).then(|| GammaMatrix::vconcat(v, keep_stats))
617 };
618 Ok(CollapsedOut {
619 mu_observed: GammaMatrix::vconcat(mu_obs, keep_stats),
620 mu_adjusted: join(mu_adj),
621 mu_residual: join(mu_res),
622 gamma: join(gam),
623 delta: join(del),
624 stats_kept: keep_stats,
625 })
626}
627
628#[derive(Debug, Clone)]
630pub struct CollapsedOut {
631 pub mu_observed: GammaMatrix,
632 pub mu_adjusted: Option<GammaMatrix>,
633 pub mu_residual: Option<GammaMatrix>,
634 pub gamma: Option<GammaMatrix>,
635 pub delta: Option<GammaMatrix>,
636 pub stats_kept: bool,
640}
641
642impl CollapsedOut {
643 fn release_stats(&mut self) {
648 self.mu_observed.release_stats();
649 for p in [
650 &mut self.mu_adjusted,
651 &mut self.mu_residual,
652 &mut self.gamma,
653 &mut self.delta,
654 ] {
655 if let Some(g) = p.as_mut() {
656 g.release_stats();
657 }
658 }
659 self.stats_kept = false;
660 }
661
662 pub fn observed_counts(
675 &self,
676 cell_to_pb: &[usize],
677 ) -> anyhow::Result<(DMatrix<f32>, Vec<f32>)> {
678 anyhow::ensure!(
679 self.stats_kept,
680 "CollapsedOut::observed_counts needs kept sufficient statistics; \
681 set MultilevelParams::keep_finest_stats"
682 );
683 let param = &self.mu_observed;
684 let (d, n_pb) = (param.nrows(), param.ncols());
685 let mut sizes = vec![0f32; n_pb];
686 let mut n_oob = 0usize;
687 for &pb in cell_to_pb {
688 if pb < n_pb {
689 sizes[pb] += 1.0;
690 } else {
691 n_oob += 1;
692 }
693 }
694 anyhow::ensure!(
695 n_oob == 0,
696 "CollapsedOut::observed_counts: {n_oob} cell→pseudobulk id(s) \
697 are out of range for {n_pb} pseudobulk(s)"
698 );
699 let counts = DMatrix::<f32>::from_fn(d, n_pb, |g, s| param.evidence_mean(g, s) * sizes[s]);
700 Ok((counts, sizes))
701 }
702}
703
704#[derive(Debug, Clone)]
706pub struct CollapsedStat {
707 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>,
718 pub anchor_batches: Vec<usize>,
721 pub size_ds: Option<nalgebra::DMatrix<f32>>,
731 pub obs_mask_db: Option<nalgebra::DMatrix<f32>>,
736 pub exclude_unmatched_from_delta: bool,
741}
742
743impl CollapsedStat {
744 pub fn new(ngene: usize, nsample: usize, nbatch: usize) -> Self {
745 Self {
746 observed_sum_ds: nalgebra::DMatrix::<f32>::zeros(ngene, nsample),
747 imputed_sum_ds: nalgebra::DMatrix::<f32>::zeros(ngene, nsample),
748 size_s: nalgebra::DVector::<f32>::zeros(nsample),
749 observed_sum_db: nalgebra::DMatrix::<f32>::zeros(ngene, nbatch),
750 n_bs: nalgebra::DMatrix::<f32>::zeros(nbatch, nsample),
751 matched_bs: nalgebra::DMatrix::<f32>::zeros(nbatch, nsample),
752 anchor_batches: Vec::new(),
753 size_ds: None,
754 obs_mask_db: None,
755 exclude_unmatched_from_delta: false,
756 }
757 }
758
759 pub fn num_genes(&self) -> usize {
760 self.observed_sum_ds.nrows()
761 }
762
763 pub fn num_samples(&self) -> usize {
764 self.observed_sum_ds.ncols()
765 }
766
767 pub fn num_batches(&self) -> usize {
768 self.observed_sum_db.ncols()
769 }
770
771 pub fn frame_weights(&self) -> DVector<f32> {
775 let nb = self.num_batches();
776 if self.anchor_batches.is_empty() {
777 DVector::from_fn(nb, |b, _| self.n_bs.row(b).sum())
778 } else {
779 let mut w = DVector::<f32>::zeros(nb);
780 for &b in &self.anchor_batches {
781 w[b] = 1.0;
782 }
783 w
784 }
785 }
786
787 pub fn clear(&mut self) {
788 self.observed_sum_ds.fill(0_f32);
789 self.imputed_sum_ds.fill(0_f32);
790 self.observed_sum_db.fill(0_f32);
791 self.size_s.fill(0_f32);
792 self.n_bs.fill(0_f32);
793 self.matched_bs.fill(0_f32);
794 self.anchor_batches.clear();
795 self.size_ds = None;
796 self.obs_mask_db = None;
797 }
798
799 pub fn select_columns(&self, indices: &[usize]) -> Self {
801 let n_new = indices.len();
802 let ng = self.num_genes();
803 let nb = self.num_batches();
804 let mut out = Self::new(ng, n_new, nb);
805 for (new_col, &old_col) in indices.iter().enumerate() {
806 out.observed_sum_ds
807 .column_mut(new_col)
808 .copy_from(&self.observed_sum_ds.column(old_col));
809 out.imputed_sum_ds
810 .column_mut(new_col)
811 .copy_from(&self.imputed_sum_ds.column(old_col));
812 out.size_s[new_col] = self.size_s[old_col];
813 for b in 0..nb {
814 out.n_bs[(b, new_col)] = self.n_bs[(b, old_col)];
815 out.matched_bs[(b, new_col)] = self.matched_bs[(b, old_col)];
816 }
817 }
818 if let Some(size_ds) = self.size_ds.as_ref() {
819 out.size_ds = Some(nalgebra::DMatrix::from_fn(ng, n_new, |g, new_col| {
820 size_ds[(g, indices[new_col])]
821 }));
822 }
823 out.obs_mask_db = self.obs_mask_db.clone();
824 out.observed_sum_db.copy_from(&self.observed_sum_db);
825 out.anchor_batches = self.anchor_batches.clone();
826 out.exclude_unmatched_from_delta = self.exclude_unmatched_from_delta;
827 out
828 }
829
830 pub fn select_rows(&self, r0: usize, nrows: usize) -> Self {
835 Self {
836 observed_sum_ds: self.observed_sum_ds.rows(r0, nrows).into_owned(),
837 imputed_sum_ds: self.imputed_sum_ds.rows(r0, nrows).into_owned(),
838 size_s: self.size_s.clone(),
839 observed_sum_db: self.observed_sum_db.rows(r0, nrows).into_owned(),
840 n_bs: self.n_bs.clone(),
841 matched_bs: self.matched_bs.clone(),
842 anchor_batches: self.anchor_batches.clone(),
843 size_ds: self
844 .size_ds
845 .as_ref()
846 .map(|m| m.rows(r0, nrows).into_owned()),
847 obs_mask_db: self
848 .obs_mask_db
849 .as_ref()
850 .map(|m| m.rows(r0, nrows).into_owned()),
851 exclude_unmatched_from_delta: self.exclude_unmatched_from_delta,
852 }
853 }
854}
855
856pub fn resample_and_optimize(
859 stat: &CollapsedStat,
860 rng: &mut impl rand::Rng,
861 opt_iter: usize,
862) -> anyhow::Result<CollapsedOut> {
863 use rand::seq::SliceRandom;
864 let n = stat.num_samples();
865 let target = n / 2;
866 let mut indices: Vec<usize> = (0..n).collect();
867 indices.shuffle(rng);
868 indices.truncate(target);
869 indices.sort_unstable();
870 let sub_stat = stat.select_columns(&indices);
871 optimize(
872 &sub_stat,
873 (1.0, 1.0),
874 opt_iter,
875 "Optimizing",
876 CalibrateTarget::All,
877 false,
878 )
879}
880
881pub(super) const DEFAULT_NUM_LEVELS: usize = 2;
886pub(super) const DEFAULT_COARSEST_SORT_DIM: usize = 7;
887
888pub(super) fn collect_matched_stat_coarse(
894 layout: &PbSampleLayout,
895 gene_sums: &[Vec<(usize, f32)>],
896 pbsamp_to_group: &[usize],
897 batch_knn_lookup: &[ColumnDict<usize>],
898 knn: usize,
899 anchor_batches: Option<&[usize]>,
900 stat: &mut CollapsedStat,
901) -> anyhow::Result<()> {
902 let num_pb = layout.cell_counts.len();
903 debug_assert_eq!(pbsamp_to_group.len(), num_pb);
904
905 stat.anchor_batches = anchor_batches.map(<[usize]>::to_vec).unwrap_or_default();
908 let neighbors_per_sc = per_batch_sc_neighbors(layout, batch_knn_lookup, knn, anchor_batches)?;
909
910 use indicatif::ParallelProgressIterator;
911 let prog_bar = styled_progress_bar(num_pb as u64, "pb-samples (matched stats)");
912 let arc_stat = Arc::new(Mutex::new(stat));
913
914 (0..num_pb)
915 .into_par_iter()
916 .progress_with(prog_bar.clone())
917 .for_each(|pbsamp_idx| {
918 let pbsamp_group = pbsamp_to_group[pbsamp_idx];
919 let sc_count = layout.cell_counts[pbsamp_idx];
920
921 if sc_count < 1.0 {
922 return;
923 }
924
925 let filtered = &neighbors_per_sc[pbsamp_idx];
926 if filtered.is_empty() {
927 return;
928 }
929
930 let max_neg_d = filtered
932 .iter()
933 .map(|(_, d)| -d)
934 .fold(f32::NEG_INFINITY, f32::max);
935 let mut weights: Vec<f32> = filtered
936 .iter()
937 .map(|(_, d)| (-d - max_neg_d).exp())
938 .collect();
939 let w_sum: f32 = weights.iter().sum();
940 if w_sum > 0.0 {
941 weights.iter_mut().for_each(|w| *w /= w_sum);
942 }
943
944 let mut y_hat: HashMap<usize, f32> = HashMap::default();
948 for ((matched_sc, _), &w) in filtered.iter().zip(weights.iter()) {
949 let matched_count = layout.cell_counts[*matched_sc];
950 if matched_count < 1.0 {
951 continue;
952 }
953 let inv_count = 1.0 / matched_count;
954 for &(gene, val) in &gene_sums[*matched_sc] {
955 *y_hat.entry(gene).or_default() += w * val * inv_count;
956 }
957 }
958
959 let mut stat = arc_stat.lock().expect("lock stat");
960
961 for (&gene, &y) in &y_hat {
963 stat.imputed_sum_ds[(gene, pbsamp_group)] += sc_count * y;
964 }
965
966 for ((matched_sc, _), &w) in filtered.iter().zip(weights.iter()) {
969 if layout.cell_counts[*matched_sc] < 1.0 {
970 continue;
971 }
972 stat.matched_bs[(layout.pb_sample_to_batch[*matched_sc], pbsamp_group)] +=
973 sc_count * w;
974 }
975 });
976
977 prog_bar.finish_and_clear();
978 Ok(())
979}
980
981pub(super) fn merge_stat(
986 fine_stat: &CollapsedStat,
987 fine_to_coarse: &[usize],
988 num_coarse_groups: usize,
989) -> CollapsedStat {
990 let num_genes = fine_stat.num_genes();
991 let num_batches = fine_stat.num_batches();
992 let mut coarse = CollapsedStat::new(num_genes, num_coarse_groups, num_batches);
993
994 for (fine_g, &coarse_g) in fine_to_coarse.iter().enumerate() {
995 coarse
996 .observed_sum_ds
997 .column_mut(coarse_g)
998 .add_assign(&fine_stat.observed_sum_ds.column(fine_g));
999 coarse
1000 .imputed_sum_ds
1001 .column_mut(coarse_g)
1002 .add_assign(&fine_stat.imputed_sum_ds.column(fine_g));
1003 coarse.size_s[coarse_g] += fine_stat.size_s[fine_g];
1004 for b in 0..num_batches {
1005 coarse.n_bs[(b, coarse_g)] += fine_stat.n_bs[(b, fine_g)];
1006 coarse.matched_bs[(b, coarse_g)] += fine_stat.matched_bs[(b, fine_g)];
1007 }
1008 }
1009
1010 if let Some(fine_size_ds) = fine_stat.size_ds.as_ref() {
1013 let mut size_ds = nalgebra::DMatrix::<f32>::zeros(num_genes, num_coarse_groups);
1014 for (fine_g, &coarse_g) in fine_to_coarse.iter().enumerate() {
1015 size_ds
1016 .column_mut(coarse_g)
1017 .add_assign(&fine_size_ds.column(fine_g));
1018 }
1019 coarse.size_ds = Some(size_ds);
1020 }
1021 coarse.obs_mask_db = fine_stat.obs_mask_db.clone();
1022 coarse.anchor_batches = fine_stat.anchor_batches.clone();
1023 coarse.exclude_unmatched_from_delta = fine_stat.exclude_unmatched_from_delta;
1024
1025 coarse.observed_sum_db.copy_from(&fine_stat.observed_sum_db);
1026 coarse
1027}
1028
1029#[cfg(test)]
1030#[path = "stats_tests.rs"]
1031mod gene_block_tests;