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)]
22pub enum QualityEncoding {
23 #[default]
25 Phred33,
26 Phred64,
28}
29
30impl QualityEncoding {
31 pub const fn offset(self) -> u8 {
33 match self {
34 QualityEncoding::Phred33 => PHRED33,
35 QualityEncoding::Phred64 => PHRED64,
36 }
37 }
38
39 pub fn detect(quality: &[u8]) -> Option<QualityEncoding> {
52 let mut min = u8::MAX;
53 let mut max = 0u8;
54 for &q in quality {
55 min = min.min(q);
56 max = max.max(q);
57 }
58 if quality.is_empty() {
59 return None;
60 }
61 if min < PHRED64 {
65 Some(QualityEncoding::Phred33)
66 } else if max > b'J' {
67 Some(QualityEncoding::Phred64)
68 } else {
69 None
70 }
71 }
72}
73
74#[inline]
76pub fn score(ch: u8, offset: u8) -> u8 {
77 ch.saturating_sub(offset)
78}
79
80#[inline]
82pub fn encode(score: u8, offset: u8) -> u8 {
83 offset.saturating_add(score.min(126 - offset))
84}
85
86pub fn scores(quality: &[u8], offset: u8) -> Vec<u8> {
88 quality.iter().map(|&q| score(q, offset)).collect()
89}
90
91#[inline]
99pub fn error_probability(score: u8) -> f64 {
100 10f64.powf(-(score as f64) / 10.0)
101}
102
103pub fn probability_to_score(p: f64) -> u8 {
105 if p <= 0.0 {
106 return 93;
107 }
108 let q = -10.0 * p.log10();
109 q.round().clamp(0.0, 93.0) as u8
110}
111
112pub fn mean_score(quality: &[u8], offset: u8) -> Option<f64> {
114 if quality.is_empty() {
115 return None;
116 }
117 let sum: u64 = quality.iter().map(|&q| score(q, offset) as u64).sum();
118 Some(sum as f64 / quality.len() as f64)
119}
120
121pub fn expected_errors(quality: &[u8], offset: u8) -> f64 {
126 quality
127 .iter()
128 .map(|&q| error_probability(score(q, offset)))
129 .sum()
130}
131
132pub fn mean_quality(quality: &[u8], offset: u8) -> Option<f64> {
134 if quality.is_empty() {
135 return None;
136 }
137 let mean_p = expected_errors(quality, offset) / quality.len() as f64;
138 Some(-10.0 * mean_p.log10())
139}
140
141pub fn fraction_at_least(quality: &[u8], offset: u8, threshold: u8) -> f64 {
143 if quality.is_empty() {
144 return 0.0;
145 }
146 let n = quality
147 .iter()
148 .filter(|&&q| score(q, offset) >= threshold)
149 .count();
150 n as f64 / quality.len() as f64
151}
152
153pub fn trim_ends(quality: &[u8], offset: u8, min_score: u8) -> Range<usize> {
157 let start = quality.iter().position(|&q| score(q, offset) >= min_score);
158 match start {
159 None => 0..0,
160 Some(start) => {
161 let end = quality
162 .iter()
163 .rposition(|&q| score(q, offset) >= min_score)
164 .unwrap()
165 + 1;
166 start..end
167 }
168 }
169}
170
171pub fn trim_mott(quality: &[u8], offset: u8, threshold: u8) -> Range<usize> {
176 let mut best = 0..0;
177 let mut best_score = 0i64;
178 let mut running = 0i64;
179 let mut start = 0usize;
180
181 for (i, &q) in quality.iter().enumerate() {
182 running += score(q, offset) as i64 - threshold as i64;
183 if running < 0 {
184 running = 0;
185 start = i + 1;
186 } else if running > best_score {
187 best_score = running;
188 best = start..i + 1;
189 }
190 }
191 best
192}
193
194pub fn trim_sliding_window(
201 quality: &[u8],
202 offset: u8,
203 window: usize,
204 min_mean: f64,
205) -> Range<usize> {
206 if window == 0 || quality.len() < window {
207 return 0..quality.len();
208 }
209 let threshold = min_mean * window as f64;
210 let mut sum: f64 = quality[..window]
211 .iter()
212 .map(|&q| score(q, offset) as f64)
213 .sum();
214 if sum < threshold {
215 return 0..0;
216 }
217 for i in window..quality.len() {
218 sum += score(quality[i], offset) as f64;
219 sum -= score(quality[i - window], offset) as f64;
220 if sum < threshold {
221 return 0..i + 1 - window;
222 }
223 }
224 0..quality.len()
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 #[test]
232 fn scores_round_trip() {
233 for s in 0u8..=60 {
234 assert_eq!(score(encode(s, PHRED33), PHRED33), s);
235 }
236 }
237
238 #[test]
239 fn error_probabilities() {
240 assert_eq!(probability_to_score(0.001), 30);
241 assert_eq!(probability_to_score(1.0), 0);
242 assert!((expected_errors(b"!!!!", PHRED33) - 4.0).abs() < 1e-9);
243 }
244
245 #[test]
246 fn mean_quality_is_error_weighted() {
247 let q = b"IIIII!"; let arithmetic = mean_score(q, PHRED33).unwrap();
251 let weighted = mean_quality(q, PHRED33).unwrap();
252 assert!(arithmetic > 33.0, "{arithmetic}");
253 assert!(weighted < 8.0, "{weighted}");
254 }
255
256 #[test]
257 fn trims_ends() {
258 assert_eq!(trim_ends(b"!!III!!", PHRED33, 30), 2..5);
259 assert_eq!(trim_ends(b"!!!!", PHRED33, 30), 0..0);
260 assert_eq!(trim_ends(b"IIII", PHRED33, 30), 0..4);
261 }
262
263 #[test]
264 fn mott_keeps_best_window() {
265 let q = b"###IIIIIIII###";
267 assert_eq!(trim_mott(q, PHRED33, 20), 3..11);
268 assert_eq!(trim_mott(b"####", PHRED33, 20), 0..0);
269 }
270
271 #[test]
272 fn sliding_window_cuts_at_drop() {
273 let q = b"IIIIIIII####";
276 assert_eq!(trim_sliding_window(q, PHRED33, 4, 20.0), 0..7);
277 assert_eq!(trim_sliding_window(b"IIIIIIII", PHRED33, 4, 20.0), 0..8);
278 assert_eq!(trim_sliding_window(b"II", PHRED33, 4, 20.0), 0..2);
279 assert_eq!(trim_sliding_window(b"####IIII", PHRED33, 4, 20.0), 0..0);
280 }
281
282 #[test]
283 fn q30_fraction() {
284 assert!((fraction_at_least(b"IIII!!!!", PHRED33, 30) - 0.5).abs() < 1e-9);
285 assert_eq!(fraction_at_least(b"", PHRED33, 30), 0.0);
286 }
287}