1use std::io;
5
6use aho_corasick::{packed, Span};
7use memchr::memmem;
8
9use crate::config::{Limits, LimitsExt};
10use crate::modules::QCModule;
11use crate::report::charts::line_graph::{render_line_graph, LineGraphData};
12use crate::report::charts::scaled_chart_width;
13use crate::sequence::Sequence;
14use crate::utils::base_group::BaseGroup;
15use crate::utils::format::java_format_double;
16
17struct Adapter {
19 name: String,
20 first_hits: Vec<u64>,
23}
24
25impl Adapter {
26 fn new(name: &str) -> Self {
27 Adapter {
28 name: name.to_string(),
29 first_hits: vec![0; 1],
30 }
31 }
32
33 fn record_hit(&mut self, position: usize) {
34 if let Some(count) = self.first_hits.get_mut(position) {
37 *count += 1;
38 }
39 }
40
41 fn cumulative_positions(&self) -> Vec<u64> {
43 self.first_hits
44 .iter()
45 .scan(0u64, |total, &hits| {
46 *total += hits;
47 Some(*total)
48 })
49 .collect()
50 }
51}
52
53struct AdapterSearch {
56 finders: Vec<memmem::Finder<'static>>,
57 teddy: Option<packed::Searcher>,
58}
59
60impl AdapterSearch {
61 fn new(adapters: &[(String, String)]) -> Self {
62 let seqs: Vec<&[u8]> = adapters.iter().map(|(_, s)| s.as_bytes()).collect();
63 AdapterSearch {
64 finders: seqs
65 .iter()
66 .map(|s| memmem::Finder::new(s).into_owned())
67 .collect(),
68 teddy: Self::build_teddy(&seqs),
69 }
70 }
71
72 fn build_teddy(seqs: &[&[u8]]) -> Option<packed::Searcher> {
75 if seqs.is_empty() || seqs.len() > 64 || seqs.iter().any(|s| s.is_empty()) {
76 return None;
77 }
78 for (i, a) in seqs.iter().enumerate() {
79 for (j, b) in seqs.iter().enumerate() {
80 if i != j && b.starts_with(a) {
81 return None;
82 }
83 }
84 }
85 packed::Config::new()
86 .match_kind(packed::MatchKind::LeftmostFirst)
87 .builder()
88 .extend(seqs)
89 .build()
90 }
91
92 fn first_matches(&self, seq: &[u8], mut on_hit: impl FnMut(usize, usize)) {
95 let Some(teddy) = &self.teddy else {
96 for (a, finder) in self.finders.iter().enumerate() {
97 if let Some(start) = finder.find(seq) {
98 on_hit(a, start);
99 }
100 }
101 return;
102 };
103
104 let mut remaining: u64 = u64::MAX >> (64 - self.finders.len());
105 let mut at = 0;
106 while remaining != 0 && at < seq.len() {
107 let Some(m) = teddy.find_in(seq, Span::from(at..seq.len())) else {
108 return;
109 };
110 let a = m.pattern().as_usize();
111 if remaining & (1 << a) == 0 {
112 break;
116 }
117 remaining &= !(1 << a);
118 on_hit(a, m.start());
119 at = m.start() + 1;
122 }
123 let rest = &seq[at.min(seq.len())..];
124 while remaining != 0 {
125 let a = remaining.trailing_zeros() as usize;
126 remaining &= remaining - 1;
127 if let Some(start) = self.finders[a].find(rest) {
128 on_hit(a, at + start);
129 }
130 }
131 }
132}
133
134pub struct AdapterContent {
135 adapters: Vec<Adapter>,
136 search: AdapterSearch,
137 longest_sequence: usize,
138 longest_adapter: usize,
139 total_count: u64,
140 limits: Limits,
141 nogroup: bool,
142 expgroup: bool,
143 computed: Option<ComputedEnrichment>,
145}
146
147struct ComputedEnrichment {
148 enrichments: Vec<Vec<f64>>,
149 x_labels: Vec<String>,
150}
151
152impl AdapterContent {
153 pub fn new(
154 limits: &Limits,
155 adapter_entries: &[(String, String)],
156 nogroup: bool,
157 expgroup: bool,
158 ) -> Self {
159 let mut longest_adapter = 0;
160 let mut adapters = Vec::with_capacity(adapter_entries.len());
161
162 for (name, seq) in adapter_entries {
163 if seq.len() > longest_adapter {
164 longest_adapter = seq.len();
165 }
166 adapters.push(Adapter::new(name));
167 }
168
169 if adapter_entries
170 .iter()
171 .any(|(_, seq)| seq.len() != longest_adapter)
172 {
173 static WARNED: crate::progress::OncePerRun = crate::progress::OncePerRun::new();
174 WARNED.log(|| "[Warning] You are using adapter sequences with different lengths. Matches will only be reported up to the position where the longest adapter could match. Matches to shorter adapters at the end of sequences will not be recorded.".to_string());
175 }
176
177 AdapterContent {
178 adapters,
179 search: AdapterSearch::new(adapter_entries),
180 longest_sequence: 0,
181 longest_adapter,
182 total_count: 0,
183 limits: limits.clone(),
184 nogroup,
185 expgroup,
186 computed: None,
187 }
188 }
189
190 fn calculate_enrichment(&mut self) {
192 if self.computed.is_some() {
193 return;
194 }
195
196 let all_positions: Vec<Vec<u64>> = self
197 .adapters
198 .iter()
199 .map(Adapter::cumulative_positions)
200 .collect();
201 let max_length = all_positions.iter().map(Vec::len).max().unwrap_or(0);
202
203 let groups = BaseGroup::make_base_groups(max_length, self.nogroup, self.expgroup);
205
206 let x_labels: Vec<String> = groups.iter().map(|g| g.label()).collect();
207
208 let mut enrichments = vec![vec![0.0f64; groups.len()]; self.adapters.len()];
209
210 for (a, positions) in all_positions.iter().enumerate() {
211 for (g, group) in groups.iter().enumerate() {
212 let lower = group.lower_count; let upper = group.upper_count; for p in lower..=upper {
218 if p < positions.len() {
219 enrichments[a][g] +=
220 (positions[p] as f64 * 100.0) / self.total_count as f64;
221 }
222 }
223
224 enrichments[a][g] /= (upper - lower + 1) as f64;
226 }
227 }
228
229 self.computed = Some(ComputedEnrichment {
230 enrichments,
231 x_labels,
232 });
233 }
234
235 fn reads_too_short(&self) -> bool {
238 self.longest_adapter > self.longest_sequence
239 }
240
241 fn adapter_names(&self) -> Vec<String> {
243 self.adapters.iter().map(|a| a.name.clone()).collect()
244 }
245
246 fn ensure_calculated(&self) -> &ComputedEnrichment {
247 static DEFAULT: ComputedEnrichment = ComputedEnrichment {
248 enrichments: Vec::new(),
249 x_labels: Vec::new(),
250 };
251 self.computed.as_ref().unwrap_or(&DEFAULT)
252 }
253}
254
255impl AdapterContent {
256 fn build_chart_svg(&self) -> String {
257 let computed = self.ensure_calculated();
258
259 render_line_graph(&LineGraphData {
261 width: scaled_chart_width(computed.x_labels.len()),
262 data: computed.enrichments.clone(),
263 min_y: 0.0,
264 max_y: 100.0,
265 x_label: "Position in read (bp)".to_string(),
266 series_names: self.adapter_names(),
267 x_categories: computed.x_labels.clone(),
268 title: "% Adapter".to_string(),
269 })
270 }
271}
272
273impl QCModule for AdapterContent {
274 fn cost_hint(&self) -> u32 {
275 9
276 }
277
278 fn process_sequence(&mut self, sequence: &Sequence) {
279 self.computed = None;
280 self.total_count += 1;
281
282 let seq_len = sequence.sequence.len();
283
284 if seq_len > self.longest_sequence && seq_len > self.longest_adapter {
286 self.longest_sequence = seq_len;
287 let new_len = (self.longest_sequence - self.longest_adapter) + 1;
288 for adapter in &mut self.adapters {
289 adapter.first_hits.resize(new_len, 0);
290 }
291 }
292
293 let adapters = &mut self.adapters;
294 self.search
295 .first_matches(&sequence.sequence, |a, start| adapters[a].record_hit(start));
296 }
297
298 fn finalize(&mut self) {
299 self.calculate_enrichment();
300 }
301
302 fn name(&self) -> &str {
303 "Adapter Content"
304 }
305
306 fn description(&self) -> &str {
307 "Searches for specific adapter sequences in a library"
308 }
309
310 fn reset(&mut self) {
311 self.total_count = 0;
312 self.longest_sequence = 0;
313 self.computed = None;
314 for adapter in &mut self.adapters {
315 adapter.first_hits = vec![0; 1];
316 }
317 }
318
319 fn raises_error(&self) -> bool {
320 let threshold = self.limits.threshold("adapter\terror", 10.0);
321 let computed = self.ensure_calculated();
322 computed
323 .enrichments
324 .iter()
325 .any(|enrichments| enrichments.iter().any(|&val| val > threshold))
326 }
327
328 fn raises_warning(&self) -> bool {
329 if self.reads_too_short() {
330 return true;
331 }
332
333 let threshold = self.limits.threshold("adapter\twarn", 5.0);
334 let computed = self.ensure_calculated();
335 computed
336 .enrichments
337 .iter()
338 .any(|enrichments| enrichments.iter().any(|&val| val > threshold))
339 }
340
341 fn ignore_filtered_sequences(&self) -> bool {
342 true
343 }
344
345 fn ignore_in_report(&self) -> bool {
346 self.limits.is_ignored("adapter")
347 }
348
349 fn write_html_report(&self, writer: &mut dyn io::Write, png: bool) -> io::Result<()> {
350 if self.reads_too_short() {
351 return write!(
352 writer,
353 "<p>Can't analyse adapters as read length is too short ({} vs {})</p>",
354 self.longest_adapter, self.longest_sequence
355 );
356 }
357 crate::report::html::write_chart(self, "Adapter graph", png, writer)
358 }
359
360 fn write_text_report(&self, writer: &mut dyn io::Write) -> io::Result<()> {
361 let computed = self.ensure_calculated();
362
363 if self.reads_too_short() {
364 return Ok(());
365 }
366
367 write!(writer, "#Position")?;
369 for adapter in &self.adapters {
370 write!(writer, "\t{}", adapter.name)?;
371 }
372 writeln!(writer)?;
373
374 for (row, x_label) in computed.x_labels.iter().enumerate() {
376 write!(writer, "{}", x_label)?;
377 for a in 0..self.adapters.len() {
378 write!(
379 writer,
380 "\t{}",
381 java_format_double(computed.enrichments[a][row])
382 )?;
383 }
384 writeln!(writer)?;
385 }
386
387 Ok(())
388 }
389
390 fn chart_image_name(&self) -> Option<&str> {
392 Some("adapter_content")
393 }
394 fn chart_alt_text(&self) -> Option<&str> {
395 Some("Adapter graph")
396 }
397 fn generate_chart_svg(&self) -> Option<String> {
398 if self.reads_too_short() {
399 return None;
400 }
401 Some(self.build_chart_svg())
402 }
403}
404
405#[cfg(test)]
406mod tests {
407 use super::*;
408
409 const DEFAULT_ADAPTERS: [&str; 6] = [
410 "AGATCGGAAGAG",
411 "TGGAATTCTCGG",
412 "GATCGTCGGACT",
413 "CTGTCTCTTATA",
414 "AAAAAAAAAAAA",
415 "GGGGGGGGGGGG",
416 ];
417
418 fn entries(seqs: &[&str]) -> Vec<(String, String)> {
419 seqs.iter()
420 .map(|s| (s.to_string(), s.to_string()))
421 .collect()
422 }
423
424 fn memmem_first(adapters: &[(String, String)], seq: &[u8]) -> Vec<Option<usize>> {
425 adapters
426 .iter()
427 .map(|(_, a)| memmem::find(seq, a.as_bytes()))
428 .collect()
429 }
430
431 fn search_first(search: &AdapterSearch, seq: &[u8]) -> Vec<Option<usize>> {
432 let mut found = vec![None; search.finders.len()];
433 search.first_matches(seq, |a, start| {
434 assert!(found[a].is_none(), "adapter {a} reported twice");
435 found[a] = Some(start);
436 });
437 found
438 }
439
440 #[test]
443 fn test_adapter_search_matches_memmem() {
444 let adapters = entries(&DEFAULT_ADAPTERS);
445 let search = AdapterSearch::new(&adapters);
446 assert!(search.teddy.is_some(), "default adapters use Teddy");
447
448 let poly_g = vec![b'G'; 150];
449 let mut poly_a_tail = b"ACGT".repeat(500);
450 poly_a_tail.extend_from_slice(&[b'A'; 5000]);
451 poly_a_tail.extend_from_slice(b"AGATCGGAAGAG");
452 let mut seqs: Vec<Vec<u8>> = vec![
453 b"".to_vec(),
454 b"AGATCGGAAGA".to_vec(),
455 b"NNNAGATCGGAAGAGNNN".to_vec(),
456 b"CCAGATCGGAAGAGATCGGAAGAGTT".to_vec(),
458 b"CTGTCTCTTATAAAAAAAAAAA".to_vec(),
461 b"GGGGGGGGGGGGGGGGGGGGAGATCGGAAGAGGGGGGGGGGGGGGGAAAAAAAAAAAAAA".to_vec(),
463 b"AAAAAAAAAAAAAAAAAAAAAAAAACTGTCTCTTATAAAAAAAAAAAAAAAATGGAATTCTCGG".to_vec(),
464 poly_g,
465 poly_a_tail,
466 b"GGGGGGGGGGGGAAAAAAAAAAAACTGTCTCTTATAGATCGTCGGACTTGGAATTCTCGGAGATCGGAAGAG".to_vec(),
467 ];
468 let mut state = 0x2545_f491_4f6c_dd1du64;
471 let mut next = || {
472 state ^= state << 13;
473 state ^= state >> 7;
474 state ^= state << 17;
475 state
476 };
477 for _ in 0..5000 {
478 let len = (next() % 300) as usize;
479 let mut seq = Vec::with_capacity(len + 40);
480 while seq.len() < len {
481 match next() % 16 {
482 0 | 1 => seq.extend_from_slice(adapters[(next() % 6) as usize].1.as_bytes()),
483 2 => {
484 let run = (next() % 40) as usize;
485 seq.extend(std::iter::repeat_n(b"AG"[(next() % 2) as usize], run));
486 }
487 _ => seq.push(b"ACGTN"[(next() % 5) as usize]),
488 }
489 }
490 seqs.push(seq);
491 }
492
493 for seq in &seqs {
494 assert_eq!(
495 search_first(&search, seq),
496 memmem_first(&adapters, seq),
497 "{}",
498 String::from_utf8_lossy(seq)
499 );
500 }
501 }
502
503 #[test]
506 fn test_adapter_search_prefix_fallback() {
507 let prefix = entries(&["ACGTACGT", "ACGTACGTAA"]);
508 let search = AdapterSearch::new(&prefix);
509 assert!(search.teddy.is_none());
510 assert_eq!(
511 search_first(&search, b"TTACGTACGTAA"),
512 vec![Some(2), Some(2)]
513 );
514
515 assert!(AdapterSearch::new(&entries(&["ACGTACGT", "ACGTACGT"]))
516 .teddy
517 .is_none());
518 assert!(AdapterSearch::new(&entries(&["ACGTACGT", "CGTACGTA"]))
519 .teddy
520 .is_some());
521 }
522
523 #[test]
526 fn test_cumulative_positions() {
527 let mut adapter = Adapter::new("x");
528 adapter.first_hits = vec![0; 5];
529 for p in [0, 2, 2, 4, 9] {
530 adapter.record_hit(p);
531 }
532 assert_eq!(adapter.cumulative_positions(), vec![1, 1, 3, 3, 4]);
533 }
534}