use crate::index::Index;
use minibwa_sys as sys;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Strand {
Forward,
Reverse,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CigarKind {
Match,
Ins,
Del,
RefSkip,
SoftClip,
HardClip,
Pad,
Eq,
Diff,
}
impl CigarKind {
pub(crate) fn from_op(op: u32) -> CigarKind {
match op {
0 => CigarKind::Match,
1 => CigarKind::Ins,
2 => CigarKind::Del,
3 => CigarKind::RefSkip,
4 => CigarKind::SoftClip,
5 => CigarKind::HardClip,
6 => CigarKind::Pad,
7 => CigarKind::Eq,
8 => CigarKind::Diff,
other => panic!("unknown minibwa cigar op {other}"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CigarOp {
pub kind: CigarKind,
pub len: u32,
}
impl std::fmt::Display for CigarOp {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let ch = match self.kind {
CigarKind::Match => 'M',
CigarKind::Ins => 'I',
CigarKind::Del => 'D',
CigarKind::RefSkip => 'N',
CigarKind::SoftClip => 'S',
CigarKind::HardClip => 'H',
CigarKind::Pad => 'P',
CigarKind::Eq => '=',
CigarKind::Diff => 'X',
};
write!(f, "{}{}", self.len, ch)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Hit {
pub tid: i64,
pub contig: Option<String>,
pub ref_start: i64,
pub ref_end: i64,
pub query_start: i32,
pub query_end: i32,
pub strand: Strand,
pub mapq: u8,
pub score: i32,
pub n_sub: i32,
pub blen: i32,
pub mlen: i32,
pub proper_pair: bool,
pub is_primary: bool,
pub is_secondary: bool,
pub is_supplementary: bool,
pub cigar: Vec<CigarOp>,
}
impl Hit {
pub fn cigar_string(&self) -> String {
if self.cigar.is_empty() {
return "*".to_owned();
}
self.cigar.iter().map(|op| op.to_string()).collect()
}
}
pub(crate) fn classify(parent: i32, id: i32, sam_pri: bool) -> (bool, bool, bool) {
if parent != id {
(false, true, false) } else if !sam_pri {
(false, false, true) } else {
(true, false, false) }
}
pub(crate) unsafe fn hit_from_raw(h: &sys::mb_hit_t, idx: &Index) -> Hit {
let mut cigar = Vec::new();
if !h.p.is_null() {
let extra = unsafe { &*h.p };
let n = extra.n_cigar.max(0) as usize;
let words = unsafe { extra.cigar.as_slice(n) };
cigar.reserve(n);
for &w in words {
cigar.push(CigarOp {
kind: CigarKind::from_op(w & 0xf),
len: w >> 4,
});
}
}
let (is_primary, is_secondary, is_supplementary) = classify(h.parent, h.id, h.sam_pri() != 0);
Hit {
tid: h.tid,
contig: idx.contig_name(h.tid as usize).map(str::to_owned),
ref_start: h.ts,
ref_end: h.te,
query_start: h.qs,
query_end: h.qe,
strand: if h.rev() != 0 {
Strand::Reverse
} else {
Strand::Forward
},
mapq: h.mapq.clamp(0, 255) as u8,
score: h.score,
n_sub: h.n_sub,
blen: h.blen,
mlen: h.mlen,
proper_pair: h.proper_pair() != 0,
is_primary,
is_secondary,
is_supplementary,
cigar,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::meth::Meth;
#[test]
fn cigar_kind_from_op() {
assert_eq!(CigarKind::from_op(0), CigarKind::Match);
assert_eq!(CigarKind::from_op(4), CigarKind::SoftClip);
assert_eq!(CigarKind::from_op(8), CigarKind::Diff);
}
#[test]
fn meth_codes() {
assert_eq!(Meth::None.as_mt(), 0);
assert_eq!(Meth::C2T.as_mt(), 1);
assert_eq!(Meth::G2A.as_mt(), 2);
}
#[test]
fn classify_flags() {
assert_eq!(classify(5, 5, true), (true, false, false)); assert_eq!(classify(5, 5, false), (false, false, true)); assert_eq!(classify(2, 5, true), (false, true, false)); }
#[test]
fn cigar_op_display() {
assert_eq!(
CigarOp {
kind: CigarKind::Match,
len: 100
}
.to_string(),
"100M"
);
assert_eq!(
CigarOp {
kind: CigarKind::Ins,
len: 5
}
.to_string(),
"5I"
);
assert_eq!(
CigarOp {
kind: CigarKind::Del,
len: 3
}
.to_string(),
"3D"
);
assert_eq!(
CigarOp {
kind: CigarKind::Eq,
len: 10
}
.to_string(),
"10="
);
assert_eq!(
CigarOp {
kind: CigarKind::Diff,
len: 2
}
.to_string(),
"2X"
);
assert_eq!(
CigarOp {
kind: CigarKind::SoftClip,
len: 7
}
.to_string(),
"7S"
);
}
#[test]
fn cigar_string_empty_is_star() {
let hit = Hit {
tid: -1,
contig: None,
ref_start: 0,
ref_end: 0,
query_start: 0,
query_end: 0,
strand: Strand::Forward,
mapq: 0,
score: 0,
n_sub: 0,
blen: 0,
mlen: 0,
proper_pair: false,
is_primary: false,
is_secondary: false,
is_supplementary: false,
cigar: vec![],
};
assert_eq!(hit.cigar_string(), "*");
}
#[test]
fn cigar_string_nonempty() {
let hit = Hit {
tid: 0,
contig: Some("chr1".to_owned()),
ref_start: 100,
ref_end: 250,
query_start: 0,
query_end: 150,
strand: Strand::Forward,
mapq: 60,
score: 100,
n_sub: 0,
blen: 0,
mlen: 0,
proper_pair: false,
is_primary: true,
is_secondary: false,
is_supplementary: false,
cigar: vec![
CigarOp {
kind: CigarKind::Match,
len: 100,
},
CigarOp {
kind: CigarKind::Ins,
len: 5,
},
CigarOp {
kind: CigarKind::Match,
len: 45,
},
],
};
assert_eq!(hit.cigar_string(), "100M5I45M");
}
}