1use crate::qc_lib::{cell_verdicts_quiet, robust_band, QcConfig, QcMetrics, RobustBand, Verdicts};
14use rayon::prelude::*;
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
18pub enum Rule {
19 Genes,
22 Counts,
24 Mito,
26 Ribo,
28 GeneCells,
30}
31
32impl Rule {
33 pub fn label(self) -> &'static str {
35 match self {
36 Rule::Genes => "detected genes",
37 Rule::Counts => "total counts",
38 Rule::Mito => "mito fraction",
39 Rule::Ribo => "ribo fraction",
40 Rule::GeneCells => "cells per gene",
41 }
42 }
43
44 pub fn short(self) -> &'static str {
46 match self {
47 Rule::Genes => "genes",
48 Rule::Counts => "counts",
49 Rule::Mito => "mito",
50 Rule::Ribo => "ribo",
51 Rule::GeneCells => "gene cells",
52 }
53 }
54
55 pub fn on_cells(self) -> bool {
57 self != Rule::GeneCells
58 }
59
60 pub fn keeps_above(self) -> bool {
62 matches!(self, Rule::Genes | Rule::Counts | Rule::GeneCells)
63 }
64
65 pub fn count(self) -> bool {
67 self.keeps_above()
68 }
69}
70
71#[derive(Clone, Copy, Debug, PartialEq, Eq)]
73pub enum Mode {
74 Mad,
76 Value,
78}
79
80#[derive(Clone, Debug, Default)]
82pub struct RuleCount {
83 pub removes: usize,
85 pub only: usize,
87 pub pass_others: Vec<bool>,
89 pub fails: Vec<bool>,
91 pub only_mask: Vec<bool>,
93}
94
95pub struct CellRules {
97 metrics: QcMetrics,
98 cfg: QcConfig,
99 base: QcConfig,
101 rules: Vec<Rule>,
102 verdicts: Verdicts,
103 counts: Vec<RuleCount>,
104 bands: Vec<Option<RobustBand>>,
107 edges: Vec<Option<f32>>,
108 removed_cells: Vec<bool>,
110 removed_genes: Vec<bool>,
111 groups: Vec<(Vec<Rule>, usize)>,
113}
114
115impl CellRules {
116 pub fn new(metrics: QcMetrics, mut cfg: QcConfig) -> Self {
119 cfg.qc_histogram = false;
120 let mut rules = vec![Rule::Genes, Rule::Counts];
121 if metrics.mito_frac.is_some() {
122 rules.push(Rule::Mito);
123 }
124 if metrics.ribo_frac.is_some() {
125 rules.push(Rule::Ribo);
126 }
127 if metrics.feature_n_cells.is_some() {
128 rules.push(Rule::GeneCells);
129 }
130 let verdicts = cell_verdicts_quiet(&metrics, &cfg);
131 let mut out = Self {
132 metrics,
133 base: cfg.clone(),
134 cfg,
135 rules,
136 verdicts,
137 counts: vec![],
138 bands: vec![],
139 edges: vec![],
140 removed_cells: vec![],
141 removed_genes: vec![],
142 groups: vec![],
143 };
144 out.recount();
145 out
146 }
147
148 pub fn metrics(&self) -> &QcMetrics {
149 &self.metrics
150 }
151
152 pub fn config(&self) -> &QcConfig {
153 &self.cfg
154 }
155
156 pub fn rules(&self) -> &[Rule] {
158 &self.rules
159 }
160
161 pub fn verdicts(&self) -> &Verdicts {
163 &self.verdicts
164 }
165
166 fn index(&self, rule: Rule) -> usize {
167 self.position(rule).expect("a rule of the data")
168 }
169
170 fn position(&self, rule: Rule) -> Option<usize> {
173 self.rules.iter().position(|&r| r == rule)
174 }
175
176 pub fn count(&self, rule: Rule) -> &RuleCount {
178 &self.counts[self.index(rule)]
179 }
180
181 pub fn removed(&self, rule: Rule) -> &[bool] {
183 match rule.on_cells() {
184 true => &self.removed_cells,
185 false => &self.removed_genes,
186 }
187 }
188
189 pub fn band(&self, rule: Rule) -> Option<RobustBand> {
193 self.bands[self.position(rule)?]
194 }
195
196 pub fn kept_cells(&self) -> Vec<usize> {
198 (0..self.verdicts.train_keep.len())
199 .filter(|&c| self.verdicts.emitted(c))
200 .collect()
201 }
202
203 pub fn kept_genes(&self) -> Vec<usize> {
205 (0..self.verdicts.feature_keep.len())
206 .filter(|&r| self.verdicts.feature_keep[r])
207 .collect()
208 }
209
210 pub fn values(&self, rule: Rule) -> &[f32] {
212 let m = &self.metrics;
213 match rule {
214 Rule::Genes => &m.n_genes,
215 Rule::Counts => &m.total_counts,
216 Rule::Mito => m.mito_frac.as_deref().unwrap_or(&[]),
217 Rule::Ribo => m.ribo_frac.as_deref().unwrap_or(&[]),
218 Rule::GeneCells => m.feature_n_cells.as_deref().unwrap_or(&[]),
219 }
220 }
221
222 pub fn is_on(&self, rule: Rule) -> bool {
227 self.position(rule).is_some() && is_on(&self.cfg, rule)
228 }
229
230 pub fn mode(&self, rule: Rule) -> Mode {
232 let c = &self.cfg;
233 let mad = match rule {
234 Rule::Genes => c.mad_on_n_genes,
235 Rule::Counts => c.mad_on_counts,
236 Rule::Mito => c.mad_on_mito,
237 Rule::Ribo | Rule::GeneCells => false,
238 };
239 if mad {
240 Mode::Mad
241 } else {
242 Mode::Value
243 }
244 }
245
246 pub fn banded(rule: Rule) -> bool {
248 matches!(rule, Rule::Genes | Rule::Counts | Rule::Mito)
249 }
250
251 pub fn edge(&self, rule: Rule) -> Option<f32> {
255 self.edges[self.position(rule)?]
256 }
257
258 fn fit_band(&self, rule: Rule, consider: &[bool]) -> Option<RobustBand> {
260 let (values, log1p) = match rule {
261 Rule::Genes => (&self.metrics.n_genes[..], true),
262 Rule::Counts => (&self.metrics.total_counts[..], true),
263 Rule::Mito => (self.metrics.mito_frac.as_deref()?, false),
264 Rule::Ribo => (self.metrics.ribo_frac.as_deref()?, false),
265 Rule::GeneCells => return None,
266 };
267 robust_band(values, self.cfg.n_mads, log1p, Some(consider)).map(|b| b.raw(log1p))
268 }
269
270 fn fit_edge(&self, rule: Rule, band: Option<RobustBand>) -> Option<f32> {
272 let c = &self.cfg;
273 let tightest = |parts: [Option<f32>; 3], above: bool| {
274 parts
275 .into_iter()
276 .flatten()
277 .reduce(|a, b| if above { a.max(b) } else { a.min(b) })
278 };
279 match rule {
280 Rule::Genes => tightest(
281 [
282 (c.min_cell_nnz > 0).then_some(c.min_cell_nnz as f32),
283 c.mad_on_n_genes.then(|| band.map(|b| b.lo)).flatten(),
284 c.auto_cell_cutoff
285 .then(|| {
286 crate::qc::suggest_nnz_cutoff(&self.metrics.n_genes).map(|k| k as f32)
287 })
288 .flatten(),
289 ],
290 true,
291 ),
292 Rule::Counts => tightest(
293 [
294 (c.min_counts_per_cell > 0.0).then_some(c.min_counts_per_cell),
295 c.mad_on_counts.then(|| band.map(|b| b.lo)).flatten(),
296 None,
297 ],
298 true,
299 ),
300 Rule::Mito => tightest(
301 [
302 c.mito_max_frac,
303 c.mad_on_mito.then(|| band.map(|b| b.hi)).flatten(),
304 None,
305 ],
306 false,
307 ),
308 Rule::Ribo => c.ribo_max_frac,
309 Rule::GeneCells => (c.feature_min_cells > 0).then_some(c.feature_min_cells as f32),
310 }
311 }
312
313 pub fn set_on(&mut self, rule: Rule, on: bool) {
317 if on == self.is_on(rule) {
318 return;
319 }
320 if on {
321 restore(&mut self.cfg, &self.base, rule);
322 if !is_on(&self.cfg, rule) {
323 let max = self.values(rule).iter().copied().fold(0.0f32, f32::max);
324 match rule {
325 Rule::Genes => self.cfg.mad_on_n_genes = true,
326 Rule::Counts => self.cfg.mad_on_counts = true,
327 Rule::Mito => self.cfg.mad_on_mito = true,
328 Rule::Ribo => self.cfg.ribo_max_frac = Some(max),
329 Rule::GeneCells => self.cfg.feature_min_cells = 1,
330 }
331 }
332 } else {
333 turn_off(&mut self.cfg, rule);
334 }
335 self.recount();
336 }
337
338 pub fn set_value(&mut self, rule: Rule, value: f32) {
340 let c = &mut self.cfg;
341 let value = value.max(0.0);
342 match rule {
343 Rule::Genes => {
344 c.mad_on_n_genes = false;
345 c.auto_cell_cutoff = false;
346 c.min_cell_nnz = value.round() as usize;
347 }
348 Rule::Counts => {
349 c.mad_on_counts = false;
350 c.min_counts_per_cell = value;
351 }
352 Rule::Mito => {
353 c.mad_on_mito = false;
354 c.mito_max_frac = Some(value.min(1.0));
355 }
356 Rule::Ribo => c.ribo_max_frac = Some(value.min(1.0)),
357 Rule::GeneCells => c.feature_min_cells = value.round() as usize,
358 }
359 self.recount();
360 }
361
362 pub fn set_mads(&mut self, n_mads: f32) {
364 self.cfg.n_mads = n_mads.clamp(0.5, 50.0);
365 self.recount();
366 }
367
368 pub fn toggle_mode(&mut self, rule: Rule) {
372 if !Self::banded(rule) {
373 return;
374 }
375 match self.mode(rule) {
376 Mode::Mad => {
377 let edge = self.edge(rule).unwrap_or_else(|| self.keeps_all(rule));
380 self.set_value(rule, edge);
381 }
382 Mode::Value => {
383 let (c, b) = (&mut self.cfg, &self.base);
384 match rule {
385 Rule::Genes => {
386 c.mad_on_n_genes = true;
387 c.min_cell_nnz = b.min_cell_nnz;
388 c.auto_cell_cutoff = b.auto_cell_cutoff;
389 }
390 Rule::Counts => {
391 c.mad_on_counts = true;
392 c.min_counts_per_cell = b.min_counts_per_cell;
393 }
394 Rule::Mito => {
395 c.mad_on_mito = true;
396 c.mito_max_frac = b.mito_max_frac;
397 }
398 Rule::Ribo | Rule::GeneCells => {}
399 }
400 self.recount();
401 }
402 }
403 }
404
405 fn keeps_all(&self, rule: Rule) -> f32 {
408 match rule.keeps_above() {
409 true => 0.0,
410 false => self.values(rule).iter().copied().fold(0.0f32, f32::max),
411 }
412 }
413
414 pub fn reset(&mut self) {
416 self.cfg = self.base.clone();
417 self.recount();
418 }
419
420 pub fn overlap(&self) -> &[(Vec<Rule>, usize)] {
424 &self.groups
425 }
426
427 fn group(&self) -> Vec<(Vec<Rule>, usize)> {
428 let cell_rules: Vec<(usize, Rule)> = self
429 .rules
430 .iter()
431 .copied()
432 .enumerate()
433 .filter(|(_, r)| r.on_cells())
434 .collect();
435 let mut groups: Vec<(Vec<Rule>, usize)> = vec![];
436 for c in (0..self.removed_cells.len()).filter(|&c| self.removed_cells[c]) {
437 let set: Vec<Rule> = cell_rules
438 .iter()
439 .filter(|(i, _)| self.counts[*i].fails[c])
440 .map(|&(_, r)| r)
441 .collect();
442 match groups.iter_mut().find(|(s, _)| *s == set) {
443 Some(g) => g.1 += 1,
444 None => groups.push((set, 1)),
445 }
446 }
447 groups.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.len().cmp(&b.0.len())));
448 groups
449 }
450
451 pub fn flags(&self) -> Vec<String> {
457 let (c, d) = (&self.cfg, &QcConfig::default());
458 let mut f = vec![];
459 let mut pair = |k: &str, v: String| {
460 f.push(k.to_string());
461 f.push(v);
462 };
463 if (c.mad_on_n_genes || c.mad_on_counts || c.mad_on_mito) && c.n_mads != d.n_mads {
464 pair("--qc-mads", fmt_value(c.n_mads));
465 }
466 if c.min_cell_nnz != d.min_cell_nnz {
467 pair("--qc-min-cell-nnz", c.min_cell_nnz.to_string());
468 }
469 if c.min_counts_per_cell != d.min_counts_per_cell {
470 pair("--qc-min-counts", fmt_value(c.min_counts_per_cell));
471 }
472 match &c.mito_pattern {
473 Some(p) => pair("--qc-mito-pattern", p.clone()),
474 None => pair("--qc-mito-pattern", String::new()),
475 }
476 if let Some(v) = c.mito_max_frac {
477 pair("--qc-mito-max-frac", fmt_value(v));
478 }
479 if let Some(p) = &c.ribo_pattern {
480 pair("--qc-ribo-pattern", p.clone());
481 }
482 if let Some(v) = c.ribo_max_frac {
483 pair("--qc-ribo-max-frac", fmt_value(v));
484 }
485 if c.feature_min_cells != d.feature_min_cells || c.auto_cell_cutoff {
486 pair("--qc-feature-min-cells", c.feature_min_cells.to_string());
487 }
488 if !c.mad_on_n_genes {
489 f.push("--qc-mad-on-genes=false".into());
490 }
491 if !c.mad_on_counts {
492 f.push("--qc-mad-on-counts=false".into());
493 }
494 if c.mito_pattern.is_some() && !c.mad_on_mito {
495 f.push("--qc-mad-on-mito=false".into());
496 }
497 if c.auto_cell_cutoff {
498 f.push("--qc-auto-cutoff".into());
499 }
500 f
501 }
502
503 fn recount(&mut self) {
507 self.verdicts = cell_verdicts_quiet(&self.metrics, &self.cfg);
508 let all = &self.verdicts;
509 let n = all.train_keep.len();
510 self.removed_cells = (0..n).map(|c| !all.emitted(c)).collect();
511 self.removed_genes = all.feature_keep.iter().map(|&k| !k).collect();
512 let (metrics, cfg, rules) = (&self.metrics, &self.cfg, &self.rules);
513 let (removed_cells, removed_genes) = (&self.removed_cells, &self.removed_genes);
514 self.counts = rules
515 .par_iter()
516 .map(|&rule| {
517 let mut without = cfg.clone();
518 turn_off(&mut without, rule);
519 let mut alone = cfg.clone();
520 for &other in rules.iter().filter(|&&o| o != rule) {
521 turn_off(&mut alone, other);
522 }
523 let (w, a) = rayon::join(
524 || cell_verdicts_quiet(metrics, &without),
525 || cell_verdicts_quiet(metrics, &alone),
526 );
527 let (pass_others, fails, removed): (Vec<bool>, Vec<bool>, &[bool]) =
528 if rule.on_cells() {
529 (
530 (0..n).map(|c| w.emitted(c)).collect(),
531 (0..n).map(|c| is_on(cfg, rule) && !a.emitted(c)).collect(),
532 removed_cells,
533 )
534 } else {
535 (
536 w.feature_keep.clone(),
537 a.feature_keep.iter().map(|&k| !k).collect(),
538 removed_genes,
539 )
540 };
541 let only_mask: Vec<bool> = (0..removed.len())
542 .map(|i| removed[i] && pass_others[i])
543 .collect();
544 RuleCount {
545 removes: fails.iter().filter(|&&f| f).count(),
546 only: only_mask.iter().filter(|&&o| o).count(),
547 pass_others,
548 fails,
549 only_mask,
550 }
551 })
552 .collect();
553 let consider: Vec<bool> = self
555 .metrics
556 .n_genes
557 .iter()
558 .map(|&g| (g as usize) >= self.cfg.min_cell_nnz)
559 .collect();
560 self.bands = self
561 .rules
562 .iter()
563 .map(|&r| self.fit_band(r, &consider))
564 .collect();
565 self.edges = (0..self.rules.len())
566 .map(|i| self.fit_edge(self.rules[i], self.bands[i]))
567 .collect();
568 self.groups = self.group();
569 }
570}
571
572fn is_on(c: &QcConfig, rule: Rule) -> bool {
573 match rule {
574 Rule::Genes => c.mad_on_n_genes || c.min_cell_nnz > 0 || c.auto_cell_cutoff,
575 Rule::Counts => c.mad_on_counts || c.min_counts_per_cell > 0.0,
576 Rule::Mito => c.mad_on_mito || c.mito_max_frac.is_some(),
577 Rule::Ribo => c.ribo_max_frac.is_some(),
578 Rule::GeneCells => c.feature_min_cells > 0,
579 }
580}
581
582fn turn_off(c: &mut QcConfig, rule: Rule) {
583 match rule {
584 Rule::Genes => {
585 c.mad_on_n_genes = false;
586 c.min_cell_nnz = 0;
587 c.auto_cell_cutoff = false;
588 }
589 Rule::Counts => {
590 c.mad_on_counts = false;
591 c.min_counts_per_cell = 0.0;
592 }
593 Rule::Mito => {
594 c.mad_on_mito = false;
595 c.mito_max_frac = None;
596 }
597 Rule::Ribo => c.ribo_max_frac = None,
598 Rule::GeneCells => c.feature_min_cells = 0,
599 }
600}
601
602fn restore(c: &mut QcConfig, b: &QcConfig, rule: Rule) {
603 match rule {
604 Rule::Genes => {
605 c.mad_on_n_genes = b.mad_on_n_genes;
606 c.min_cell_nnz = b.min_cell_nnz;
607 c.auto_cell_cutoff = b.auto_cell_cutoff;
608 }
609 Rule::Counts => {
610 c.mad_on_counts = b.mad_on_counts;
611 c.min_counts_per_cell = b.min_counts_per_cell;
612 }
613 Rule::Mito => {
614 c.mad_on_mito = b.mad_on_mito;
615 c.mito_max_frac = b.mito_max_frac;
616 }
617 Rule::Ribo => c.ribo_max_frac = b.ribo_max_frac,
618 Rule::GeneCells => c.feature_min_cells = b.feature_min_cells,
619 }
620}
621
622fn fmt_value(v: f32) -> String {
624 format!("{v}")
625}
626
627#[cfg(test)]
628#[path = "qc_rules/tests.rs"]
629mod tests;