use crate::metrics::{TemplateFilterCounts, TemplateFilterReason};
use crate::sam::SamTag;
use crate::template::Template;
use crate::umi::{UmiValidation, validate_umi};
use fgumi_raw_bam;
use fgumi_raw_bam::RawRecord;
#[derive(Debug, Clone)]
pub struct TemplateFilterConfig {
pub umi_tag: [u8; 2],
pub min_mapq: u8,
pub include_non_pf: bool,
pub min_umi_length: Option<usize>,
pub no_umi: bool,
pub allow_unmapped: bool,
}
#[must_use]
pub fn template_has_malformed_record(template: &Template) -> bool {
template.records().iter().any(|r| r.len() < fgumi_raw_bam::MIN_BAM_RECORD_LEN)
}
#[must_use]
pub fn template_is_fully_unmapped(template: &Template) -> bool {
let (raw_r1, raw_r2) = (template.r1(), template.r2());
if raw_r1.is_none() && raw_r2.is_none() {
return false;
}
raw_r1.is_none_or(RawRecord::is_unmapped) && raw_r2.is_none_or(RawRecord::is_unmapped)
}
impl Default for TemplateFilterConfig {
fn default() -> Self {
Self {
umi_tag: *SamTag::RX,
min_mapq: 0,
include_non_pf: false,
min_umi_length: None,
no_umi: false,
allow_unmapped: false,
}
}
}
pub fn filter_template(
template: &Template,
config: &TemplateFilterConfig,
counts: &mut TemplateFilterCounts,
) -> bool {
let primary_reads = u64::from(template.r1().is_some()) + u64::from(template.r2().is_some());
if template_has_malformed_record(template) {
counts.record_rejected(TemplateFilterReason::MalformedRecord, primary_reads);
return false;
}
let raw_r1 = template.r1();
let raw_r2 = template.r2();
if raw_r1.is_none() && raw_r2.is_none() {
counts.record_rejected(TemplateFilterReason::NoPrimaryReads, primary_reads);
return false;
}
if template_is_fully_unmapped(template) && !config.allow_unmapped {
counts.record_rejected(TemplateFilterReason::Unmapped, primary_reads);
return false;
}
for raw in [raw_r1, raw_r2].into_iter().flatten() {
if !config.include_non_pf && raw.is_qc_fail() {
counts.record_rejected(TemplateFilterReason::NotPassingFilter, primary_reads);
return false;
}
if !raw.is_unmapped() {
let mapq = fgumi_raw_bam::mapq(raw);
if mapq < config.min_mapq {
counts.record_rejected(TemplateFilterReason::LowMappingQuality, primary_reads);
return false;
}
}
}
for raw in [raw_r1, raw_r2].into_iter().flatten() {
let aux = fgumi_raw_bam::aux_data_slice(raw);
let check_mq = !raw.is_mate_unmapped();
let check_umi = !config.no_umi;
let (found_mq, found_umi) =
scan_aux_for_mq_and_umi(aux, config.umi_tag, check_mq, check_umi);
if let Some(mq) = found_mq
&& mq < i64::from(config.min_mapq)
{
counts.record_rejected(TemplateFilterReason::LowMateMappingQuality, primary_reads);
return false;
}
if config.no_umi {
continue;
}
if let Some(umi_bytes) = found_umi {
match validate_umi(umi_bytes) {
UmiValidation::ContainsN => {
counts.record_rejected(TemplateFilterReason::NsInUmi, primary_reads);
return false;
}
UmiValidation::Valid(base_count) => {
if let Some(min_len) = config.min_umi_length
&& base_count < min_len
{
counts.record_rejected(TemplateFilterReason::UmiTooShort, primary_reads);
return false;
}
}
}
} else {
counts.record_rejected(TemplateFilterReason::MissingUmi, primary_reads);
return false;
}
}
counts.record_accepted(primary_reads);
true
}
fn scan_aux_for_mq_and_umi(
aux: &[u8],
umi_tag: [u8; 2],
check_mq: bool,
check_umi: bool,
) -> (Option<i64>, Option<&[u8]>) {
if !check_mq && !check_umi {
return (None, None);
}
let mut found_mq: Option<i64> = None;
let mut found_umi: Option<&[u8]> = None;
let mut p = 0;
while p + 3 <= aux.len() {
let t = [aux[p], aux[p + 1]];
let val_type = aux[p + 2];
if check_umi && t == umi_tag && val_type == b'Z' {
let start = p + 3;
if let Some(end) = aux[start..].iter().position(|&b| b == 0) {
found_umi = Some(&aux[start..start + end]);
p = start + end + 1;
} else {
break;
}
if !check_mq || found_mq.is_some() {
break;
}
continue;
}
if check_mq && t == *SamTag::MQ {
found_mq = fgumi_raw_bam::extract_int_value(aux, p, val_type);
}
if let Some(size) = fgumi_raw_bam::tag_value_size(val_type, &aux[p + 3..]) {
p += 3 + size;
} else {
break;
}
if (!check_umi || found_umi.is_some()) && (!check_mq || found_mq.is_some()) {
break;
}
}
(found_mq, found_umi)
}
#[cfg(test)]
mod tests {
use super::*;
use fgumi_raw_bam::SamBuilder;
fn unmapped_template() -> Template {
let mut builder = SamBuilder::new();
builder
.read_name(b"unmapped")
.sequence(b"ACGT")
.qualities(&[30; 4])
.flags(fgumi_raw_bam::flags::UNMAPPED | fgumi_raw_bam::flags::MATE_UNMAPPED)
.add_string_tag(SamTag::RX, b"ACGT");
Template::from_records(vec![builder.build()]).expect("template construction")
}
fn config(allow_unmapped: bool) -> TemplateFilterConfig {
TemplateFilterConfig {
umi_tag: *SamTag::RX,
min_mapq: 0,
include_non_pf: false,
min_umi_length: None,
no_umi: false,
allow_unmapped,
}
}
#[rstest::rstest]
#[case::rejected_when_disallowed(false, false)]
#[case::accepted_when_allowed(true, true)]
fn unmapped_templates_respect_allow_unmapped(
#[case] allow_unmapped: bool,
#[case] expect_accept: bool,
) {
let mut counts = TemplateFilterCounts::new();
let accepted = filter_template(&unmapped_template(), &config(allow_unmapped), &mut counts);
assert_eq!(accepted, expect_accept);
assert_eq!(
counts.rejected_templates(TemplateFilterReason::Unmapped),
u64::from(!expect_accept)
);
assert_eq!(counts.accepted_templates(), u64::from(expect_accept));
}
}