Skip to main content

data_beans/
qc_rules.rs

1//! Cell QC as rules one can move: the settings of [`crate::qc_lib`] seen
2//! rule by rule, each with the edge it holds a metric to, how many cells it
3//! fails on its own, and how many it alone removes.
4//!
5//! Nothing here decides anything [`cell_verdicts`] does not: a [`CellRules`]
6//! holds a [`QcConfig`] and the [`QcMetrics`] measured once, and every count
7//! is that function run again under the config with a rule turned off or
8//! left alone. Moving a rule changes the config, so [`CellRules::flags`]
9//! gives `qc` flags that decide the same cells. What a frame draws (the
10//! bands, edges, removed masks and their overlap) is worked out once per
11//! change, in [`CellRules::recount`], not per frame.
12
13use crate::qc_lib::{cell_verdicts_quiet, robust_band, QcConfig, QcMetrics, RobustBand, Verdicts};
14use rayon::prelude::*;
15
16/// One rule of cell QC, and the gene rule.
17#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
18pub enum Rule {
19    /// Detected genes per cell: the near-empty floor, the MAD band's lower
20    /// edge, and the automatic cutoff.
21    Genes,
22    /// Total counts per cell: the MAD band's lower edge and the floor.
23    Counts,
24    /// Mitochondrial fraction: the MAD band's upper edge and the cap.
25    Mito,
26    /// Ribosomal fraction: the cap.
27    Ribo,
28    /// Cells detecting each gene (the feature axis).
29    GeneCells,
30}
31
32impl Rule {
33    /// What a table or figure calls it.
34    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    /// A short name, for the overlap of rules a cell fails.
45    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    /// Whether it decides cells (else genes).
56    pub fn on_cells(self) -> bool {
57        self != Rule::GeneCells
58    }
59
60    /// Whether it keeps what is at or above its edge (else at or below).
61    pub fn keeps_above(self) -> bool {
62        matches!(self, Rule::Genes | Rule::Counts | Rule::GeneCells)
63    }
64
65    /// Whether its metric is a count, read on a log axis (else a fraction).
66    pub fn count(self) -> bool {
67        self.keeps_above()
68    }
69}
70
71/// How a rule's edge is set.
72#[derive(Clone, Copy, Debug, PartialEq, Eq)]
73pub enum Mode {
74    /// By the MAD band (`--qc-mads`, shared by every banded rule).
75    Mad,
76    /// By a value of its own.
77    Value,
78}
79
80/// What one rule does under the current settings.
81#[derive(Clone, Debug, Default)]
82pub struct RuleCount {
83    /// Cells (genes, for [`Rule::GeneCells`]) it fails on its own.
84    pub removes: usize,
85    /// Of those, the ones no other rule removes.
86    pub only: usize,
87    /// Which pass every other rule.
88    pub pass_others: Vec<bool>,
89    /// Which it fails on its own.
90    pub fails: Vec<bool>,
91    /// Which only it removes: removed, and passing every other rule.
92    pub only_mask: Vec<bool>,
93}
94
95/// Cell QC as rules, with what each does.
96pub struct CellRules {
97    metrics: QcMetrics,
98    cfg: QcConfig,
99    /// The settings it started from, which a rule turned back on returns to.
100    base: QcConfig,
101    rules: Vec<Rule>,
102    verdicts: Verdicts,
103    counts: Vec<RuleCount>,
104    /// Per rule, in table order: its metric's robust band, raw, and the
105    /// edge it holds the metric to.
106    bands: Vec<Option<RobustBand>>,
107    edges: Vec<Option<f32>>,
108    /// Whether each cell, and each gene, is removed.
109    removed_cells: Vec<bool>,
110    removed_genes: Vec<bool>,
111    /// The removed cells by the rules they fail; see [`CellRules::overlap`].
112    groups: Vec<(Vec<Rule>, usize)>,
113}
114
115impl CellRules {
116    /// Rules over `metrics` (with its per-gene cell counts measured) under
117    /// `cfg`. The histogram printing of `cfg` is left to `compute_qc`.
118    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    /// The rules the data has, in table order.
157    pub fn rules(&self) -> &[Rule] {
158        &self.rules
159    }
160
161    /// The verdicts under the current settings.
162    pub fn verdicts(&self) -> &Verdicts {
163        &self.verdicts
164    }
165
166    fn index(&self, rule: Rule) -> usize {
167        self.rules
168            .iter()
169            .position(|&r| r == rule)
170            .expect("a rule of the data")
171    }
172
173    /// What `rule` does now.
174    pub fn count(&self, rule: Rule) -> &RuleCount {
175        &self.counts[self.index(rule)]
176    }
177
178    /// Whether each cell (each gene, for [`Rule::GeneCells`]) is removed now.
179    pub fn removed(&self, rule: Rule) -> &[bool] {
180        match rule.on_cells() {
181            true => &self.removed_cells,
182            false => &self.removed_genes,
183        }
184    }
185
186    /// The robust band of `rule`'s metric on its own scale, as qc_lib fits
187    /// it (over the cells above the near-empty floor), whether or not the
188    /// rule holds to it; `None` for a rule with no band, or no spread.
189    pub fn band(&self, rule: Rule) -> Option<RobustBand> {
190        self.bands[self.index(rule)]
191    }
192
193    /// The cells QC keeps (in the output), and how many there are.
194    pub fn kept_cells(&self) -> Vec<usize> {
195        (0..self.verdicts.train_keep.len())
196            .filter(|&c| self.verdicts.emitted(c))
197            .collect()
198    }
199
200    /// The genes QC keeps.
201    pub fn kept_genes(&self) -> Vec<usize> {
202        (0..self.verdicts.feature_keep.len())
203            .filter(|&r| self.verdicts.feature_keep[r])
204            .collect()
205    }
206
207    /// The values `rule` is held to: one per cell, or per gene.
208    pub fn values(&self, rule: Rule) -> &[f32] {
209        let m = &self.metrics;
210        match rule {
211            Rule::Genes => &m.n_genes,
212            Rule::Counts => &m.total_counts,
213            Rule::Mito => m.mito_frac.as_deref().unwrap_or(&[]),
214            Rule::Ribo => m.ribo_frac.as_deref().unwrap_or(&[]),
215            Rule::GeneCells => m.feature_n_cells.as_deref().unwrap_or(&[]),
216        }
217    }
218
219    /// Whether `rule` decides anything now.
220    pub fn is_on(&self, rule: Rule) -> bool {
221        is_on(&self.cfg, rule)
222    }
223
224    /// How `rule`'s edge is set now.
225    pub fn mode(&self, rule: Rule) -> Mode {
226        let c = &self.cfg;
227        let mad = match rule {
228            Rule::Genes => c.mad_on_n_genes,
229            Rule::Counts => c.mad_on_counts,
230            Rule::Mito => c.mad_on_mito,
231            Rule::Ribo | Rule::GeneCells => false,
232        };
233        if mad {
234            Mode::Mad
235        } else {
236            Mode::Value
237        }
238    }
239
240    /// Whether `rule` can be set by the MAD band.
241    pub fn banded(rule: Rule) -> bool {
242        matches!(rule, Rule::Genes | Rule::Counts | Rule::Mito)
243    }
244
245    /// The edge `rule` holds its metric to now, on the metric's own scale:
246    /// the tightest of its band edge and its own value; `None` when it is
247    /// off or its band has no spread.
248    pub fn edge(&self, rule: Rule) -> Option<f32> {
249        self.edges[self.index(rule)]
250    }
251
252    /// The band of `rule`'s metric fitted now, for [`CellRules::recount`].
253    fn fit_band(&self, rule: Rule, consider: &[bool]) -> Option<RobustBand> {
254        let (values, log1p) = match rule {
255            Rule::Genes => (&self.metrics.n_genes[..], true),
256            Rule::Counts => (&self.metrics.total_counts[..], true),
257            Rule::Mito => (self.metrics.mito_frac.as_deref()?, false),
258            Rule::Ribo => (self.metrics.ribo_frac.as_deref()?, false),
259            Rule::GeneCells => return None,
260        };
261        robust_band(values, self.cfg.n_mads, log1p, Some(consider)).map(|b| b.raw(log1p))
262    }
263
264    /// The edge of `rule` under the current settings, from its fitted band.
265    fn fit_edge(&self, rule: Rule, band: Option<RobustBand>) -> Option<f32> {
266        let c = &self.cfg;
267        let tightest = |parts: [Option<f32>; 3], above: bool| {
268            parts
269                .into_iter()
270                .flatten()
271                .reduce(|a, b| if above { a.max(b) } else { a.min(b) })
272        };
273        match rule {
274            Rule::Genes => tightest(
275                [
276                    (c.min_cell_nnz > 0).then_some(c.min_cell_nnz as f32),
277                    c.mad_on_n_genes.then(|| band.map(|b| b.lo)).flatten(),
278                    c.auto_cell_cutoff
279                        .then(|| {
280                            crate::qc::suggest_nnz_cutoff(&self.metrics.n_genes).map(|k| k as f32)
281                        })
282                        .flatten(),
283                ],
284                true,
285            ),
286            Rule::Counts => tightest(
287                [
288                    (c.min_counts_per_cell > 0.0).then_some(c.min_counts_per_cell),
289                    c.mad_on_counts.then(|| band.map(|b| b.lo)).flatten(),
290                    None,
291                ],
292                true,
293            ),
294            Rule::Mito => tightest(
295                [
296                    c.mito_max_frac,
297                    c.mad_on_mito.then(|| band.map(|b| b.hi)).flatten(),
298                    None,
299                ],
300                false,
301            ),
302            Rule::Ribo => c.ribo_max_frac,
303            Rule::GeneCells => (c.feature_min_cells > 0).then_some(c.feature_min_cells as f32),
304        }
305    }
306
307    /// Turn `rule` off, or back on as the starting settings had it (by the
308    /// MAD band where it has one; a cap where it has none, at the largest
309    /// value, keeping everything until it is moved).
310    pub fn set_on(&mut self, rule: Rule, on: bool) {
311        if on == self.is_on(rule) {
312            return;
313        }
314        if on {
315            restore(&mut self.cfg, &self.base, rule);
316            if !is_on(&self.cfg, rule) {
317                let max = self.values(rule).iter().copied().fold(0.0f32, f32::max);
318                match rule {
319                    Rule::Genes => self.cfg.mad_on_n_genes = true,
320                    Rule::Counts => self.cfg.mad_on_counts = true,
321                    Rule::Mito => self.cfg.mad_on_mito = true,
322                    Rule::Ribo => self.cfg.ribo_max_frac = Some(max),
323                    Rule::GeneCells => self.cfg.feature_min_cells = 1,
324                }
325            }
326        } else {
327            turn_off(&mut self.cfg, rule);
328        }
329        self.recount();
330    }
331
332    /// Hold `rule` to `value` of its own, off the MAD band.
333    pub fn set_value(&mut self, rule: Rule, value: f32) {
334        let c = &mut self.cfg;
335        let value = value.max(0.0);
336        match rule {
337            Rule::Genes => {
338                c.mad_on_n_genes = false;
339                c.auto_cell_cutoff = false;
340                c.min_cell_nnz = value.round() as usize;
341            }
342            Rule::Counts => {
343                c.mad_on_counts = false;
344                c.min_counts_per_cell = value;
345            }
346            Rule::Mito => {
347                c.mad_on_mito = false;
348                c.mito_max_frac = Some(value.min(1.0));
349            }
350            Rule::Ribo => c.ribo_max_frac = Some(value.min(1.0)),
351            Rule::GeneCells => c.feature_min_cells = value.round() as usize,
352        }
353        self.recount();
354    }
355
356    /// Set the MAD band's width, which every banded rule shares.
357    pub fn set_mads(&mut self, n_mads: f32) {
358        self.cfg.n_mads = n_mads.clamp(0.5, 50.0);
359        self.recount();
360    }
361
362    /// Switch `rule` between the MAD band and a value of its own, the value
363    /// starting where the edge is now. Back on the band, its own value
364    /// returns to the starting one.
365    pub fn toggle_mode(&mut self, rule: Rule) {
366        if !Self::banded(rule) {
367            return;
368        }
369        match self.mode(rule) {
370            Mode::Mad => {
371                // From where the edge is; with none (a band with no spread),
372                // from a value that keeps everything.
373                let edge = self.edge(rule).unwrap_or_else(|| self.keeps_all(rule));
374                self.set_value(rule, edge);
375            }
376            Mode::Value => {
377                let (c, b) = (&mut self.cfg, &self.base);
378                match rule {
379                    Rule::Genes => {
380                        c.mad_on_n_genes = true;
381                        c.min_cell_nnz = b.min_cell_nnz;
382                        c.auto_cell_cutoff = b.auto_cell_cutoff;
383                    }
384                    Rule::Counts => {
385                        c.mad_on_counts = true;
386                        c.min_counts_per_cell = b.min_counts_per_cell;
387                    }
388                    Rule::Mito => {
389                        c.mad_on_mito = true;
390                        c.mito_max_frac = b.mito_max_frac;
391                    }
392                    Rule::Ribo | Rule::GeneCells => {}
393                }
394                self.recount();
395            }
396        }
397    }
398
399    /// A value of `rule`'s own that removes nothing: 0 for a floor, the
400    /// largest value for a cap.
401    fn keeps_all(&self, rule: Rule) -> f32 {
402        match rule.keeps_above() {
403            true => 0.0,
404            false => self.values(rule).iter().copied().fold(0.0f32, f32::max),
405        }
406    }
407
408    /// Back to the starting settings.
409    pub fn reset(&mut self) {
410        self.cfg = self.base.clone();
411        self.recount();
412    }
413
414    /// The removed cells grouped by the cell rules each fails on its own,
415    /// largest group first. A cell removed only as the rules act together
416    /// (a band refitted without a floor, say) has an empty set.
417    pub fn overlap(&self) -> &[(Vec<Rule>, usize)] {
418        &self.groups
419    }
420
421    fn group(&self) -> Vec<(Vec<Rule>, usize)> {
422        let cell_rules: Vec<(usize, Rule)> = self
423            .rules
424            .iter()
425            .copied()
426            .enumerate()
427            .filter(|(_, r)| r.on_cells())
428            .collect();
429        let mut groups: Vec<(Vec<Rule>, usize)> = vec![];
430        for c in (0..self.removed_cells.len()).filter(|&c| self.removed_cells[c]) {
431            let set: Vec<Rule> = cell_rules
432                .iter()
433                .filter(|(i, _)| self.counts[*i].fails[c])
434                .map(|&(_, r)| r)
435                .collect();
436            match groups.iter_mut().find(|(s, _)| *s == set) {
437                Some(g) => g.1 += 1,
438                None => groups.push((set, 1)),
439            }
440        }
441        groups.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.len().cmp(&b.0.len())));
442        groups
443    }
444
445    /// `qc` flags that decide what these settings do, past `qc`'s defaults.
446    /// The mito pattern is always given, an empty one turning the metric off
447    /// (`qc` starts from a pattern of its own); under `--qc-auto-cutoff` the
448    /// gene rule is always given too, 0 for off, as `qc` puts it at the
449    /// trough only when none is given.
450    pub fn flags(&self) -> Vec<String> {
451        let (c, d) = (&self.cfg, &QcConfig::default());
452        let mut f = vec![];
453        let mut pair = |k: &str, v: String| {
454            f.push(k.to_string());
455            f.push(v);
456        };
457        if (c.mad_on_n_genes || c.mad_on_counts || c.mad_on_mito) && c.n_mads != d.n_mads {
458            pair("--qc-mads", fmt_value(c.n_mads));
459        }
460        if c.min_cell_nnz != d.min_cell_nnz {
461            pair("--qc-min-cell-nnz", c.min_cell_nnz.to_string());
462        }
463        if c.min_counts_per_cell != d.min_counts_per_cell {
464            pair("--qc-min-counts", fmt_value(c.min_counts_per_cell));
465        }
466        match &c.mito_pattern {
467            Some(p) => pair("--qc-mito-pattern", p.clone()),
468            None => pair("--qc-mito-pattern", String::new()),
469        }
470        if let Some(v) = c.mito_max_frac {
471            pair("--qc-mito-max-frac", fmt_value(v));
472        }
473        if let Some(p) = &c.ribo_pattern {
474            pair("--qc-ribo-pattern", p.clone());
475        }
476        if let Some(v) = c.ribo_max_frac {
477            pair("--qc-ribo-max-frac", fmt_value(v));
478        }
479        if c.feature_min_cells != d.feature_min_cells || c.auto_cell_cutoff {
480            pair("--qc-feature-min-cells", c.feature_min_cells.to_string());
481        }
482        if !c.mad_on_n_genes {
483            f.push("--qc-mad-on-genes=false".into());
484        }
485        if !c.mad_on_counts {
486            f.push("--qc-mad-on-counts=false".into());
487        }
488        if c.mito_pattern.is_some() && !c.mad_on_mito {
489            f.push("--qc-mad-on-mito=false".into());
490        }
491        if c.auto_cell_cutoff {
492            f.push("--qc-auto-cutoff".into());
493        }
494        f
495    }
496
497    /// Decide again under the current settings, what each rule does (the
498    /// config with it off and with it alone, decided in parallel), and what
499    /// a frame draws from them.
500    fn recount(&mut self) {
501        self.verdicts = cell_verdicts_quiet(&self.metrics, &self.cfg);
502        let all = &self.verdicts;
503        let n = all.train_keep.len();
504        self.removed_cells = (0..n).map(|c| !all.emitted(c)).collect();
505        self.removed_genes = all.feature_keep.iter().map(|&k| !k).collect();
506        let (metrics, cfg, rules) = (&self.metrics, &self.cfg, &self.rules);
507        let (removed_cells, removed_genes) = (&self.removed_cells, &self.removed_genes);
508        self.counts = rules
509            .par_iter()
510            .map(|&rule| {
511                let mut without = cfg.clone();
512                turn_off(&mut without, rule);
513                let mut alone = cfg.clone();
514                for &other in rules.iter().filter(|&&o| o != rule) {
515                    turn_off(&mut alone, other);
516                }
517                let (w, a) = rayon::join(
518                    || cell_verdicts_quiet(metrics, &without),
519                    || cell_verdicts_quiet(metrics, &alone),
520                );
521                let (pass_others, fails, removed): (Vec<bool>, Vec<bool>, &[bool]) =
522                    if rule.on_cells() {
523                        (
524                            (0..n).map(|c| w.emitted(c)).collect(),
525                            (0..n).map(|c| is_on(cfg, rule) && !a.emitted(c)).collect(),
526                            removed_cells,
527                        )
528                    } else {
529                        (
530                            w.feature_keep.clone(),
531                            a.feature_keep.iter().map(|&k| !k).collect(),
532                            removed_genes,
533                        )
534                    };
535                let only_mask: Vec<bool> = (0..removed.len())
536                    .map(|i| removed[i] && pass_others[i])
537                    .collect();
538                RuleCount {
539                    removes: fails.iter().filter(|&&f| f).count(),
540                    only: only_mask.iter().filter(|&&o| o).count(),
541                    pass_others,
542                    fails,
543                    only_mask,
544                }
545            })
546            .collect();
547        // Bands over the cells above the near-empty floor, as qc_lib fits them.
548        let consider: Vec<bool> = self
549            .metrics
550            .n_genes
551            .iter()
552            .map(|&g| (g as usize) >= self.cfg.min_cell_nnz)
553            .collect();
554        self.bands = self
555            .rules
556            .iter()
557            .map(|&r| self.fit_band(r, &consider))
558            .collect();
559        self.edges = (0..self.rules.len())
560            .map(|i| self.fit_edge(self.rules[i], self.bands[i]))
561            .collect();
562        self.groups = self.group();
563    }
564}
565
566fn is_on(c: &QcConfig, rule: Rule) -> bool {
567    match rule {
568        Rule::Genes => c.mad_on_n_genes || c.min_cell_nnz > 0 || c.auto_cell_cutoff,
569        Rule::Counts => c.mad_on_counts || c.min_counts_per_cell > 0.0,
570        Rule::Mito => c.mad_on_mito || c.mito_max_frac.is_some(),
571        Rule::Ribo => c.ribo_max_frac.is_some(),
572        Rule::GeneCells => c.feature_min_cells > 0,
573    }
574}
575
576fn turn_off(c: &mut QcConfig, rule: Rule) {
577    match rule {
578        Rule::Genes => {
579            c.mad_on_n_genes = false;
580            c.min_cell_nnz = 0;
581            c.auto_cell_cutoff = false;
582        }
583        Rule::Counts => {
584            c.mad_on_counts = false;
585            c.min_counts_per_cell = 0.0;
586        }
587        Rule::Mito => {
588            c.mad_on_mito = false;
589            c.mito_max_frac = None;
590        }
591        Rule::Ribo => c.ribo_max_frac = None,
592        Rule::GeneCells => c.feature_min_cells = 0,
593    }
594}
595
596fn restore(c: &mut QcConfig, b: &QcConfig, rule: Rule) {
597    match rule {
598        Rule::Genes => {
599            c.mad_on_n_genes = b.mad_on_n_genes;
600            c.min_cell_nnz = b.min_cell_nnz;
601            c.auto_cell_cutoff = b.auto_cell_cutoff;
602        }
603        Rule::Counts => {
604            c.mad_on_counts = b.mad_on_counts;
605            c.min_counts_per_cell = b.min_counts_per_cell;
606        }
607        Rule::Mito => {
608            c.mad_on_mito = b.mad_on_mito;
609            c.mito_max_frac = b.mito_max_frac;
610        }
611        Rule::Ribo => c.ribo_max_frac = b.ribo_max_frac,
612        Rule::GeneCells => c.feature_min_cells = b.feature_min_cells,
613    }
614}
615
616/// A flag's value: as short as reads back to the same `f32`.
617fn fmt_value(v: f32) -> String {
618    format!("{v}")
619}
620
621#[cfg(test)]
622#[path = "qc_rules/tests.rs"]
623mod tests;