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.rules
168 .iter()
169 .position(|&r| r == rule)
170 .expect("a rule of the data")
171 }
172
173 pub fn count(&self, rule: Rule) -> &RuleCount {
175 &self.counts[self.index(rule)]
176 }
177
178 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 pub fn band(&self, rule: Rule) -> Option<RobustBand> {
190 self.bands[self.index(rule)]
191 }
192
193 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 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 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 pub fn is_on(&self, rule: Rule) -> bool {
221 is_on(&self.cfg, rule)
222 }
223
224 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 pub fn banded(rule: Rule) -> bool {
242 matches!(rule, Rule::Genes | Rule::Counts | Rule::Mito)
243 }
244
245 pub fn edge(&self, rule: Rule) -> Option<f32> {
249 self.edges[self.index(rule)]
250 }
251
252 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 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 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 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 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 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 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 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 pub fn reset(&mut self) {
410 self.cfg = self.base.clone();
411 self.recount();
412 }
413
414 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 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 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 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
616fn fmt_value(v: f32) -> String {
618 format!("{v}")
619}
620
621#[cfg(test)]
622#[path = "qc_rules/tests.rs"]
623mod tests;