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.position(rule).expect("a rule of the data")
168    }
169
170    /// Where `rule` is in table order; `None` for a rule the data lacks
171    /// (no mito metric when the pattern matched no feature).
172    fn position(&self, rule: Rule) -> Option<usize> {
173        self.rules.iter().position(|&r| r == rule)
174    }
175
176    /// What `rule`, one of [`CellRules::rules`], does now.
177    pub fn count(&self, rule: Rule) -> &RuleCount {
178        &self.counts[self.index(rule)]
179    }
180
181    /// Whether each cell (each gene, for [`Rule::GeneCells`]) is removed now.
182    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    /// The robust band of `rule`'s metric on its own scale, as qc_lib fits
190    /// it (over the cells above the near-empty floor), whether or not the
191    /// rule holds to it; `None` for a rule with no band, or no spread.
192    pub fn band(&self, rule: Rule) -> Option<RobustBand> {
193        self.bands[self.position(rule)?]
194    }
195
196    /// The cells QC keeps (in the output), and how many there are.
197    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    /// The genes QC keeps.
204    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    /// The values `rule` is held to: one per cell, or per gene.
211    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    /// Whether `rule` decides anything now.
223    /// A rule the data lacks (no mito metric when the pattern matched no
224    /// feature) decides nothing, whatever its settings say; they are kept,
225    /// so the flags carry them to data that has it.
226    pub fn is_on(&self, rule: Rule) -> bool {
227        self.position(rule).is_some() && is_on(&self.cfg, rule)
228    }
229
230    /// How `rule`'s edge is set now.
231    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    /// Whether `rule` can be set by the MAD band.
247    pub fn banded(rule: Rule) -> bool {
248        matches!(rule, Rule::Genes | Rule::Counts | Rule::Mito)
249    }
250
251    /// The edge `rule` holds its metric to now, on the metric's own scale:
252    /// the tightest of its band edge and its own value; `None` when it is
253    /// off, its band has no spread, or the data lacks it.
254    pub fn edge(&self, rule: Rule) -> Option<f32> {
255        self.edges[self.position(rule)?]
256    }
257
258    /// The band of `rule`'s metric fitted now, for [`CellRules::recount`].
259    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    /// The edge of `rule` under the current settings, from its fitted band.
271    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    /// Turn `rule` off, or back on as the starting settings had it (by the
314    /// MAD band where it has one; a cap where it has none, at the largest
315    /// value, keeping everything until it is moved).
316    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    /// Hold `rule` to `value` of its own, off the MAD band.
339    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    /// Set the MAD band's width, which every banded rule shares.
363    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    /// Switch `rule` between the MAD band and a value of its own, the value
369    /// starting where the edge is now. Back on the band, its own value
370    /// returns to the starting one.
371    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                // From where the edge is; with none (a band with no spread),
378                // from a value that keeps everything.
379                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    /// A value of `rule`'s own that removes nothing: 0 for a floor, the
406    /// largest value for a cap.
407    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    /// Back to the starting settings.
415    pub fn reset(&mut self) {
416        self.cfg = self.base.clone();
417        self.recount();
418    }
419
420    /// The removed cells grouped by the cell rules each fails on its own,
421    /// largest group first. A cell removed only as the rules act together
422    /// (a band refitted without a floor, say) has an empty set.
423    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    /// `qc` flags that decide what these settings do, past `qc`'s defaults.
452    /// The mito pattern is always given, an empty one turning the metric off
453    /// (`qc` starts from a pattern of its own); under `--qc-auto-cutoff` the
454    /// gene rule is always given too, 0 for off, as `qc` puts it at the
455    /// trough only when none is given.
456    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    /// Decide again under the current settings, what each rule does (the
504    /// config with it off and with it alone, decided in parallel), and what
505    /// a frame draws from them.
506    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        // Bands over the cells above the near-empty floor, as qc_lib fits them.
554        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
622/// A flag's value: as short as reads back to the same `f32`.
623fn fmt_value(v: f32) -> String {
624    format!("{v}")
625}
626
627#[cfg(test)]
628#[path = "qc_rules/tests.rs"]
629mod tests;