1use std::io;
5use std::sync::atomic::{AtomicBool, Ordering};
6use std::sync::{Arc, Mutex};
7
8use crate::config::Limits;
9use crate::modules::QCModule;
10use crate::sequence::Sequence;
11use crate::utils::base_counts::{count_acgt, IDX_A, IDX_C, IDX_G, IDX_T};
12use crate::utils::phred;
13
14const PUBLISH_INTERVAL: u32 = 256;
19
20#[derive(Debug, Clone, Copy)]
30pub struct BasicStatsCounters {
31 actual_count: u64,
32 min_length: usize,
33 max_length: usize,
34 total_bases: u64,
35 g_count: u64,
36 c_count: u64,
37 a_count: u64,
38 t_count: u64,
39 lowest_char: u16,
40 known_encoding: Option<phred::PhredEncoding>,
42 colorspace: Option<bool>,
45 median_length: usize,
48}
49
50impl Default for BasicStatsCounters {
56 fn default() -> Self {
57 BasicStatsCounters {
58 actual_count: 0,
59 min_length: 0,
60 max_length: 0,
61 total_bases: 0,
62 g_count: 0,
63 c_count: 0,
64 a_count: 0,
65 t_count: 0,
66 lowest_char: phred::NO_QUALITY_SEEN,
67 known_encoding: None,
68 colorspace: None,
69 median_length: 0,
70 }
71 }
72}
73
74impl BasicStatsCounters {
75 pub const MEASURES: [&'static str; 8] = [
78 "File type",
79 "Encoding",
80 "Total Sequences",
81 "Total Bases",
82 "Sequence length",
83 "Mean Length",
84 "Median Length",
85 "%GC",
86 ];
87
88 pub fn rows(&self) -> Vec<(&'static str, String)> {
92 let file_type = match self.colorspace {
94 None => "null",
95 Some(true) => "Colorspace converted to bases",
96 Some(false) => "Conventional base calls",
97 };
98
99 let encoding = phred::resolve(self.known_encoding, self.lowest_char)
102 .map(|e| e.name.to_string())
103 .unwrap_or_else(|_| "Unknown".to_string());
104
105 let sequence_length = if self.min_length == self.max_length {
106 self.min_length.to_string()
107 } else {
108 format!("{}-{}", self.min_length, self.max_length)
109 };
110
111 let mean_length = self.total_bases.checked_div(self.actual_count).unwrap_or(0);
113
114 let total = self.a_count + self.t_count + self.g_count + self.c_count;
116 let gc = ((self.g_count + self.c_count) * 100)
117 .checked_div(total)
118 .unwrap_or(0);
119
120 let values = [
121 file_type.to_string(),
122 encoding,
123 self.actual_count.to_string(),
124 format_length(self.total_bases),
125 sequence_length,
126 mean_length.to_string(),
127 self.median_length.to_string(),
128 gc.to_string(),
129 ];
130 Self::MEASURES.into_iter().zip(values).collect()
131 }
132}
133
134pub struct LiveStats {
147 snapshot: Mutex<Option<BasicStatsCounters>>,
148 wanted: AtomicBool,
149}
150
151impl Default for LiveStats {
152 fn default() -> Self {
153 Self {
154 snapshot: Mutex::new(None),
155 wanted: AtomicBool::new(true),
156 }
157 }
158}
159
160impl LiveStats {
161 pub fn new() -> Self {
162 Self::default()
163 }
164
165 pub fn request(&self) {
167 self.wanted.store(true, Ordering::Relaxed);
168 }
169
170 fn take_request(&self) -> bool {
172 self.wanted.load(Ordering::Relaxed) && self.wanted.swap(false, Ordering::Relaxed)
173 }
174
175 pub fn snapshot(&self) -> Option<BasicStatsCounters> {
178 *self.snapshot.lock().unwrap_or_else(|e| e.into_inner())
179 }
180
181 fn publish(&self, counters: BasicStatsCounters) {
182 *self.snapshot.lock().unwrap_or_else(|e| e.into_inner()) = Some(counters);
183 }
184}
185
186pub fn format_length(original_length: u64) -> String {
191 let mut length = original_length as f64;
192
193 let unit = if length >= 1_000_000_000.0 {
194 length /= 1_000_000_000.0;
195 " Gbp"
196 } else if length >= 1_000_000.0 {
197 length /= 1_000_000.0;
198 " Mbp"
199 } else if length >= 1_000.0 {
200 length /= 1_000.0;
201 " kbp"
202 } else {
203 " bp"
204 };
205
206 let raw = format!("{}", length);
210 let chars: Vec<char> = raw.chars().collect();
211
212 let mut last_index = 0;
213
214 for (i, &ch) in chars.iter().enumerate() {
216 last_index = i;
217 if ch == '.' {
218 break;
219 }
220 }
221
222 if last_index + 1 < chars.len() && chars[last_index + 1] != '0' {
224 last_index += 1;
225 } else if last_index > 0 && chars[last_index] == '.' {
226 last_index -= 1;
228 }
229
230 let truncated: String = chars[..=last_index].iter().collect();
231 format!("{}{}", truncated, unit)
232}
233
234pub struct BasicStats {
235 name: Option<String>,
236 counters: BasicStatsCounters,
237 length_counts: Vec<u64>,
240 live: Option<Arc<LiveStats>>,
242}
243
244impl BasicStats {
245 pub fn new(_limits: &Limits) -> Self {
246 BasicStats {
247 name: None,
248 counters: BasicStatsCounters::default(),
249 length_counts: Vec::new(),
250 live: None,
251 }
252 }
253
254 pub fn set_file_name(&mut self, name: &str) {
258 let name = name.strip_prefix("stdin:").unwrap_or(name);
259 self.name = Some(name.to_string());
260 }
261
262 fn publish(&mut self) {
264 if let Some(ref live) = self.live {
265 self.counters.median_length = median_length(&self.length_counts);
266 live.publish(self.counters);
267 }
268 }
269}
270
271fn median_length(length_counts: &[u64]) -> usize {
274 let rank50 = length_counts.iter().sum::<u64>() / 2;
275 let mut running = 0;
276 for (len, &count) in length_counts.iter().enumerate() {
277 running += count;
278 if running > rank50 {
279 return len;
280 }
281 }
282 0
283}
284
285impl QCModule for BasicStats {
286 fn cost_hint(&self) -> u32 {
287 2
288 }
289
290 fn process_sequence(&mut self, sequence: &Sequence) {
291 let seen = self.counters.actual_count;
295 if seen != 0
296 && seen.is_multiple_of(PUBLISH_INTERVAL as u64)
297 && self.live.as_ref().is_some_and(|live| live.take_request())
298 {
299 self.publish();
300 }
301
302 let c = &mut self.counters;
303 c.actual_count += 1;
304 c.total_bases += sequence.sequence.len() as u64;
305
306 let len = sequence.sequence.len();
307 if !sequence.is_filtered {
308 if self.length_counts.len() <= len {
309 self.length_counts.resize(len + 1, 0);
310 }
311 self.length_counts[len] += 1;
312 }
313 if c.actual_count == 1 {
314 c.colorspace = Some(sequence.colorspace.is_some());
315 c.min_length = len;
316 c.max_length = len;
317 } else {
318 c.min_length = c.min_length.min(len);
319 c.max_length = c.max_length.max(len);
320 }
321
322 let counts = count_acgt(&sequence.sequence);
323 c.a_count += counts[IDX_A];
324 c.c_count += counts[IDX_C];
325 c.g_count += counts[IDX_G];
326 c.t_count += counts[IDX_T];
327
328 if let Some(lowest) = sequence.quality.iter().copied().min() {
329 c.lowest_char = c.lowest_char.min(lowest as u16);
330 }
331 }
332
333 fn attach_live_stats(&mut self, live: Arc<LiveStats>) {
334 self.live = Some(live);
335 }
336
337 fn finalize(&mut self) {
340 if self.counters.known_encoding.is_none() {
341 if let Some(warning) = phred::phred64_suspicion(self.counters.lowest_char) {
342 crate::progress::log_line(&match &self.name {
344 Some(name) => format!("{name}: {warning}"),
345 None => warning,
346 });
347 }
348 }
349 self.publish();
350 }
351
352 fn set_filename(&mut self, name: &str) {
353 self.set_file_name(name);
354 }
355
356 fn set_phred_encoding(&mut self, encoding: phred::PhredEncoding) {
357 self.counters.known_encoding = Some(encoding);
358 }
359
360 fn name(&self) -> &str {
361 "Basic Statistics"
362 }
363
364 fn description(&self) -> &str {
365 "Calculates some basic statistics about the file"
366 }
367
368 fn reset(&mut self) {
369 self.counters.min_length = 0;
370 self.counters.max_length = 0;
371 self.counters.g_count = 0;
372 self.counters.c_count = 0;
373 self.counters.a_count = 0;
374 self.counters.t_count = 0;
375 }
376
377 fn raises_error(&self) -> bool {
379 false
380 }
381
382 fn raises_warning(&self) -> bool {
383 false
384 }
385
386 fn ignore_filtered_sequences(&self) -> bool {
387 false
388 }
389
390 fn ignore_in_report(&self) -> bool {
391 false
392 }
393
394 fn write_text_report(&self, writer: &mut dyn io::Write) -> io::Result<()> {
395 phred::resolve(self.counters.known_encoding, self.counters.lowest_char)
397 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
398
399 writeln!(writer, "#Measure\tValue")?;
401
402 writeln!(writer, "Filename\t{}", self.name.as_deref().unwrap_or(""))?;
404
405 let counters = BasicStatsCounters {
406 median_length: median_length(&self.length_counts),
407 ..self.counters
408 };
409 for (measure, value) in counters.rows() {
410 writeln!(writer, "{}\t{}", measure, value)?;
411 }
412
413 Ok(())
414 }
415}
416
417#[cfg(test)]
418mod tests {
419 use super::*;
420
421 #[test]
422 fn test_format_length_bp() {
423 assert_eq!(format_length(16), "16 bp");
424 assert_eq!(format_length(80), "80 bp");
425 assert_eq!(format_length(999), "999 bp");
426 }
427
428 #[test]
429 fn test_format_length_kbp() {
430 assert_eq!(format_length(1000), "1 kbp");
431 assert_eq!(format_length(1500), "1.5 kbp");
432 assert_eq!(format_length(10000), "10 kbp");
433 }
434
435 #[test]
436 fn test_format_length_mbp() {
437 assert_eq!(format_length(1_000_000), "1 Mbp");
438 assert_eq!(format_length(1_200_000), "1.2 Mbp");
439 }
440
441 #[test]
442 fn test_format_length_gbp() {
443 assert_eq!(format_length(1_000_000_000), "1 Gbp");
444 }
445
446 fn sequences(count: usize) -> Vec<Sequence> {
447 (0..count)
448 .map(|i| {
449 let len = 40 + (i % 7);
451 let bases: Vec<u8> = (0..len).map(|p| b"ACGTGGCN"[(i * 3 + p) % 8]).collect();
452 let quality = vec![b'I'; len];
453 Sequence::new(format!("READ{}", i), bases, quality)
454 })
455 .collect()
456 }
457
458 fn text_rows(module: &BasicStats) -> Vec<(String, String)> {
459 let mut buf = Vec::new();
460 module.write_text_report(&mut buf).expect("text report");
461 String::from_utf8(buf)
462 .expect("utf8")
463 .lines()
464 .skip(2) .filter_map(|line| line.split_once('\t'))
466 .map(|(k, v)| (k.to_string(), v.to_string()))
467 .collect()
468 }
469
470 #[test]
474 fn test_live_snapshot_matches_report() {
475 let limits = Limits::new();
476 let live = Arc::new(LiveStats::new());
477 let mut module = BasicStats::new(&limits);
478 module.set_file_name("sample.fastq");
479 module.attach_live_stats(Arc::clone(&live));
480
481 assert!(live.snapshot().is_none());
484
485 let seqs = sequences(PUBLISH_INTERVAL as usize * 4);
487 for seq in &seqs {
488 module.process_sequence(seq);
489 }
490
491 let mid = live.snapshot().expect("published during the run");
493 assert!(mid.actual_count > 0);
494 assert!(mid.actual_count <= seqs.len() as u64);
495
496 module.finalize();
497 let final_snapshot = live.snapshot().expect("published at finalize");
498 assert_eq!(final_snapshot.actual_count, seqs.len() as u64);
499
500 let expected: Vec<(String, String)> = final_snapshot
501 .rows()
502 .into_iter()
503 .map(|(k, v)| (k.to_string(), v))
504 .collect();
505 assert_eq!(text_rows(&module), expected);
506 }
507
508 #[test]
511 fn test_live_stats_publish_on_request() {
512 let limits = Limits::new();
513 let live = Arc::new(LiveStats::new());
514 let mut module = BasicStats::new(&limits);
515 module.attach_live_stats(Arc::clone(&live));
516
517 let seqs = sequences(PUBLISH_INTERVAL as usize * 4);
518 let (first, rest) = seqs.split_at(PUBLISH_INTERVAL as usize * 2);
519 for seq in first {
520 module.process_sequence(seq);
521 }
522 let published = live.snapshot().expect("first snapshot").actual_count;
523 assert_eq!(published, PUBLISH_INTERVAL as u64);
524
525 let (unasked, asked) = rest.split_at(PUBLISH_INTERVAL as usize);
526 for seq in unasked {
527 module.process_sequence(seq);
528 }
529 assert_eq!(live.snapshot().unwrap().actual_count, published);
530
531 live.request();
532 for seq in asked {
533 module.process_sequence(seq);
534 }
535 assert!(live.snapshot().unwrap().actual_count > published);
536 }
537
538 #[test]
540 fn test_no_live_stats_by_default() {
541 let limits = Limits::new();
542 let mut module = BasicStats::new(&limits);
543 for seq in &sequences(100) {
544 module.process_sequence(seq);
545 }
546 module.finalize();
547 let rows = text_rows(&module);
548 assert_eq!(rows[2], ("Total Sequences".to_string(), "100".to_string()));
549 }
550
551 #[test]
555 fn test_counters_rows_placeholder_state() {
556 let counters = BasicStatsCounters::default();
557 let rows = counters.rows();
558 assert_eq!(rows.len(), 8);
559 assert_eq!(rows[0], ("File type", "null".to_string()));
560 assert_eq!(rows[2], ("Total Sequences", "0".to_string()));
561 assert_eq!(rows[7], ("%GC", "0".to_string()));
563 }
564}