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) {
343 let c = &mut self.cfg;
344 let value = value.max(0.0);
345 match rule {
346 Rule::Genes => {
347 c.mad_on_n_genes = false;
348 c.auto_cell_cutoff = false;
349 c.min_cell_nnz = value.ceil() as usize;
350 }
351 Rule::Counts => {
352 c.mad_on_counts = false;
353 c.min_counts_per_cell = value;
354 }
355 Rule::Mito => {
356 c.mad_on_mito = false;
357 c.mito_max_frac = Some(value.min(1.0));
358 }
359 Rule::Ribo => c.ribo_max_frac = Some(value.min(1.0)),
360 Rule::GeneCells => c.feature_min_cells = value.ceil() as usize,
361 }
362 self.recount();
363 }
364
365 pub fn set_mads(&mut self, n_mads: f32) {
367 self.cfg.n_mads = n_mads.clamp(0.5, 50.0);
368 self.recount();
369 }
370
371 pub fn toggle_mode(&mut self, rule: Rule) {
375 if !Self::banded(rule) {
376 return;
377 }
378 match self.mode(rule) {
379 Mode::Mad => {
380 let edge = self.edge(rule).unwrap_or_else(|| self.keeps_all(rule));
383 self.set_value(rule, edge);
384 }
385 Mode::Value => {
386 let (c, b) = (&mut self.cfg, &self.base);
387 match rule {
388 Rule::Genes => {
389 c.mad_on_n_genes = true;
390 c.min_cell_nnz = b.min_cell_nnz;
391 c.auto_cell_cutoff = b.auto_cell_cutoff;
392 }
393 Rule::Counts => {
394 c.mad_on_counts = true;
395 c.min_counts_per_cell = b.min_counts_per_cell;
396 }
397 Rule::Mito => {
398 c.mad_on_mito = true;
399 c.mito_max_frac = b.mito_max_frac;
400 }
401 Rule::Ribo | Rule::GeneCells => {}
402 }
403 self.recount();
404 }
405 }
406 }
407
408 fn keeps_all(&self, rule: Rule) -> f32 {
411 match rule.keeps_above() {
412 true => 0.0,
413 false => self.values(rule).iter().copied().fold(0.0f32, f32::max),
414 }
415 }
416
417 pub fn reset(&mut self) {
419 self.cfg = self.base.clone();
420 self.recount();
421 }
422
423 pub fn overlap(&self) -> &[(Vec<Rule>, usize)] {
427 &self.groups
428 }
429
430 fn group(&self) -> Vec<(Vec<Rule>, usize)> {
431 let cell_rules: Vec<(usize, Rule)> = self
432 .rules
433 .iter()
434 .copied()
435 .enumerate()
436 .filter(|(_, r)| r.on_cells())
437 .collect();
438 let mut groups: Vec<(Vec<Rule>, usize)> = vec![];
439 for c in (0..self.removed_cells.len()).filter(|&c| self.removed_cells[c]) {
440 let set: Vec<Rule> = cell_rules
441 .iter()
442 .filter(|(i, _)| self.counts[*i].fails[c])
443 .map(|&(_, r)| r)
444 .collect();
445 match groups.iter_mut().find(|(s, _)| *s == set) {
446 Some(g) => g.1 += 1,
447 None => groups.push((set, 1)),
448 }
449 }
450 groups.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.len().cmp(&b.0.len())));
451 groups
452 }
453
454 pub fn flags(&self) -> Vec<String> {
460 let (c, d) = (&self.cfg, &QcConfig::default());
461 let mut f = vec![];
462 let mut pair = |k: &str, v: String| {
463 f.push(k.to_string());
464 f.push(v);
465 };
466 if (c.mad_on_n_genes || c.mad_on_counts || c.mad_on_mito) && c.n_mads != d.n_mads {
467 pair("--qc-mads", fmt_value(c.n_mads));
468 }
469 if c.min_cell_nnz != d.min_cell_nnz {
470 pair("--qc-min-cell-nnz", c.min_cell_nnz.to_string());
471 }
472 if c.min_counts_per_cell != d.min_counts_per_cell {
473 pair("--qc-min-counts", fmt_value(c.min_counts_per_cell));
474 }
475 match &c.mito_pattern {
476 Some(p) => pair("--qc-mito-pattern", p.clone()),
477 None => pair("--qc-mito-pattern", String::new()),
478 }
479 if let Some(v) = c.mito_max_frac {
480 pair("--qc-mito-max-frac", fmt_value(v));
481 }
482 if let Some(p) = &c.ribo_pattern {
483 pair("--qc-ribo-pattern", p.clone());
484 }
485 if let Some(v) = c.ribo_max_frac {
486 pair("--qc-ribo-max-frac", fmt_value(v));
487 }
488 if c.feature_min_cells != d.feature_min_cells || c.auto_cell_cutoff {
489 pair("--qc-feature-min-cells", c.feature_min_cells.to_string());
490 }
491 if !c.mad_on_n_genes {
492 f.push("--qc-mad-on-genes=false".into());
493 }
494 if !c.mad_on_counts {
495 f.push("--qc-mad-on-counts=false".into());
496 }
497 if c.mito_pattern.is_some() && !c.mad_on_mito {
498 f.push("--qc-mad-on-mito=false".into());
499 }
500 if c.auto_cell_cutoff {
501 f.push("--qc-auto-cutoff".into());
502 }
503 f
504 }
505
506 fn recount(&mut self) {
510 self.verdicts = cell_verdicts_quiet(&self.metrics, &self.cfg);
511 let all = &self.verdicts;
512 let n = all.train_keep.len();
513 self.removed_cells = (0..n).map(|c| !all.emitted(c)).collect();
514 self.removed_genes = all.feature_keep.iter().map(|&k| !k).collect();
515 let (metrics, cfg, rules) = (&self.metrics, &self.cfg, &self.rules);
516 let (removed_cells, removed_genes) = (&self.removed_cells, &self.removed_genes);
517 self.counts = rules
518 .par_iter()
519 .map(|&rule| {
520 let mut without = cfg.clone();
521 turn_off(&mut without, rule);
522 let mut alone = cfg.clone();
523 for &other in rules.iter().filter(|&&o| o != rule) {
524 turn_off(&mut alone, other);
525 }
526 let (w, a) = rayon::join(
527 || cell_verdicts_quiet(metrics, &without),
528 || cell_verdicts_quiet(metrics, &alone),
529 );
530 let (pass_others, fails, removed): (Vec<bool>, Vec<bool>, &[bool]) =
531 if rule.on_cells() {
532 (
533 (0..n).map(|c| w.emitted(c)).collect(),
534 (0..n).map(|c| is_on(cfg, rule) && !a.emitted(c)).collect(),
535 removed_cells,
536 )
537 } else {
538 (
539 w.feature_keep.clone(),
540 a.feature_keep.iter().map(|&k| !k).collect(),
541 removed_genes,
542 )
543 };
544 let only_mask: Vec<bool> = (0..removed.len())
545 .map(|i| removed[i] && pass_others[i])
546 .collect();
547 RuleCount {
548 removes: fails.iter().filter(|&&f| f).count(),
549 only: only_mask.iter().filter(|&&o| o).count(),
550 pass_others,
551 fails,
552 only_mask,
553 }
554 })
555 .collect();
556 let consider: Vec<bool> = self
558 .metrics
559 .n_genes
560 .iter()
561 .map(|&g| (g as usize) >= self.cfg.min_cell_nnz)
562 .collect();
563 self.bands = self
564 .rules
565 .iter()
566 .map(|&r| self.fit_band(r, &consider))
567 .collect();
568 self.edges = (0..self.rules.len())
569 .map(|i| self.fit_edge(self.rules[i], self.bands[i]))
570 .collect();
571 self.groups = self.group();
572 }
573}
574
575fn is_on(c: &QcConfig, rule: Rule) -> bool {
576 match rule {
577 Rule::Genes => c.mad_on_n_genes || c.min_cell_nnz > 0 || c.auto_cell_cutoff,
578 Rule::Counts => c.mad_on_counts || c.min_counts_per_cell > 0.0,
579 Rule::Mito => c.mad_on_mito || c.mito_max_frac.is_some(),
580 Rule::Ribo => c.ribo_max_frac.is_some(),
581 Rule::GeneCells => c.feature_min_cells > 0,
582 }
583}
584
585fn turn_off(c: &mut QcConfig, rule: Rule) {
586 match rule {
587 Rule::Genes => {
588 c.mad_on_n_genes = false;
589 c.min_cell_nnz = 0;
590 c.auto_cell_cutoff = false;
591 }
592 Rule::Counts => {
593 c.mad_on_counts = false;
594 c.min_counts_per_cell = 0.0;
595 }
596 Rule::Mito => {
597 c.mad_on_mito = false;
598 c.mito_max_frac = None;
599 }
600 Rule::Ribo => c.ribo_max_frac = None,
601 Rule::GeneCells => c.feature_min_cells = 0,
602 }
603}
604
605fn restore(c: &mut QcConfig, b: &QcConfig, rule: Rule) {
606 match rule {
607 Rule::Genes => {
608 c.mad_on_n_genes = b.mad_on_n_genes;
609 c.min_cell_nnz = b.min_cell_nnz;
610 c.auto_cell_cutoff = b.auto_cell_cutoff;
611 }
612 Rule::Counts => {
613 c.mad_on_counts = b.mad_on_counts;
614 c.min_counts_per_cell = b.min_counts_per_cell;
615 }
616 Rule::Mito => {
617 c.mad_on_mito = b.mad_on_mito;
618 c.mito_max_frac = b.mito_max_frac;
619 }
620 Rule::Ribo => c.ribo_max_frac = b.ribo_max_frac,
621 Rule::GeneCells => c.feature_min_cells = b.feature_min_cells,
622 }
623}
624
625fn fmt_value(v: f32) -> String {
627 format!("{v}")
628}
629
630#[cfg(test)]
631#[path = "qc_rules/tests.rs"]
632mod tests;