use crate::sim::copula::groups::{group_sizes, members};
use crate::sim::copula::marginals::phi;
use crate::sim::copula::mixture::{PoissonMixture, Prepared};
use crate::sim::copula::{GlobalCopulaFit, GroupFit};
use crate::sim::core::standardized_log_topic;
use indicatif::ParallelProgressIterator;
use log::info;
use nalgebra::{DMatrix, DVector};
use rand::SeedableRng;
use rand_distr::{Distribution, Normal};
use rayon::prelude::*;
const MOMENT_BLOCK: usize = 256;
const SPREAD_STEPS: usize = 20;
const MIN_TOPIC_VARIANCE: f64 = 1e-8;
pub enum CellTerm<'a> {
Topics {
dict_dk: &'a DMatrix<f32>,
theta_kn: &'a DMatrix<f32>,
},
PerCell(&'a (dyn Fn(usize) -> DVector<f32> + Sync)),
}
impl CellTerm<'_> {
fn at(&self, j: usize) -> DVector<f32> {
match self {
Self::Topics { dict_dk, theta_kn } => {
standardized_log_topic(&(*dict_dk * theta_kn.column(j)))
}
Self::PerCell(term) => term(j),
}
}
}
pub struct CellPrograms<'a> {
pub term: CellTerm<'a>,
pub pve_topic: f32,
pub cap_spread: bool,
pub pve_noise: f32,
pub batch_delta: &'a [DVector<f32>],
pub batch_membership: &'a [usize],
}
pub struct TermScaling {
pub amp: Vec<f32>,
pub centre: Vec<f32>,
}
impl TermScaling {
pub fn pooled(per_group: &[TermScaling], groups: &[usize]) -> Self {
let dd = per_group.first().map_or(0, |s| s.amp.len());
let counts = group_sizes(groups, per_group.len());
let n = groups.len().max(1) as f32;
let (mut amp, mut centre) = (vec![0.0_f32; dd], vec![0.0_f32; dd]);
for (s, &c) in per_group.iter().zip(&counts) {
let w = c as f32 / n;
for g in 0..dd {
amp[g] += w * s.amp[g];
centre[g] += w * s.centre[g];
}
}
Self { amp, centre }
}
}
pub struct ReferenceSample {
pub triplets: Triplets,
pub groups: Vec<usize>,
pub scaling: Vec<TermScaling>,
}
const GROUP_STREAM: u64 = 0x6705_5eed_0000_0001;
pub type Triplets = Vec<(u64, u64, f32)>;
pub fn depth_log_offset(fit: &GlobalCopulaFit, depth: Option<usize>) -> f32 {
let lib_ref: f32 = fit.mu_hat.iter().sum::<f32>().max(1e-30);
match depth {
Some(d) if d > 0 => ((d as f32) / lib_ref).ln(),
_ => 0.0,
}
}
pub fn sample_reference_counts(
fit: &GlobalCopulaFit,
depth_offset: f32,
programs: &CellPrograms,
couple_depth: bool,
rseed: u64,
seed_offset: u64,
) -> anyhow::Result<ReferenceSample> {
let dd = fit.n_genes;
let nn = programs.batch_membership.len();
if let CellTerm::Topics { theta_kn, .. } = &programs.term {
anyhow::ensure!(
theta_kn.ncols() == nn,
"reference sampler: {} cells of topic proportions for {} cells",
theta_kn.ncols(),
nn
);
}
anyhow::ensure!(
programs
.batch_membership
.iter()
.all(|&b| b < programs.batch_delta.len()),
"reference sampler: a cell's batch has no shift"
);
if nn > 0 {
let len = programs.term.at(0).len();
anyhow::ensure!(
len == dd,
"reference sampler: a per-cell term of {len} genes for {dd} genes"
);
}
let topic = |j: usize| programs.term.at(j);
let groups = fit.draw_groups(nn, rseed.wrapping_add(seed_offset) ^ GROUP_STREAM);
let group_cells = members(&groups, fit.groups.len());
let budgets: Vec<DispersionBudget> = fit
.groups
.iter()
.zip(&group_cells)
.map(|(group, cells)| DispersionBudget::new(fit, group, programs, cells, &topic))
.collect();
info!(
"reference sampler: {} group(s); topic term capped by the reference dispersion for {} \
gene-groups; {} of {} active gene-groups over their variance; depth {}",
fit.groups.len(),
budgets.iter().map(|b| b.n_capped).sum::<usize>(),
budgets.iter().map(|b| b.n_over).sum::<usize>(),
fit.active_genes.len() * fit.groups.len(),
if couple_depth {
"coupled as in the reference"
} else {
"fixed"
}
);
let batch_centre = batch_centre(programs.batch_delta, programs.batch_membership, dd);
let with_topic = budgets.iter().any(|b| b.topic_amp.iter().any(|&a| a > 0.0));
let fixed = |b: &DispersionBudget, g: usize| b.topic_amp[g] == 0.0 && b.noise_amp[g] == 0.0;
let tables: Vec<Vec<Vec<Vec<f64>>>> = budgets
.iter()
.zip(&group_cells)
.map(|(budget, cells)| {
programs
.batch_delta
.iter()
.map(|shift| {
let mut per_gene = vec![Vec::new(); dd];
if cells.is_empty() {
return per_gene;
}
let built: Vec<(usize, Vec<f64>)> = fit
.active_genes
.par_iter()
.filter(|&&g| fixed(budget, g))
.map(|&g| {
let m = (depth_offset + shift[g] - batch_centre[g]).exp() as f64;
(g, budget.marginal[g].cdf_table(m))
})
.collect();
for (g, t) in built {
per_gene[g] = t;
}
per_gene
})
.collect()
})
.collect();
let triplets: Triplets = (0..nn)
.into_par_iter()
.progress_count(nn as u64)
.map(|j| -> Triplets {
let mut rng = rand::rngs::StdRng::seed_from_u64(
rseed.wrapping_add(seed_offset).wrapping_add(j as u64),
);
let normal = Normal::new(0.0_f32, 1.0_f32).unwrap();
let z_depth: f32 = if couple_depth {
normal.sample(&mut rng)
} else {
0.0
};
let t = with_topic.then(|| topic(j));
let k = groups[j];
let (budget, group) = (&budgets[k], &fit.groups[k]);
let with_noise = budget.with_noise;
let b = programs.batch_membership[j];
let shift = &programs.batch_delta[b];
let z_hvg = fit.copula.sample(&mut rng);
let mut counts = Triplets::with_capacity(fit.active_genes.len() / 8);
for &g in &fit.active_genes {
let mut ln_m = depth_offset + shift[g] - batch_centre[g];
if let Some(t) = t.as_ref() {
ln_m += budget.topic_amp[g] * t[g] - budget.ln_m1[g];
}
if with_noise {
let a = budget.noise_amp[g];
ln_m += a * normal.sample(&mut rng) - 0.5 * a * a;
}
let e = match fit.hvg_pos[g] {
Some(h) => z_hvg[h as usize],
None => normal.sample(&mut rng),
};
let rho = if couple_depth {
group.depth_rho[g]
} else {
0.0
};
let z = rho * z_depth + (1.0 - rho * rho).max(0.0).sqrt() * e;
let u = phi(z as f64).clamp(1e-7, 1.0 - 1e-7);
let x = if fixed(budget, g) {
tables[k][b][g].partition_point(|&f| f < u) as u32
} else {
let m = (ln_m as f64).exp();
if !m.is_finite() || m <= 0.0 {
continue;
}
budget.marginal[g].inverse_cdf(u, m)
};
if x > 0 {
counts.push((g as u64, j as u64, x as f32));
}
}
counts
})
.flatten()
.collect();
let scaling = budgets
.into_iter()
.map(|b| TermScaling {
amp: b.topic_amp,
centre: b.ln_m1,
})
.collect();
Ok(ReferenceSample {
triplets,
groups,
scaling,
})
}
pub(crate) fn batch_centre(delta: &[DVector<f32>], membership: &[usize], dd: usize) -> Vec<f32> {
if membership.is_empty() || delta.is_empty() {
return vec![0.0; dd];
}
let mut counts = vec![0usize; delta.len()];
for &b in membership {
counts[b] += 1;
}
let n = membership.len() as f64;
(0..dd)
.map(|g| {
let mean: f64 = delta
.iter()
.zip(&counts)
.map(|(d, &c)| c as f64 / n * (d[g] as f64).exp())
.sum();
mean.ln() as f32
})
.collect()
}
struct DispersionBudget {
n_capped: usize,
n_over: usize,
topic_amp: Vec<f32>,
noise_amp: Vec<f32>,
with_noise: bool,
ln_m1: Vec<f32>,
marginal: Vec<Prepared>,
}
impl DispersionBudget {
fn new(
fit: &GlobalCopulaFit,
group: &GroupFit,
programs: &CellPrograms,
cells: &[usize],
topic: &(impl Fn(usize) -> DVector<f32> + Sync),
) -> Self {
let nn = cells.len();
if nn == 0 {
return Self {
n_capped: 0,
n_over: 0,
topic_amp: vec![0.0; fit.n_genes],
noise_amp: vec![0.0; fit.n_genes],
with_noise: false,
ln_m1: vec![0.0; fit.n_genes],
marginal: Vec::new(),
};
}
let dd = fit.n_genes;
let genes = &fit.active_genes;
let alpha_topic = (programs.pve_topic.max(0.0) as f64).sqrt();
let alpha_noise = (programs.pve_noise.max(0.0) as f64).sqrt();
let budget: Vec<f64> = group
.marginals
.iter()
.map(|m| m.rate_cv2().ln_1p())
.collect();
let mut topic_amp = vec![0.0_f32; dd];
let mut ln_m1 = vec![0.0_f32; dd];
let mut spread = vec![1.0_f64; dd];
let mut n_capped = 0;
if alpha_topic > 0.0 && nn > 0 {
let (mean_t, mean_t2) = first_two_moments(genes.len(), nn, |i, out| {
let t = topic(cells[i]);
for (o, &g) in out.iter_mut().zip(genes) {
*o = t[g] as f64;
}
});
for (i, &g) in genes.iter().enumerate() {
let v = mean_t2[i] - mean_t[i] * mean_t[i];
if v <= MIN_TOPIC_VARIANCE {
continue;
}
topic_amp[g] = alpha_topic.min((budget[g] / v).sqrt()) as f32;
}
let exp_moments = |amp: &[f32], idx: &[usize]| {
first_two_moments(idx.len(), nn, |i, out| {
let t = topic(cells[i]);
for (o, &g) in out.iter_mut().zip(idx) {
*o = ((amp[g] * t[g]) as f64).exp();
}
})
};
let (mut m1, mut m2) = exp_moments(&topic_amp, genes);
if programs.cap_spread {
let fits = |g: usize, a: f64, b: f64| {
b / (a * a).max(f64::MIN_POSITIVE) <= budget[g].exp()
};
let over: Vec<usize> = genes
.iter()
.enumerate()
.filter_map(|(i, &g)| (!fits(g, m1[i], m2[i])).then_some(g))
.collect();
if !over.is_empty() {
let mut lo = vec![0.0_f32; dd];
let mut hi = topic_amp.clone();
let mut trial = topic_amp.clone();
for _ in 0..SPREAD_STEPS {
for &g in &over {
trial[g] = 0.5 * (lo[g] + hi[g]);
}
let (a, b) = exp_moments(&trial, &over);
for (i, &g) in over.iter().enumerate() {
if fits(g, a[i], b[i]) {
lo[g] = trial[g];
} else {
hi[g] = trial[g];
}
}
}
for &g in &over {
topic_amp[g] = lo[g];
}
(m1, m2) = exp_moments(&topic_amp, genes);
}
}
n_capped = genes
.iter()
.filter(|&&g| {
let a = topic_amp[g] as f64;
a > 0.0 && a < alpha_topic * (1.0 - 1e-6)
})
.count();
for (i, &g) in genes.iter().enumerate() {
ln_m1[g] = m1[i].max(f64::MIN_POSITIVE).ln() as f32;
spread[g] = m2[i] / (m1[i] * m1[i]).max(f64::MIN_POSITIVE);
}
}
let mut noise_amp = vec![0.0_f32; dd];
for &g in genes {
let left = budget[g] - spread[g].ln();
noise_amp[g] = alpha_noise.min(left.max(0.0).sqrt()) as f32;
}
let left: Vec<(PoissonMixture, bool)> = (0..dd)
.into_par_iter()
.map(|g| {
if noise_amp[g] == 0.0 && spread[g] <= 1.0 {
return (group.marginals[g].clone(), true);
}
let b = noise_amp[g] as f64;
group.marginals[g].leaving_room_for(spread[g] * (b * b).exp())
})
.collect();
let n_over = genes.iter().filter(|&&g| !left[g].1).count();
Self {
n_capped,
n_over,
with_noise: noise_amp.iter().any(|&b| b > 0.0),
topic_amp,
noise_amp,
ln_m1,
marginal: left.into_iter().map(|(m, _)| m.prepare()).collect(),
}
}
}
pub(crate) fn first_two_moments(
n: usize,
nn: usize,
fill: impl Fn(usize, &mut [f64]) + Sync,
) -> (Vec<f64>, Vec<f64>) {
let blocks: Vec<(Vec<f64>, Vec<f64>)> = (0..nn.div_ceil(MOMENT_BLOCK))
.into_par_iter()
.map(|b| {
let (mut s1, mut s2, mut buf) = (vec![0.0; n], vec![0.0; n], vec![0.0; n]);
for j in (b * MOMENT_BLOCK)..((b + 1) * MOMENT_BLOCK).min(nn) {
fill(j, &mut buf);
for ((a, c), &x) in s1.iter_mut().zip(s2.iter_mut()).zip(&buf) {
*a += x;
*c += x * x;
}
}
(s1, s2)
})
.collect();
let (mut m1, mut m2) = (vec![0.0_f64; n], vec![0.0_f64; n]);
for (s1, s2) in blocks {
for i in 0..n {
m1[i] += s1[i];
m2[i] += s2[i];
}
}
let k = nn.max(1) as f64;
m1.iter_mut().for_each(|x| *x /= k);
m2.iter_mut().for_each(|x| *x /= k);
(m1, m2)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn batch_centre_is_the_log_mean_shift() {
let delta = vec![
DVector::from_vec(vec![0.5_f32, 0.0]),
DVector::from_vec(vec![-0.5, 0.0]),
];
let c = batch_centre(&delta, &[0, 0, 0, 1], 2);
let want = (0.75 * 0.5_f64.exp() + 0.25 * (-0.5_f64).exp()).ln();
assert!((c[0] as f64 - want).abs() < 1e-6);
assert_eq!(c[1], 0.0);
}
#[test]
fn moments_are_thread_independent_block_sums() {
let (m1, m2) = first_two_moments(2, 1000, |j, out| {
out[0] = j as f64;
out[1] = 1.0;
});
assert!((m1[0] - 499.5).abs() < 1e-9);
assert!((m1[1] - 1.0).abs() < 1e-12 && (m2[1] - 1.0).abs() < 1e-12);
}
}