seqcolyte / qc /core /src /seqops.rs
seqmachines's picture
Deploy Seqcolyte Studio
ff34739 verified
Raw
History Blame Contribute Delete
2.95 kB
//! Byte-level sequence predicates — verbatim ports of `qc/model.py`.
/// `qc.model.startswith_fuzzy(seq, pattern, max_mismatch)`.
///
/// True if `seq` begins with `pattern` allowing up to `max_mismatch` substitutions.
/// Shorter-than-pattern `seq` is never a match (mirrors the Python length guard).
pub fn startswith_fuzzy(seq: &[u8], pattern: &[u8], max_mismatch: u32) -> bool {
if seq.len() < pattern.len() {
return false;
}
let mut mm = 0u32;
for i in 0..pattern.len() {
if seq[i] != pattern[i] {
mm += 1;
if mm > max_mismatch {
return false;
}
}
}
true
}
/// `qc.model.has_homopolymer_tail(seq, base, window=20, min_run=15, tol=3)`.
///
/// True if the 3' end of `seq` is a near-pure run of `base`. `seq[-window:]` becomes the
/// whole read when it is shorter than `window`; reads shorter than `min_run` are excluded.
pub fn has_homopolymer_tail(seq: &[u8], base: u8) -> bool {
const WINDOW: usize = 20;
const MIN_RUN: usize = 15;
const TOL: usize = 3;
if seq.len() < MIN_RUN {
return false;
}
let start = seq.len().saturating_sub(WINDOW);
let tail = &seq[start..];
let count = tail.iter().filter(|&&b| b == base).count();
count >= tail.len() - TOL
}
#[cfg(test)]
mod tests {
use super::*;
const TSO: &[u8] = b"AAGCAGTGGTATCAACGCAGAGTACATGGG";
#[test]
fn startswith_fuzzy_edges() {
// exact
let mut s = TSO.to_vec();
s.extend_from_slice(b"AAAA");
assert!(startswith_fuzzy(&s, TSO, 0));
// 2 substitutions ok
let mut s2 = b"XX".to_vec();
s2.extend_from_slice(&TSO[2..]);
s2.extend_from_slice(b"AAAA");
assert!(startswith_fuzzy(&s2, TSO, 2));
// 3 substitutions too many
let mut s3 = b"XXX".to_vec();
s3.extend_from_slice(&TSO[3..]);
s3.extend_from_slice(b"AAAA");
assert!(!startswith_fuzzy(&s3, TSO, 2));
// shorter than pattern
assert!(!startswith_fuzzy(b"AAG", TSO, 2));
assert!(!startswith_fuzzy(b"", b"A", 0));
}
#[test]
fn homopolymer_tail_edges() {
// 21 G tail within a 91bp read
let mut a = vec![b'A'; 70];
a.extend(std::iter::repeat(b'G').take(21));
assert!(has_homopolymer_tail(&a, b'G'));
// no G tail
assert!(!has_homopolymer_tail(&vec![b'A'; 91], b'G'));
// too short
assert!(!has_homopolymer_tail(&vec![b'G'; 14], b'G'));
// exactly min_run of pure base
assert!(has_homopolymer_tail(&vec![b'G'; 15], b'G'));
// tail with tol=3 mismatches allowed (last 20: 17 G + 3 A)
let mut b = vec![b'A'; 71];
b.extend(std::iter::repeat(b'G').take(17));
b.extend(std::iter::repeat(b'A').take(3));
// last 20 = 17 G + 3 A -> count 17 >= 20-3 -> true
assert!(has_homopolymer_tail(&b, b'G'));
}
}