1use crate::alg::dc_poisson::compact_labels;
30use crate::alg::hvg::select_hvg_streaming;
31use crate::alg::random_projection::{mixed_partition, RandProjOps};
32use crate::sparse_data_visitors::VisitColumnsOps;
33use crate::sparse_io_vector::SparseIoVec;
34use log::info;
35use nalgebra::DMatrix;
36use nalgebra_sparse::CscMatrix;
37use rayon::prelude::*;
38use std::collections::HashMap;
39use std::sync::Mutex;
40
41#[derive(Clone, Debug)]
42pub struct MixedRefineParams {
43 pub max_sweeps: usize,
45 pub max_fit_rounds: usize,
47 pub fit_tol: f64,
49 pub num_genes: usize,
51 pub pseudocount: f64,
53 pub min_gain: f64,
55 pub block_size: Option<usize>,
56}
57
58impl Default for MixedRefineParams {
59 fn default() -> Self {
60 Self {
61 max_sweeps: 5,
62 max_fit_rounds: 50,
63 fit_tol: 1e-4,
64 num_genes: 2000,
65 pseudocount: 1.0,
66 min_gain: 1.0,
67 block_size: None,
68 }
69 }
70}
71
72pub(crate) struct CodeNeighbours {
79 codes: Vec<usize>,
80 candidates_of_code: HashMap<usize, Vec<usize>>,
81}
82
83impl CodeNeighbours {
84 pub(crate) fn new(codes: Vec<usize>, labels: &[usize], bits: usize) -> Self {
87 let label_of_code: HashMap<usize, usize> =
88 codes.iter().copied().zip(labels.iter().copied()).collect();
89 let candidates_of_code = label_of_code
90 .keys()
91 .map(|&code| {
92 let mut groups: Vec<usize> = std::iter::once(code)
93 .chain((0..bits).map(|k| code ^ (1 << k)))
94 .filter_map(|c| label_of_code.get(&c).copied())
95 .collect();
96 groups.sort_unstable();
97 groups.dedup();
98 (code, groups)
99 })
100 .collect();
101 Self {
102 codes,
103 candidates_of_code,
104 }
105 }
106
107 pub(crate) fn candidates(&self, cell: usize) -> &[usize] {
109 &self.candidates_of_code[&self.codes[cell]]
110 }
111}
112
113pub(crate) trait CountBlocks: Sync {
119 fn num_genes(&self) -> usize;
120 fn visit_blocks<F>(&self, visit: &F) -> anyhow::Result<()>
123 where
124 F: Fn(usize, &CscMatrix<f32>) -> anyhow::Result<()> + Sync + Send;
125}
126
127impl CountBlocks for CscMatrix<f32> {
128 fn num_genes(&self) -> usize {
129 self.nrows()
130 }
131 fn visit_blocks<F>(&self, visit: &F) -> anyhow::Result<()>
132 where
133 F: Fn(usize, &CscMatrix<f32>) -> anyhow::Result<()> + Sync + Send,
134 {
135 visit(0, self)
136 }
137}
138
139struct DataBlocks<'a>(&'a SparseIoVec, Option<usize>);
141
142impl CountBlocks for DataBlocks<'_> {
143 fn num_genes(&self) -> usize {
144 self.0.num_rows()
145 }
146 fn visit_blocks<F>(&self, visit: &F) -> anyhow::Result<()>
147 where
148 F: Fn(usize, &CscMatrix<f32>) -> anyhow::Result<()> + Sync + Send,
149 {
150 let visitor = |(lb, ub): (usize, usize),
151 data: &SparseIoVec,
152 _: &(),
153 _: std::sync::Arc<Mutex<&mut ()>>|
154 -> anyhow::Result<()> { visit(lb, &data.read_columns_csc(lb..ub)?) };
155 self.0
156 .visit_columns_by_block(&visitor, &(), &mut (), self.1)
157 }
158}
159
160struct Totals {
166 y_ig: DMatrix<f64>,
168 depth: Vec<f64>,
169 rate: Vec<f64>,
171}
172
173fn accumulate<S: CountBlocks>(
176 source: &S,
177 labels: &[usize],
178 k: usize,
179 batch: Option<(&[usize], usize)>,
180) -> anyhow::Result<(DMatrix<f64>, Option<Totals>)> {
181 let ng = source.num_genes();
182 let n = labels.len();
183 let out = Mutex::new((
184 DMatrix::<f64>::zeros(k, ng),
185 batch.map(|(_, n_indv)| (DMatrix::<f64>::zeros(n_indv, ng), vec![0.0; n])),
186 ));
187 source.visit_blocks(&|lb, csc| {
188 let mut guard = out.lock().expect("sums lock");
189 let (y_pg, totals) = &mut *guard;
190 for (j, col) in csc.col_iter().enumerate() {
191 let c = lb + j;
192 for (&g, &v) in col.row_indices().iter().zip(col.values()) {
193 y_pg[(labels[c], g)] += f64::from(v);
194 }
195 if let (Some((y_ig, depth)), Some((batch, _))) = (totals.as_mut(), batch) {
196 for (&g, &v) in col.row_indices().iter().zip(col.values()) {
197 y_ig[(batch[c], g)] += f64::from(v);
198 }
199 depth[c] = col.values().iter().map(|&v| f64::from(v)).sum();
200 }
201 }
202 Ok(())
203 })?;
204 let (y_pg, totals) = out.into_inner().expect("sums lock");
205 let totals = totals.map(|(y_ig, depth)| {
206 let total_depth: f64 = depth.iter().sum();
207 let rate = y_ig
208 .row_sum()
209 .iter()
210 .map(|&y| y / total_depth.max(1.0))
211 .collect();
212 Totals { y_ig, depth, rate }
213 });
214 Ok((y_pg, totals))
215}
216
217struct Scoring {
224 ln_mu: DMatrix<f32>,
225 a: DMatrix<f64>,
226}
227
228fn fit(
232 y_pg: &DMatrix<f64>,
233 totals: &Totals,
234 n_pi: &DMatrix<f64>,
235 omega: &mut DMatrix<f64>,
236 params: &MixedRefineParams,
237) -> Scoring {
238 let prior = params.pseudocount;
239 let (k, ng) = y_pg.shape();
240 let mut mu = DMatrix::<f64>::zeros(k, ng);
241 for _ in 0..params.max_fit_rounds.max(1) {
242 let den = n_pi * &*omega;
243 mu = DMatrix::from_fn(k, ng, |p, g| {
244 (y_pg[(p, g)] + prior * totals.rate[g]) / (den[(p, g)] + prior)
245 });
246 let den = n_pi.tr_mul(&mu);
247 let next = DMatrix::from_fn(omega.nrows(), ng, |i, g| {
248 (totals.y_ig[(i, g)] + prior) / (den[(i, g)] + prior)
249 });
250 let change = next
251 .iter()
252 .zip(omega.iter())
253 .fold(0.0f64, |c, (a, b)| c.max((a / b).ln().abs()));
254 *omega = next;
255 if change < params.fit_tol {
256 break;
257 }
258 }
259 Scoring {
260 a: &mu * omega.transpose(),
261 ln_mu: mu.map(|m| m.ln() as f32),
262 }
263}
264
265pub(crate) fn refine_mixed_labels<S: CountBlocks>(
277 source: &S,
278 batch: &[usize],
279 labels: &mut [usize],
280 neighbours: &CodeNeighbours,
281 min_batches: usize,
282 params: &MixedRefineParams,
283) -> anyhow::Result<usize> {
284 let n = batch.len();
285 anyhow::ensure!(labels.len() == n, "one batch and label per cell");
286 let n_indv = batch.iter().max().map_or(0, |&m| m + 1);
287 let k = labels.iter().max().map_or(0, |&m| m + 1);
288
289 let mut count = vec![0usize; k * n_indv];
291 let mut distinct = vec![0usize; k];
292 for (&p, &i) in labels.iter().zip(batch) {
293 count[p * n_indv + i] += 1;
294 if count[p * n_indv + i] == 1 {
295 distinct[p] += 1;
296 }
297 }
298
299 let mut totals: Option<Totals> = None;
300 let mut omega = DMatrix::<f64>::from_element(n_indv, source.num_genes(), 1.0);
301 let mut total_moves = 0;
302 for sweep in 0..params.max_sweeps {
303 let first = totals.is_none().then_some((batch, n_indv));
304 let (y_pg, new_totals) = accumulate(source, labels, k, first)?;
305 let totals = &*totals.get_or_insert_with(|| new_totals.expect("totals on first pass"));
306 let mut n_pi = DMatrix::<f64>::zeros(k, n_indv);
307 for (c, &s) in totals.depth.iter().enumerate() {
308 n_pi[(labels[c], batch[c])] += s;
309 }
310 let scoring = fit(&y_pg, totals, &n_pi, &mut omega, params);
311
312 let proposals = Mutex::new(Vec::<(usize, usize)>::new());
314 let current: &[usize] = labels;
315 source.visit_blocks(&|lb, csc| {
316 let block: Vec<(usize, usize)> = (0..csc.ncols())
317 .into_par_iter()
318 .filter_map(|j| {
319 let c = lb + j;
320 let (p, i) = (current[c], batch[c]);
321 let col = csc.col(j);
322 let score = |q: usize| -> f64 {
323 let fit: f64 = col
324 .row_indices()
325 .iter()
326 .zip(col.values())
327 .map(|(&g, &v)| f64::from(v) * f64::from(scoring.ln_mu[(q, g)]))
328 .sum();
329 fit - totals.depth[c] * scoring.a[(q, i)]
330 };
331 let stay = score(p);
332 neighbours
333 .candidates(c)
334 .iter()
335 .filter(|&&q| q != p && distinct[q] > 0)
336 .map(|&q| (q, score(q) - stay))
337 .filter(|&(_, gain)| gain > params.min_gain)
338 .max_by(|x, y| x.1.total_cmp(&y.1))
339 .map(|(q, _)| (c, q))
340 })
341 .collect();
342 proposals.lock().expect("proposals lock").extend(block);
343 Ok(())
344 })?;
345 let mut proposals = proposals.into_inner().expect("proposals lock");
346 proposals.sort_unstable();
347
348 let mut moves = 0;
350 for (c, q) in proposals {
351 let (p, i) = (labels[c], batch[c]);
352 if count[p * n_indv + i] == 1 {
353 if distinct[p] <= min_batches {
354 continue;
355 }
356 distinct[p] -= 1;
357 }
358 count[p * n_indv + i] -= 1;
359 if count[q * n_indv + i] == 0 {
360 distinct[q] += 1;
361 }
362 count[q * n_indv + i] += 1;
363 labels[c] = q;
364 moves += 1;
365 }
366 info!(
367 "mixed refine sweep {}: {} of {} cells moved",
368 sweep + 1,
369 moves,
370 n
371 );
372 total_moves += moves;
373 if moves == 0 {
374 break;
375 }
376 }
377 Ok(total_moves)
378}
379
380pub trait MixedRefineOps {
385 fn partition_columns_to_refined_mixed_groups<T>(
390 &mut self,
391 proj_kn: &nalgebra::DMatrix<f32>,
392 num_features: Option<usize>,
393 batch_membership: &[T],
394 min_batches: usize,
395 merge_levels: usize,
396 params: &MixedRefineParams,
397 ) -> anyhow::Result<usize>
398 where
399 T: std::hash::Hash + Eq + Clone;
400}
401
402impl MixedRefineOps for SparseIoVec {
403 fn partition_columns_to_refined_mixed_groups<T>(
404 &mut self,
405 proj_kn: &nalgebra::DMatrix<f32>,
406 num_features: Option<usize>,
407 batch_membership: &[T],
408 min_batches: usize,
409 merge_levels: usize,
410 params: &MixedRefineParams,
411 ) -> anyhow::Result<usize>
412 where
413 T: std::hash::Hash + Eq + Clone,
414 {
415 let part = mixed_partition(
416 proj_kn,
417 num_features,
418 batch_membership,
419 min_batches,
420 merge_levels,
421 )?;
422 let mut labels = part.labels;
423
424 if params.max_sweeps > 0 && params.num_genes > 0 {
425 let num_genes = params.num_genes.min(self.num_rows());
426 let hvg = select_hvg_streaming(self, Some(num_genes), None, None, params.block_size)?;
427 let mut keep = vec![false; self.num_rows()];
428 for &g in &hvg.selected_indices {
429 keep[g] = true;
430 }
431 let mut scored = self.clone_for_collapse();
432 scored.mask_rows(&keep)?;
433
434 let (mut groups, k) = compact_labels(&labels);
435 let mut label_of_group = vec![0; k];
436 for (&g, &l) in groups.iter().zip(&labels) {
437 label_of_group[g] = l;
438 }
439 let neighbours = CodeNeighbours::new(part.codes, &groups, part.bits);
440 let moves = refine_mixed_labels(
441 &DataBlocks(&scored, params.block_size),
442 &part.batch,
443 &mut groups,
444 &neighbours,
445 part.min_batches,
446 params,
447 )?;
448 labels = groups.iter().map(|&g| label_of_group[g]).collect();
449 info!(
450 "mixed refine: {} moves over {} genes, groups keep at least {} individuals",
451 moves,
452 scored.num_rows(),
453 part.min_batches
454 );
455 }
456
457 self.assign_group_labels(&labels);
458 Ok(self.num_groups())
459 }
460}
461
462#[cfg(test)]
463#[path = "refine_mixed_tests.rs"]
464mod tests;