use std::ops::Range;
pub const PHRED33: u8 = 33;
pub const PHRED64: u8 = 64;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum QualityEncoding {
#[default]
Phred33,
Phred64,
}
impl QualityEncoding {
pub const fn offset(self) -> u8 {
match self {
QualityEncoding::Phred33 => PHRED33,
QualityEncoding::Phred64 => PHRED64,
}
}
pub fn detect(quality: &[u8]) -> Option<QualityEncoding> {
let mut min = u8::MAX;
let mut max = 0u8;
for &q in quality {
min = min.min(q);
max = max.max(q);
}
if quality.is_empty() {
return None;
}
if min < PHRED64 {
Some(QualityEncoding::Phred33)
} else if max > b'J' {
Some(QualityEncoding::Phred64)
} else {
None
}
}
}
#[inline]
pub fn score(ch: u8, offset: u8) -> u8 {
ch.saturating_sub(offset)
}
#[inline]
pub fn encode(score: u8, offset: u8) -> u8 {
offset.saturating_add(score.min(126 - offset))
}
pub fn scores(quality: &[u8], offset: u8) -> Vec<u8> {
quality.iter().map(|&q| score(q, offset)).collect()
}
#[inline]
pub fn error_probability(score: u8) -> f64 {
10f64.powf(-(score as f64) / 10.0)
}
pub fn probability_to_score(p: f64) -> u8 {
if p <= 0.0 {
return 93;
}
let q = -10.0 * p.log10();
q.round().clamp(0.0, 93.0) as u8
}
pub fn mean_score(quality: &[u8], offset: u8) -> Option<f64> {
if quality.is_empty() {
return None;
}
let sum: u64 = quality.iter().map(|&q| score(q, offset) as u64).sum();
Some(sum as f64 / quality.len() as f64)
}
pub fn expected_errors(quality: &[u8], offset: u8) -> f64 {
quality
.iter()
.map(|&q| error_probability(score(q, offset)))
.sum()
}
pub fn mean_quality(quality: &[u8], offset: u8) -> Option<f64> {
if quality.is_empty() {
return None;
}
let mean_p = expected_errors(quality, offset) / quality.len() as f64;
Some(-10.0 * mean_p.log10())
}
pub fn fraction_at_least(quality: &[u8], offset: u8, threshold: u8) -> f64 {
if quality.is_empty() {
return 0.0;
}
let n = quality
.iter()
.filter(|&&q| score(q, offset) >= threshold)
.count();
n as f64 / quality.len() as f64
}
pub fn trim_ends(quality: &[u8], offset: u8, min_score: u8) -> Range<usize> {
let start = quality.iter().position(|&q| score(q, offset) >= min_score);
match start {
None => 0..0,
Some(start) => {
let end = quality
.iter()
.rposition(|&q| score(q, offset) >= min_score)
.unwrap()
+ 1;
start..end
}
}
}
pub fn trim_mott(quality: &[u8], offset: u8, threshold: u8) -> Range<usize> {
let mut best = 0..0;
let mut best_score = 0i64;
let mut running = 0i64;
let mut start = 0usize;
for (i, &q) in quality.iter().enumerate() {
running += score(q, offset) as i64 - threshold as i64;
if running < 0 {
running = 0;
start = i + 1;
} else if running > best_score {
best_score = running;
best = start..i + 1;
}
}
best
}
pub fn trim_sliding_window(
quality: &[u8],
offset: u8,
window: usize,
min_mean: f64,
) -> Range<usize> {
if window == 0 || quality.len() < window {
return 0..quality.len();
}
let threshold = min_mean * window as f64;
let mut sum: f64 = quality[..window]
.iter()
.map(|&q| score(q, offset) as f64)
.sum();
if sum < threshold {
return 0..0;
}
for i in window..quality.len() {
sum += score(quality[i], offset) as f64;
sum -= score(quality[i - window], offset) as f64;
if sum < threshold {
return 0..i + 1 - window;
}
}
0..quality.len()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scores_round_trip() {
for s in 0u8..=60 {
assert_eq!(score(encode(s, PHRED33), PHRED33), s);
}
}
#[test]
fn error_probabilities() {
assert_eq!(probability_to_score(0.001), 30);
assert_eq!(probability_to_score(1.0), 0);
assert!((expected_errors(b"!!!!", PHRED33) - 4.0).abs() < 1e-9);
}
#[test]
fn mean_quality_is_error_weighted() {
let q = b"IIIII!"; let arithmetic = mean_score(q, PHRED33).unwrap();
let weighted = mean_quality(q, PHRED33).unwrap();
assert!(arithmetic > 33.0, "{arithmetic}");
assert!(weighted < 8.0, "{weighted}");
}
#[test]
fn trims_ends() {
assert_eq!(trim_ends(b"!!III!!", PHRED33, 30), 2..5);
assert_eq!(trim_ends(b"!!!!", PHRED33, 30), 0..0);
assert_eq!(trim_ends(b"IIII", PHRED33, 30), 0..4);
}
#[test]
fn mott_keeps_best_window() {
let q = b"###IIIIIIII###";
assert_eq!(trim_mott(q, PHRED33, 20), 3..11);
assert_eq!(trim_mott(b"####", PHRED33, 20), 0..0);
}
#[test]
fn sliding_window_cuts_at_drop() {
let q = b"IIIIIIII####";
assert_eq!(trim_sliding_window(q, PHRED33, 4, 20.0), 0..7);
assert_eq!(trim_sliding_window(b"IIIIIIII", PHRED33, 4, 20.0), 0..8);
assert_eq!(trim_sliding_window(b"II", PHRED33, 4, 20.0), 0..2);
assert_eq!(trim_sliding_window(b"####IIII", PHRED33, 4, 20.0), 0..0);
}
#[test]
fn q30_fraction() {
assert!((fraction_at_least(b"IIII!!!!", PHRED33, 30) - 0.5).abs() < 1e-9);
assert_eq!(fraction_at_least(b"", PHRED33, 30), 0.0);
}
}