1use std::ops::Range;
9
10pub const PHRED33: u8 = 33;
12pub const PHRED64: u8 = 64;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
17pub enum QualityEncoding {
18 #[default]
20 Phred33,
21 Phred64,
23}
24
25impl QualityEncoding {
26 pub const fn offset(self) -> u8 {
28 match self {
29 QualityEncoding::Phred33 => PHRED33,
30 QualityEncoding::Phred64 => PHRED64,
31 }
32 }
33
34 pub fn detect(quality: &[u8]) -> Option<QualityEncoding> {
47 let mut min = u8::MAX;
48 let mut max = 0u8;
49 for &q in quality {
50 min = min.min(q);
51 max = max.max(q);
52 }
53 if quality.is_empty() {
54 return None;
55 }
56 if min < PHRED64 {
60 Some(QualityEncoding::Phred33)
61 } else if max > b'J' {
62 Some(QualityEncoding::Phred64)
63 } else {
64 None
65 }
66 }
67}
68
69#[inline]
71pub fn score(ch: u8, offset: u8) -> u8 {
72 ch.saturating_sub(offset)
73}
74
75#[inline]
77pub fn encode(score: u8, offset: u8) -> u8 {
78 offset.saturating_add(score.min(126 - offset))
79}
80
81pub fn scores(quality: &[u8], offset: u8) -> Vec<u8> {
83 quality.iter().map(|&q| score(q, offset)).collect()
84}
85
86#[inline]
94pub fn error_probability(score: u8) -> f64 {
95 10f64.powf(-(score as f64) / 10.0)
96}
97
98pub fn probability_to_score(p: f64) -> u8 {
100 if p <= 0.0 {
101 return 93;
102 }
103 let q = -10.0 * p.log10();
104 q.round().clamp(0.0, 93.0) as u8
105}
106
107pub fn mean_score(quality: &[u8], offset: u8) -> Option<f64> {
109 if quality.is_empty() {
110 return None;
111 }
112 let sum: u64 = quality.iter().map(|&q| score(q, offset) as u64).sum();
113 Some(sum as f64 / quality.len() as f64)
114}
115
116pub fn expected_errors(quality: &[u8], offset: u8) -> f64 {
121 quality
122 .iter()
123 .map(|&q| error_probability(score(q, offset)))
124 .sum()
125}
126
127pub fn mean_quality(quality: &[u8], offset: u8) -> Option<f64> {
129 if quality.is_empty() {
130 return None;
131 }
132 let mean_p = expected_errors(quality, offset) / quality.len() as f64;
133 Some(-10.0 * mean_p.log10())
134}
135
136pub fn fraction_at_least(quality: &[u8], offset: u8, threshold: u8) -> f64 {
138 if quality.is_empty() {
139 return 0.0;
140 }
141 let n = quality
142 .iter()
143 .filter(|&&q| score(q, offset) >= threshold)
144 .count();
145 n as f64 / quality.len() as f64
146}
147
148pub fn trim_ends(quality: &[u8], offset: u8, min_score: u8) -> Range<usize> {
152 let start = quality.iter().position(|&q| score(q, offset) >= min_score);
153 match start {
154 None => 0..0,
155 Some(start) => {
156 let end = quality
157 .iter()
158 .rposition(|&q| score(q, offset) >= min_score)
159 .unwrap()
160 + 1;
161 start..end
162 }
163 }
164}
165
166pub fn trim_mott(quality: &[u8], offset: u8, threshold: u8) -> Range<usize> {
171 let mut best = 0..0;
172 let mut best_score = 0i64;
173 let mut running = 0i64;
174 let mut start = 0usize;
175
176 for (i, &q) in quality.iter().enumerate() {
177 running += score(q, offset) as i64 - threshold as i64;
178 if running < 0 {
179 running = 0;
180 start = i + 1;
181 } else if running > best_score {
182 best_score = running;
183 best = start..i + 1;
184 }
185 }
186 best
187}
188
189pub fn trim_sliding_window(
196 quality: &[u8],
197 offset: u8,
198 window: usize,
199 min_mean: f64,
200) -> Range<usize> {
201 if window == 0 || quality.len() < window {
202 return 0..quality.len();
203 }
204 let threshold = min_mean * window as f64;
205 let mut sum: f64 = quality[..window]
206 .iter()
207 .map(|&q| score(q, offset) as f64)
208 .sum();
209 if sum < threshold {
210 return 0..0;
211 }
212 for i in window..quality.len() {
213 sum += score(quality[i], offset) as f64;
214 sum -= score(quality[i - window], offset) as f64;
215 if sum < threshold {
216 return 0..i + 1 - window;
217 }
218 }
219 0..quality.len()
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 #[test]
227 fn scores_round_trip() {
228 for s in 0u8..=60 {
229 assert_eq!(score(encode(s, PHRED33), PHRED33), s);
230 }
231 }
232
233 #[test]
234 fn error_probabilities() {
235 assert_eq!(probability_to_score(0.001), 30);
236 assert_eq!(probability_to_score(1.0), 0);
237 assert!((expected_errors(b"!!!!", PHRED33) - 4.0).abs() < 1e-9);
238 }
239
240 #[test]
241 fn mean_quality_is_error_weighted() {
242 let q = b"IIIII!"; let arithmetic = mean_score(q, PHRED33).unwrap();
246 let weighted = mean_quality(q, PHRED33).unwrap();
247 assert!(arithmetic > 33.0, "{arithmetic}");
248 assert!(weighted < 8.0, "{weighted}");
249 }
250
251 #[test]
252 fn trims_ends() {
253 assert_eq!(trim_ends(b"!!III!!", PHRED33, 30), 2..5);
254 assert_eq!(trim_ends(b"!!!!", PHRED33, 30), 0..0);
255 assert_eq!(trim_ends(b"IIII", PHRED33, 30), 0..4);
256 }
257
258 #[test]
259 fn mott_keeps_best_window() {
260 let q = b"###IIIIIIII###";
262 assert_eq!(trim_mott(q, PHRED33, 20), 3..11);
263 assert_eq!(trim_mott(b"####", PHRED33, 20), 0..0);
264 }
265
266 #[test]
267 fn sliding_window_cuts_at_drop() {
268 let q = b"IIIIIIII####";
271 assert_eq!(trim_sliding_window(q, PHRED33, 4, 20.0), 0..7);
272 assert_eq!(trim_sliding_window(b"IIIIIIII", PHRED33, 4, 20.0), 0..8);
273 assert_eq!(trim_sliding_window(b"II", PHRED33, 4, 20.0), 0..2);
274 assert_eq!(trim_sliding_window(b"####IIII", PHRED33, 4, 20.0), 0..0);
275 }
276
277 #[test]
278 fn q30_fraction() {
279 assert!((fraction_at_least(b"IIII!!!!", PHRED33, 30) - 0.5).abs() < 1e-9);
280 assert_eq!(fraction_at_least(b"", PHRED33, 30), 0.0);
281 }
282}