1use std::collections::HashMap;
5use std::io;
6use std::sync::{Arc, Mutex};
7
8use crate::config::{Limits, LimitsExt};
9use crate::modules::QCModule;
10use crate::sequence::Sequence;
11use crate::utils::format::java_format_double;
12use crate::utils::java_hashmap;
13
14#[derive(Default)]
17pub struct OverRepresentedData {
18 pub sequences: HashMap<String, u64>,
20 pub count: u64,
22 pub count_at_unique_limit: u64,
24}
25
26impl OverRepresentedData {
27 pub fn new() -> Self {
28 Self::default()
29 }
30}
31
32struct ContaminantHit {
34 name: String,
35 length: usize,
36 percent_id: usize,
37}
38
39impl ContaminantHit {
40 fn is_better_than(&self, other: &Option<ContaminantHit>) -> bool {
44 match other {
45 None => true,
46 Some(b) => {
47 self.length > b.length
48 || (self.length == b.length && self.percent_id > b.percent_id)
49 }
50 }
51 }
52}
53
54impl std::fmt::Display for ContaminantHit {
55 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56 write!(
58 f,
59 "{} ({}% over {}bp)",
60 self.name, self.percent_id, self.length
61 )
62 }
63}
64
65struct OverrepresentedSeq {
67 seq: String,
68 count: u64,
69 percentage: f64,
70 contaminant_hit: Option<ContaminantHit>,
71}
72
73struct Contaminant {
75 name: String,
76 forward: Vec<u8>,
77 reverse: Vec<u8>,
78}
79
80impl Contaminant {
81 fn new(name: &str, sequence: &str) -> Self {
82 let forward: Vec<u8> = sequence.to_uppercase().bytes().collect();
83 let mut reverse = vec![0u8; forward.len()];
85 for (c, &base) in forward.iter().enumerate() {
86 let rev_pos = (forward.len() - 1) - c;
87 reverse[rev_pos] = match base {
88 b'G' => b'C',
89 b'A' => b'T',
90 b'T' => b'A',
91 b'C' => b'G',
92 _ => base,
93 };
94 }
95 Contaminant {
96 name: name.to_string(),
97 forward,
98 reverse,
99 }
100 }
101
102 fn find_match(&self, query: &str) -> Option<ContaminantHit> {
106 let query_upper = query.to_uppercase();
107
108 if query_upper.len() < 20 && query_upper.len() >= 8 {
110 let forward_str = std::str::from_utf8(&self.forward).unwrap_or("");
111 let reverse_str = std::str::from_utf8(&self.reverse).unwrap_or("");
112
113 if forward_str.contains(&query_upper) {
114 return Some(ContaminantHit {
115 name: self.name.clone(),
116 length: query_upper.len(),
117 percent_id: 100,
118 });
119 }
120 if reverse_str.contains(&query_upper) {
121 return Some(ContaminantHit {
122 name: self.name.clone(),
123 length: query_upper.len(),
124 percent_id: 100,
125 });
126 }
127 }
128
129 let q: Vec<u8> = query_upper.bytes().collect();
130 let mut best_hit: Option<ContaminantHit> = None;
131
132 best_hit = Self::find_strand_match(&self.forward, &q, best_hit, &self.name);
134 best_hit = Self::find_strand_match(&self.reverse, &q, best_hit, &self.name);
135
136 best_hit
137 }
138
139 fn find_strand_match(
140 ca: &[u8],
141 cb: &[u8],
142 mut best_hit: Option<ContaminantHit>,
143 name: &str,
144 ) -> Option<ContaminantHit> {
145 let min_offset = -(ca.len() as isize - 20);
146 let max_offset = cb.len() as isize - 20;
147
148 for offset in min_offset..max_offset {
149 if let Some(hit) = Self::find_match_at_offset(ca, cb, offset, name) {
150 if hit.is_better_than(&best_hit) {
151 best_hit = Some(hit);
152 }
153 }
154 }
155 best_hit
156 }
157
158 fn find_match_at_offset(
160 ca: &[u8],
161 cb: &[u8],
162 offset: isize,
163 name: &str,
164 ) -> Option<ContaminantHit> {
165 let mut best_hit: Option<ContaminantHit> = None;
166 let mut mismatch_count: usize = 0;
167 let mut start: isize = 0;
169 let mut end: isize = 0;
170
171 for (i, &ca_byte) in ca.iter().enumerate() {
173 let j = i as isize + offset;
174 if j < 0 {
175 start = i as isize + 1;
176 continue;
177 }
178 if j >= cb.len() as isize {
179 break;
180 }
181
182 if ca_byte == cb[j as usize] {
183 end = i as isize;
184 } else {
185 mismatch_count += 1;
186 if mismatch_count > 1 {
187 if end >= start {
189 let match_len = (1 + end - start) as usize;
190 if match_len > 20 {
191 let id = ((match_len - (mismatch_count - 1)) * 100) / match_len;
192 let candidate = ContaminantHit {
193 name: name.to_string(),
194 length: match_len,
195 percent_id: id,
196 };
197 if candidate.is_better_than(&best_hit) {
198 best_hit = Some(candidate);
199 }
200 }
201 }
202 start = i as isize + 1;
203 end = i as isize + 1;
204 mismatch_count = 0;
205 }
206 }
207 }
208
209 if end < start {
211 return best_hit;
212 }
213 let match_len = (1 + end - start) as usize;
214 if match_len > 20 {
215 let id = ((match_len - mismatch_count) * 100) / match_len;
216 let candidate = ContaminantHit {
217 name: name.to_string(),
218 length: match_len,
219 percent_id: id,
220 };
221 if candidate.is_better_than(&best_hit) {
222 best_hit = Some(candidate);
223 }
224 }
225
226 best_hit
227 }
228}
229
230fn find_contaminant_hit(query: &str, contaminants: &[Contaminant]) -> Option<ContaminantHit> {
233 let mut best_hit: Option<ContaminantHit> = None;
234
235 for contaminant in contaminants {
236 if let Some(hit) = contaminant.find_match(query) {
237 if hit.is_better_than(&best_hit) {
238 best_hit = Some(hit);
239 }
240 }
241 }
242
243 best_hit
244}
245
246pub struct OverRepresentedSeqs {
247 pub shared_data: Arc<Mutex<OverRepresentedData>>,
248 unique_sequence_count: usize,
250 frozen: bool,
251 dup_length: usize,
252 contaminants: Vec<Contaminant>,
253 limits: Limits,
254 computed: Option<Vec<OverrepresentedSeq>>,
256}
257
258const OBSERVATION_CUTOFF: usize = 100_000;
260
261impl OverRepresentedSeqs {
262 pub fn new(
263 limits: &Limits,
264 dup_length: usize,
265 contaminant_entries: &[(String, String)],
266 shared_data: Arc<Mutex<OverRepresentedData>>,
267 ) -> Self {
268 let contaminants: Vec<Contaminant> = contaminant_entries
269 .iter()
270 .map(|(name, seq)| Contaminant::new(name, seq))
271 .collect();
272
273 OverRepresentedSeqs {
274 shared_data,
275 unique_sequence_count: 0,
276 frozen: false,
277 dup_length,
278 contaminants,
279 limits: limits.clone(),
280 computed: None,
281 }
282 }
283
284 fn get_overrepresented_seqs(&mut self) {
285 if self.computed.is_some() {
286 return;
287 }
288
289 let warn_threshold = self.limits.threshold("overrepresented\twarn", 0.1);
290
291 let data = self.shared_data.lock().unwrap_or_else(|e| e.into_inner());
294 let total_count = data.count;
295
296 let mut keepers: Vec<OverrepresentedSeq> = Vec::new();
297
298 for (seq, &count) in &data.sequences {
299 let percentage = (count as f64 / total_count as f64) * 100.0;
300 if percentage > warn_threshold {
301 let hit = find_contaminant_hit(seq, &self.contaminants);
302 keepers.push(OverrepresentedSeq {
303 seq: seq.clone(),
304 count,
305 percentage,
306 contaminant_hit: hit,
307 });
308 }
309 }
310
311 let capacity = java_hashmap::table_capacity(data.sequences.len());
313 keepers.sort_by_cached_key(|k| {
314 let bucket = java_hashmap::bucket(java_hashmap::string_hash(&k.seq), capacity);
315 (std::cmp::Reverse(k.count), bucket, k.seq.clone())
316 });
317
318 self.computed = Some(keepers);
319 }
320
321 fn ensure_calculated(&self) -> &[OverrepresentedSeq] {
322 self.computed.as_deref().unwrap_or(&[])
323 }
324}
325
326impl QCModule for OverRepresentedSeqs {
327 fn cost_hint(&self) -> u32 {
328 4
329 }
330
331 fn process_sequence(&mut self, sequence: &Sequence) {
332 self.computed = None;
333
334 let mut data = self.shared_data.lock().unwrap_or_else(|e| e.into_inner());
335 data.count += 1;
336
337 let seq_bytes = &sequence.sequence;
340 let truncate_len = if self.dup_length != 0 && seq_bytes.len() > self.dup_length {
341 self.dup_length
342 } else if seq_bytes.len() > 50 {
343 50
345 } else {
346 seq_bytes.len()
347 };
348 let seq = std::str::from_utf8(&seq_bytes[..truncate_len]).unwrap_or("");
349
350 if let Some(count) = data.sequences.get_mut(seq) {
351 *count += 1;
352 if !self.frozen {
354 data.count_at_unique_limit = data.count;
355 }
356 } else if !self.frozen {
357 data.sequences.insert(seq.to_string(), 1);
358 self.unique_sequence_count += 1;
359 data.count_at_unique_limit = data.count;
360 if self.unique_sequence_count == OBSERVATION_CUTOFF {
361 self.frozen = true;
362 }
363 }
364 }
365
366 fn finalize(&mut self) {
367 self.get_overrepresented_seqs();
368 }
369
370 fn name(&self) -> &str {
371 "Overrepresented sequences"
372 }
373
374 fn description(&self) -> &str {
375 "Identifies sequences which are overrepresented in the set"
376 }
377
378 fn reset(&mut self) {
379 let mut data = self.shared_data.lock().unwrap_or_else(|e| e.into_inner());
380 data.count = 0;
381 data.count_at_unique_limit = 0;
382 data.sequences.clear();
383 self.unique_sequence_count = 0;
384 self.frozen = false;
385 self.computed = None;
386 }
387
388 fn raises_error(&self) -> bool {
389 let error_threshold = self.limits.threshold("overrepresented\terror", 1.0);
390 let seqs = self.ensure_calculated();
391 seqs.first().is_some_and(|s| s.percentage > error_threshold)
393 }
394
395 fn raises_warning(&self) -> bool {
396 let seqs = self.ensure_calculated();
397 !seqs.is_empty()
399 }
400
401 fn ignore_filtered_sequences(&self) -> bool {
402 true
403 }
404
405 fn ignore_in_report(&self) -> bool {
406 self.limits.is_ignored("overrepresented")
407 }
408
409 fn write_html_report(&self, writer: &mut dyn io::Write, _png: bool) -> io::Result<()> {
410 if self.ensure_calculated().is_empty() {
411 return write!(writer, "<p>No overrepresented sequences</p>");
412 }
413 let mut text = Vec::new();
414 self.write_text_report(&mut text)?;
415 crate::report::html::write_default_html_table(&String::from_utf8_lossy(&text), writer)
416 }
417
418 fn write_text_report(&self, writer: &mut dyn io::Write) -> io::Result<()> {
419 let seqs = self.ensure_calculated();
420
421 if seqs.is_empty() {
425 return Ok(());
426 }
427
428 writeln!(writer, "#Sequence\tCount\tPercentage\tPossible Source")?;
429 for s in seqs {
430 let source = match &s.contaminant_hit {
431 Some(hit) => hit.to_string(),
432 None => "No Hit".to_string(),
433 };
434 writeln!(
437 writer,
438 "{}\t{}\t{}\t{}",
439 s.seq,
440 s.count,
441 java_format_double((s.percentage * 100.0).round() / 100.0),
442 source
443 )?;
444 }
445
446 Ok(())
447 }
448}
449
450#[cfg(test)]
451mod tests {
452 use super::*;
453
454 #[test]
455 fn test_ties_in_java_order() {
456 let java_order = [
458 "GCGGTGTTAAGTGTCGAGCTACATCACTTCTCATGTAGCC",
459 "CAGATTTTCATATTATGCAGAAAATCTACTTCGCCTGATA",
460 "GATCCTATGCTTGTGAGTACCCAGAAAATAGCGACGGACC",
461 "CCGGGGCTAATCCGTCATTGTCAAGAGACATCTTTCGTCT",
462 "CGAGTCGGTTATCTTCGGATACTGTATAGTCCCACCTGGT",
463 "GATGTCAAACCCCGGGGGGAGCTCAGATATCCGATACAGG",
464 "AGAAAAGGTTCAGACCCCGGAGCCCAGCCGTCACGATTGT",
465 "GATGAAGAAATAACCTCATCCCATTGGTGACGAAAGGTTG",
466 "TAAGTAGCTGGCCGCCGAGATAGCTGAGCGGCGAACCACT",
467 "CATTAGGCTACTAACGCCGCCGGGTCGTTACTCGAAAAGC",
468 "AGAAGGCTGCAACTCATCGACTCTATGTAGTGACCGCGTC",
469 "TATGCGTATAAGCCCGGTTCACTACGTCCGTTCTGGCAAG",
470 ];
471 let mut data = OverRepresentedData::new();
472 data.count = 2240;
473 for seq in java_order.iter().rev() {
474 data.sequences.insert(seq.to_string(), 20);
475 }
476 for i in 0..2000 {
477 data.sequences.insert(format!("S{i}"), 1);
478 }
479
480 let shared = Arc::new(Mutex::new(data));
481 let mut module = OverRepresentedSeqs::new(&Limits::new(), 0, &[], shared);
482 module.get_overrepresented_seqs();
483 let order: Vec<&str> = module
484 .ensure_calculated()
485 .iter()
486 .map(|k| k.seq.as_str())
487 .collect();
488 assert_eq!(order, java_order);
489 }
490}