use super::*;
use nalgebra::DVector;
pub(super) struct KnnParams<'a> {
pub(super) knn_batches: usize,
pub(super) knn_cells: usize,
pub(super) reference_indices: Option<&'a [usize]>,
}
pub(super) fn collect_matched_stat_visitor(
sample: usize,
cells: &[usize],
data_vec: &SparseIoVec,
knn_params: &KnnParams,
arc_stat: Arc<Mutex<&mut CollapsedStat>>,
) -> anyhow::Result<()> {
let knn_batches = knn_params.knn_batches;
let knn_cells = knn_params.knn_cells;
let (y0_matched, source_columns, euclidean_distances) = match knn_params.reference_indices {
Some(target_indices) => data_vec.read_matched_columns_csc(
cells.iter().cloned(),
target_indices,
knn_cells,
true,
)?,
None => {
let (mat, src, _matched, dist) = data_vec.read_neighbouring_columns_csc(
cells.iter().cloned(),
knn_batches,
knn_cells,
true,
None,
)?;
(mat, src, dist)
}
};
let y1_pos: HashMap<_, _> = cells
.iter()
.cloned()
.enumerate()
.map(|(i, p)| (p, i))
.collect();
let neg_distance_triplets = source_columns
.iter()
.zip(euclidean_distances.iter())
.enumerate()
.map(|(t, (&s, &d))| (t, y1_pos[&s], -d))
.collect::<Vec<_>>();
let ww = CscMat::from_nonzero_triplets(
y0_matched.ncols(),
cells.len(),
neg_distance_triplets.as_ref(),
)?
.normalize_exp_logits_columns();
let y1_hat = &y0_matched * &ww;
let source_batches = data_vec.get_batch_membership(source_columns.iter().cloned());
let mut stat = arc_stat.lock().expect("lock stat");
for w_j in ww.col_iter() {
for (&k, &w) in w_j.row_indices().iter().zip(w_j.values().iter()) {
stat.matched_bs[(source_batches[k], sample)] += w;
}
}
for y_j in y1_hat.col_iter() {
let rows = y_j.row_indices();
let vals = y_j.values();
for (&gene, &y) in rows.iter().zip(vals.iter()) {
stat.imputed_sum_ds[(gene, sample)] += y;
}
}
Ok(())
}
pub(super) fn collect_basic_stat_visitor(
sample: usize,
cells: &[usize],
data_vec: &SparseIoVec,
_: &EmptyArg,
arc_stat: Arc<Mutex<&mut CollapsedStat>>,
) -> anyhow::Result<()> {
let yy = data_vec.read_columns_csc(cells.iter().cloned())?;
let mut stat = arc_stat.lock().expect("lock stat");
for (y_j, &col) in yy.col_iter().zip(cells.iter()) {
let w = data_vec.column_multiplicity(col);
let rows = y_j.row_indices();
let vals = y_j.values();
for (&gene, &y) in rows.iter().zip(vals.iter()) {
stat.observed_sum_ds[(gene, sample)] += y * w;
}
stat.size_s[sample] += w;
}
Ok(())
}
pub(super) fn collect_batch_stat_visitor(
sample: usize,
cells_in_sample: &[usize],
data_vec: &SparseIoVec,
_: &EmptyArg,
arc_stat: Arc<Mutex<&mut CollapsedStat>>,
) -> anyhow::Result<()> {
let yy = data_vec.read_columns_csc(cells_in_sample.iter().cloned())?;
let batches = data_vec.get_batch_membership(cells_in_sample.iter().cloned());
let mut stat = arc_stat.lock().expect("lock stat");
yy.col_iter()
.zip(batches.iter())
.zip(cells_in_sample.iter())
.for_each(|((y_j, &b), &col)| {
let w = data_vec.column_multiplicity(col);
let rows = y_j.row_indices();
let vals = y_j.values();
for (&gene, &y) in rows.iter().zip(vals.iter()) {
stat.observed_sum_db[(gene, b)] += y * w;
}
stat.n_bs[(b, sample)] += w;
});
Ok(())
}
fn add_effective_size(denom_ds: &mut nalgebra::DMatrix<f32>, stat: &CollapsedStat) {
match stat.size_ds.as_ref() {
Some(size_ds) => {
debug_assert_eq!(denom_ds.shape(), size_ds.shape());
*denom_ds += size_ds;
}
None => {
for s in 0..denom_ds.ncols() {
denom_ds.column_mut(s).add_scalar_mut(stat.size_s[s]);
}
}
}
}
fn pin_delta_scale(
num_db: &DMatrix<f32>,
den_db: &DMatrix<f32>,
(a0, b0): (f32, f32),
frame_w: &DVector<f32>,
mask_db: Option<&DMatrix<f32>>,
) -> DMatrix<f32> {
let (ng, nb) = num_db.shape();
let mut out = den_db.clone();
for g in 0..ng {
let (mut lsum, mut wsum) = (0f64, 0f64);
for b in 0..nb {
let w = f64::from(frame_w[b]) * mask_db.map_or(1.0, |m| f64::from(m[(g, b)]));
if w > 0.0 {
let m = f64::from(a0 + num_db[(g, b)]) / f64::from(b0 + den_db[(g, b)]);
lsum += w * m.max(f64::MIN_POSITIVE).ln();
wsum += w;
}
}
if wsum > 0.0 {
let gm = (lsum / wsum).exp() as f32;
for b in 0..nb {
if mask_db.is_some_and(|m| m[(g, b)] == 0.0) {
continue;
}
out[(g, b)] = ((b0 + den_db[(g, b)]) * gm - b0).max(0.0);
}
}
}
out
}
fn optimize_block(
stat: &CollapsedStat,
hyper: (f32, f32),
num_iter: usize,
out_target: CalibrateTarget,
prog: Option<&indicatif::ProgressBar>,
) -> anyhow::Result<CollapsedOut> {
let (a0, b0) = hyper;
let num_genes = stat.num_genes();
let num_samples = stat.num_samples();
let num_batches = stat.num_batches();
let mut mu_param = GammaMatrix::new((num_genes, num_samples), a0, b0);
if num_batches > 1 {
let n_bs = &stat.n_bs;
let w_bs = &stat.matched_bs;
let own_plus_src = n_bs + w_bs; let (obs_db_for_delta, own_plus_src_for_delta, n_bs_d) =
if stat.exclude_unmatched_from_delta {
let mut obs = stat.observed_sum_db.clone();
let mut n_bs_d = n_bs.clone();
for s in 0..num_samples {
if w_bs.column(s).sum() > 0.0 {
continue;
}
let size = stat.size_s[s];
if size <= 0.0 {
continue;
}
for b in 0..num_batches {
let n = n_bs[(b, s)];
if n <= 0.0 {
continue;
}
let frac = n / size;
for g in 0..num_genes {
obs[(g, b)] -= stat.observed_sum_ds[(g, s)] * frac;
}
n_bs_d[(b, s)] = 0.0;
}
}
let own_plus = &n_bs_d + w_bs;
(obs, own_plus, n_bs_d)
} else {
(
stat.observed_sum_db.clone(),
own_plus_src.clone(),
n_bs.clone(),
)
};
let obs_plus_imp = &stat.observed_sum_ds + &stat.imputed_sum_ds;
let obs_frac: Option<DMatrix<f32>> = stat.size_ds.as_ref().map(|size_ds| {
DMatrix::from_fn(num_genes, num_samples, |g, s| {
let n = stat.size_s[s];
if n > 0.0 {
size_ds[(g, s)] / n
} else {
0.0
}
})
});
let frame_w = if stat.exclude_unmatched_from_delta && stat.anchor_batches.is_empty() {
DVector::from_fn(num_batches, |b, _| n_bs_d.row(b).sum())
} else {
stat.frame_weights()
};
for b in 0..num_batches {
if frame_w[b] <= 0.0 {
warn!(
"batch {b} has no matched mass for δ (frame weight 0); \
δ_·{b} stays on prior ≈ 1 (e.g. blast-only / clone-only donor)"
);
}
}
let own_plus_src_delta_t = own_plus_src_for_delta.transpose();
let w_bs_t = w_bs.transpose();
let mut mu_adj_param = GammaMatrix::new((num_genes, num_samples), a0, b0);
let mut delta_param = GammaMatrix::new((num_genes, num_batches), a0, b0);
let mut delta_gb = DMatrix::<f32>::from_element(num_genes, num_batches, 1.0);
let update_mu = |mu_adj_param: &mut GammaMatrix, delta_gb: &DMatrix<f32>| {
let mut denom_ds = delta_gb * &own_plus_src;
if let Some(f) = obs_frac.as_ref() {
denom_ds.component_mul_assign(f);
}
mu_adj_param.update_stat(&obs_plus_imp, &denom_ds);
mu_adj_param.calibrate_with(CalibrateTarget::MeanOnly);
};
let mut imp_share = DMatrix::<f32>::zeros(num_genes, num_samples);
let mut mu_frac = DMatrix::<f32>::zeros(num_genes, num_samples);
for _opt_iter in 0..num_iter {
#[cfg(debug_assertions)]
{
debug!("iteration: {}", &_opt_iter);
}
update_mu(&mut mu_adj_param, &delta_gb);
let mu_ds = mu_adj_param.posterior_mean();
imp_share.gemm(1.0, &delta_gb, w_bs, 0.0); imp_share.zip_apply(&stat.imputed_sum_ds, |z, x| {
*z = if *z > 0.0 { x / *z } else { 0.0 };
});
let mut num_db = &obs_db_for_delta + (&imp_share * &w_bs_t).component_mul(&delta_gb);
let mu_frac_ref: &DMatrix<f32> = match obs_frac.as_ref() {
Some(f) => {
mu_frac.copy_from(mu_ds);
mu_frac.component_mul_assign(f);
&mu_frac
}
None => mu_ds,
};
let mut den_db = mu_frac_ref * &own_plus_src_delta_t; if let Some(mask) = stat.obs_mask_db.as_ref() {
num_db.component_mul_assign(mask);
den_db.component_mul_assign(mask);
}
let den_db = pin_delta_scale(
&num_db,
&den_db,
(a0, b0),
&frame_w,
stat.obs_mask_db.as_ref(),
);
delta_param.update_stat(&num_db, &den_db);
delta_param.calibrate_with(CalibrateTarget::MeanOnly);
delta_gb.copy_from(delta_param.posterior_mean());
if let Some(p) = prog {
p.inc(1);
}
}
update_mu(&mut mu_adj_param, &delta_gb);
mu_adj_param.calibrate_with(out_target);
delta_param.calibrate_with(out_target);
let mu_ds = mu_adj_param.posterior_mean();
let mass_times_mu = |per_sample: &dyn Fn(usize) -> f32| {
let mut m = mu_ds.clone();
for s in 0..num_samples {
m.column_mut(s).scale_mut(per_sample(s));
}
if let Some(f) = obs_frac.as_ref() {
m.component_mul_assign(f);
}
m
};
let resid_denom = mass_times_mu(&|s| stat.size_s[s]);
let mut mu_resid_param = GammaMatrix::new((num_genes, num_samples), a0, b0);
mu_resid_param.update_stat(&stat.observed_sum_ds, &resid_denom);
mu_resid_param.calibrate_with(out_target);
let src_mass_s: Vec<f32> = (0..num_samples).map(|s| w_bs.column(s).sum()).collect();
let gamma_denom = mass_times_mu(&|s| src_mass_s[s]);
let mut gamma_param = GammaMatrix::new((num_genes, num_samples), a0, b0);
gamma_param.update_stat(&stat.imputed_sum_ds, &gamma_denom);
gamma_param.calibrate_with(out_target);
{
let mut own_mass = DMatrix::<f32>::zeros(num_genes, num_samples);
add_effective_size(&mut own_mass, stat);
mu_param.update_stat(&stat.observed_sum_ds, &own_mass);
mu_param.calibrate_with(out_target);
};
if matches!(out_target, CalibrateTarget::MeanOnly) {
mu_param.sparsify_mean_to_support(&stat.observed_sum_ds);
mu_adj_param.sparsify_mean_to_support(&obs_plus_imp);
gamma_param.sparsify_mean_to_support(&stat.imputed_sum_ds);
mu_resid_param.sparsify_mean_to_support(&stat.observed_sum_ds);
}
Ok(CollapsedOut {
mu_observed: mu_param,
mu_adjusted: Some(mu_adj_param),
mu_residual: Some(mu_resid_param),
gamma: Some(gamma_param),
delta: Some(delta_param),
stats_kept: true,
})
} else {
let mut denom_ds = DMatrix::<f32>::zeros(num_genes, num_samples);
add_effective_size(&mut denom_ds, stat);
mu_param.update_stat(&stat.observed_sum_ds, &denom_ds);
mu_param.calibrate_with(out_target);
if matches!(out_target, CalibrateTarget::MeanOnly) {
mu_param.sparsify_mean_to_support(&stat.observed_sum_ds);
}
Ok(CollapsedOut {
mu_observed: mu_param,
mu_adjusted: None,
mu_residual: None,
gamma: None,
delta: None,
stats_kept: true,
})
}
}
pub(super) fn optimize(
stat: &CollapsedStat,
hyper: (f32, f32),
num_iter: usize,
label: &str,
out_target: CalibrateTarget,
keep_stats: bool,
) -> anyhow::Result<CollapsedOut> {
let num_genes = stat.num_genes();
let num_samples = stat.num_samples();
let num_batches = stat.num_batches();
const BLOCK_ELEMS: usize = 2_000_000;
let block_rows = (BLOCK_ELEMS / num_samples.max(1)).clamp(1, num_genes.max(1));
let jobs = create_jobs(num_genes, num_samples, Some(block_rows));
let n_blocks = jobs.len();
let dims = format!("{num_genes} genes × {num_samples} samples");
let keep_stats = keep_stats || matches!(out_target, CalibrateTarget::All);
let batched = num_batches > 1;
let total = if batched {
n_blocks * num_iter
} else {
n_blocks
};
let msg = if batched {
format!("{label} opt-iters · {dims} · {n_blocks} blocks")
} else {
format!("{label} gene-blocks · {dims}")
};
if n_blocks <= 1 {
if batched {
let prog = styled_progress_bar(total as u64, &msg);
let mut out = optimize_block(stat, hyper, num_iter, out_target, Some(&prog))?;
prog.finish_and_clear();
if !keep_stats {
out.release_stats();
}
return Ok(out);
}
let spin =
legume_numeric::matrix::progress::new_spinner("{spinner} [{elapsed_precise}] {msg}")
.with_message(format!("{label} single gene-block · {dims}"));
let mut out = optimize_block(stat, hyper, num_iter, out_target, None)?;
spin.finish_and_clear();
if !keep_stats {
out.release_stats();
}
return Ok(out);
}
let prog = styled_progress_bar(total as u64, &msg);
let outs = jobs
.par_iter()
.map(|&(lb, ub)| -> anyhow::Result<CollapsedOut> {
let sub = stat.select_rows(lb, ub - lb);
let mut out_b =
optimize_block(&sub, hyper, num_iter, out_target, batched.then_some(&prog))?;
if !keep_stats {
out_b.release_stats();
}
if !batched {
prog.inc(1);
}
Ok(out_b)
})
.collect::<anyhow::Result<Vec<_>>>()?;
prog.finish_and_clear();
let mut mu_obs: Vec<GammaMatrix> = Vec::with_capacity(n_blocks);
let mut mu_adj: Vec<GammaMatrix> = Vec::new();
let mut mu_res: Vec<GammaMatrix> = Vec::new();
let mut gam: Vec<GammaMatrix> = Vec::new();
let mut del: Vec<GammaMatrix> = Vec::new();
for out_b in outs {
mu_obs.push(out_b.mu_observed);
if let Some(x) = out_b.mu_adjusted {
mu_adj.push(x);
}
if let Some(x) = out_b.mu_residual {
mu_res.push(x);
}
if let Some(x) = out_b.gamma {
gam.push(x);
}
if let Some(x) = out_b.delta {
del.push(x);
}
}
let join = |v: Vec<GammaMatrix>| -> Option<GammaMatrix> {
(!v.is_empty()).then(|| GammaMatrix::vconcat(v, keep_stats))
};
Ok(CollapsedOut {
mu_observed: GammaMatrix::vconcat(mu_obs, keep_stats),
mu_adjusted: join(mu_adj),
mu_residual: join(mu_res),
gamma: join(gam),
delta: join(del),
stats_kept: keep_stats,
})
}
#[derive(Debug, Clone)]
pub struct CollapsedOut {
pub mu_observed: GammaMatrix,
pub mu_adjusted: Option<GammaMatrix>,
pub mu_residual: Option<GammaMatrix>,
pub gamma: Option<GammaMatrix>,
pub delta: Option<GammaMatrix>,
pub stats_kept: bool,
}
impl CollapsedOut {
fn release_stats(&mut self) {
self.mu_observed.release_stats();
for p in [
&mut self.mu_adjusted,
&mut self.mu_residual,
&mut self.gamma,
&mut self.delta,
] {
if let Some(g) = p.as_mut() {
g.release_stats();
}
}
self.stats_kept = false;
}
pub fn observed_counts(
&self,
cell_to_pb: &[usize],
) -> anyhow::Result<(DMatrix<f32>, Vec<f32>)> {
anyhow::ensure!(
self.stats_kept,
"CollapsedOut::observed_counts needs kept sufficient statistics; \
set MultilevelParams::keep_finest_stats"
);
let param = &self.mu_observed;
let (d, n_pb) = (param.nrows(), param.ncols());
let mut sizes = vec![0f32; n_pb];
let mut n_oob = 0usize;
for &pb in cell_to_pb {
if pb < n_pb {
sizes[pb] += 1.0;
} else {
n_oob += 1;
}
}
anyhow::ensure!(
n_oob == 0,
"CollapsedOut::observed_counts: {n_oob} cell→pseudobulk id(s) \
are out of range for {n_pb} pseudobulk(s)"
);
let counts = DMatrix::<f32>::from_fn(d, n_pb, |g, s| param.evidence_mean(g, s) * sizes[s]);
Ok((counts, sizes))
}
}
#[derive(Debug, Clone)]
pub struct CollapsedStat {
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>,
pub anchor_batches: Vec<usize>,
pub size_ds: Option<nalgebra::DMatrix<f32>>,
pub obs_mask_db: Option<nalgebra::DMatrix<f32>>,
pub exclude_unmatched_from_delta: bool,
}
impl CollapsedStat {
pub fn new(ngene: usize, nsample: usize, nbatch: usize) -> Self {
Self {
observed_sum_ds: nalgebra::DMatrix::<f32>::zeros(ngene, nsample),
imputed_sum_ds: nalgebra::DMatrix::<f32>::zeros(ngene, nsample),
size_s: nalgebra::DVector::<f32>::zeros(nsample),
observed_sum_db: nalgebra::DMatrix::<f32>::zeros(ngene, nbatch),
n_bs: nalgebra::DMatrix::<f32>::zeros(nbatch, nsample),
matched_bs: nalgebra::DMatrix::<f32>::zeros(nbatch, nsample),
anchor_batches: Vec::new(),
size_ds: None,
obs_mask_db: None,
exclude_unmatched_from_delta: false,
}
}
pub fn num_genes(&self) -> usize {
self.observed_sum_ds.nrows()
}
pub fn num_samples(&self) -> usize {
self.observed_sum_ds.ncols()
}
pub fn num_batches(&self) -> usize {
self.observed_sum_db.ncols()
}
pub fn frame_weights(&self) -> DVector<f32> {
let nb = self.num_batches();
if self.anchor_batches.is_empty() {
DVector::from_fn(nb, |b, _| self.n_bs.row(b).sum())
} else {
let mut w = DVector::<f32>::zeros(nb);
for &b in &self.anchor_batches {
w[b] = 1.0;
}
w
}
}
pub fn clear(&mut self) {
self.observed_sum_ds.fill(0_f32);
self.imputed_sum_ds.fill(0_f32);
self.observed_sum_db.fill(0_f32);
self.size_s.fill(0_f32);
self.n_bs.fill(0_f32);
self.matched_bs.fill(0_f32);
self.anchor_batches.clear();
self.size_ds = None;
self.obs_mask_db = None;
}
pub fn select_columns(&self, indices: &[usize]) -> Self {
let n_new = indices.len();
let ng = self.num_genes();
let nb = self.num_batches();
let mut out = Self::new(ng, n_new, nb);
for (new_col, &old_col) in indices.iter().enumerate() {
out.observed_sum_ds
.column_mut(new_col)
.copy_from(&self.observed_sum_ds.column(old_col));
out.imputed_sum_ds
.column_mut(new_col)
.copy_from(&self.imputed_sum_ds.column(old_col));
out.size_s[new_col] = self.size_s[old_col];
for b in 0..nb {
out.n_bs[(b, new_col)] = self.n_bs[(b, old_col)];
out.matched_bs[(b, new_col)] = self.matched_bs[(b, old_col)];
}
}
if let Some(size_ds) = self.size_ds.as_ref() {
out.size_ds = Some(nalgebra::DMatrix::from_fn(ng, n_new, |g, new_col| {
size_ds[(g, indices[new_col])]
}));
}
out.obs_mask_db = self.obs_mask_db.clone();
out.observed_sum_db.copy_from(&self.observed_sum_db);
out.anchor_batches = self.anchor_batches.clone();
out.exclude_unmatched_from_delta = self.exclude_unmatched_from_delta;
out
}
pub fn select_rows(&self, r0: usize, nrows: usize) -> Self {
Self {
observed_sum_ds: self.observed_sum_ds.rows(r0, nrows).into_owned(),
imputed_sum_ds: self.imputed_sum_ds.rows(r0, nrows).into_owned(),
size_s: self.size_s.clone(),
observed_sum_db: self.observed_sum_db.rows(r0, nrows).into_owned(),
n_bs: self.n_bs.clone(),
matched_bs: self.matched_bs.clone(),
anchor_batches: self.anchor_batches.clone(),
size_ds: self
.size_ds
.as_ref()
.map(|m| m.rows(r0, nrows).into_owned()),
obs_mask_db: self
.obs_mask_db
.as_ref()
.map(|m| m.rows(r0, nrows).into_owned()),
exclude_unmatched_from_delta: self.exclude_unmatched_from_delta,
}
}
}
pub fn resample_and_optimize(
stat: &CollapsedStat,
rng: &mut impl rand::Rng,
opt_iter: usize,
) -> anyhow::Result<CollapsedOut> {
use rand::seq::SliceRandom;
let n = stat.num_samples();
let target = n / 2;
let mut indices: Vec<usize> = (0..n).collect();
indices.shuffle(rng);
indices.truncate(target);
indices.sort_unstable();
let sub_stat = stat.select_columns(&indices);
optimize(
&sub_stat,
(1.0, 1.0),
opt_iter,
"Optimizing",
CalibrateTarget::All,
false,
)
}
pub(super) const DEFAULT_NUM_LEVELS: usize = 2;
pub(super) const DEFAULT_COARSEST_SORT_DIM: usize = 7;
pub(super) fn collect_matched_stat_coarse(
layout: &PbSampleLayout,
gene_sums: &[Vec<(usize, f32)>],
pbsamp_to_group: &[usize],
batch_knn_lookup: &[ColumnDict<usize>],
knn: usize,
anchor_batches: Option<&[usize]>,
stat: &mut CollapsedStat,
) -> anyhow::Result<()> {
let num_pb = layout.cell_counts.len();
debug_assert_eq!(pbsamp_to_group.len(), num_pb);
stat.anchor_batches = anchor_batches.map(<[usize]>::to_vec).unwrap_or_default();
let neighbors_per_sc = per_batch_sc_neighbors(layout, batch_knn_lookup, knn, anchor_batches)?;
use indicatif::ParallelProgressIterator;
let prog_bar = styled_progress_bar(num_pb as u64, "pb-samples (matched stats)");
let arc_stat = Arc::new(Mutex::new(stat));
(0..num_pb)
.into_par_iter()
.progress_with(prog_bar.clone())
.for_each(|pbsamp_idx| {
let pbsamp_group = pbsamp_to_group[pbsamp_idx];
let sc_count = layout.cell_counts[pbsamp_idx];
if sc_count < 1.0 {
return;
}
let filtered = &neighbors_per_sc[pbsamp_idx];
if filtered.is_empty() {
return;
}
let max_neg_d = filtered
.iter()
.map(|(_, d)| -d)
.fold(f32::NEG_INFINITY, f32::max);
let mut weights: Vec<f32> = filtered
.iter()
.map(|(_, d)| (-d - max_neg_d).exp())
.collect();
let w_sum: f32 = weights.iter().sum();
if w_sum > 0.0 {
weights.iter_mut().for_each(|w| *w /= w_sum);
}
let mut y_hat: HashMap<usize, f32> = HashMap::default();
for ((matched_sc, _), &w) in filtered.iter().zip(weights.iter()) {
let matched_count = layout.cell_counts[*matched_sc];
if matched_count < 1.0 {
continue;
}
let inv_count = 1.0 / matched_count;
for &(gene, val) in &gene_sums[*matched_sc] {
*y_hat.entry(gene).or_default() += w * val * inv_count;
}
}
let mut stat = arc_stat.lock().expect("lock stat");
for (&gene, &y) in &y_hat {
stat.imputed_sum_ds[(gene, pbsamp_group)] += sc_count * y;
}
for ((matched_sc, _), &w) in filtered.iter().zip(weights.iter()) {
if layout.cell_counts[*matched_sc] < 1.0 {
continue;
}
stat.matched_bs[(layout.pb_sample_to_batch[*matched_sc], pbsamp_group)] +=
sc_count * w;
}
});
prog_bar.finish_and_clear();
Ok(())
}
pub(super) fn merge_stat(
fine_stat: &CollapsedStat,
fine_to_coarse: &[usize],
num_coarse_groups: usize,
) -> CollapsedStat {
let num_genes = fine_stat.num_genes();
let num_batches = fine_stat.num_batches();
let mut coarse = CollapsedStat::new(num_genes, num_coarse_groups, num_batches);
for (fine_g, &coarse_g) in fine_to_coarse.iter().enumerate() {
coarse
.observed_sum_ds
.column_mut(coarse_g)
.add_assign(&fine_stat.observed_sum_ds.column(fine_g));
coarse
.imputed_sum_ds
.column_mut(coarse_g)
.add_assign(&fine_stat.imputed_sum_ds.column(fine_g));
coarse.size_s[coarse_g] += fine_stat.size_s[fine_g];
for b in 0..num_batches {
coarse.n_bs[(b, coarse_g)] += fine_stat.n_bs[(b, fine_g)];
coarse.matched_bs[(b, coarse_g)] += fine_stat.matched_bs[(b, fine_g)];
}
}
if let Some(fine_size_ds) = fine_stat.size_ds.as_ref() {
let mut size_ds = nalgebra::DMatrix::<f32>::zeros(num_genes, num_coarse_groups);
for (fine_g, &coarse_g) in fine_to_coarse.iter().enumerate() {
size_ds
.column_mut(coarse_g)
.add_assign(&fine_size_ds.column(fine_g));
}
coarse.size_ds = Some(size_ds);
}
coarse.obs_mask_db = fine_stat.obs_mask_db.clone();
coarse.anchor_batches = fine_stat.anchor_batches.clone();
coarse.exclude_unmatched_from_delta = fine_stat.exclude_unmatched_from_delta;
coarse.observed_sum_db.copy_from(&fine_stat.observed_sum_db);
coarse
}
#[cfg(test)]
#[path = "stats_tests.rs"]
mod gene_block_tests;