1use std::collections::HashMap;
5use std::io;
6
7use crate::config::{Limits, LimitsExt};
8use crate::modules::QCModule;
9use crate::report::charts::line_graph::{render_line_graph, LineGraphData};
10use crate::report::charts::scaled_chart_width;
11use crate::sequence::Sequence;
12use crate::utils::base_group::BaseGroup;
13
14struct Kmer {
16 sequence: String,
17 count: u64,
18 positions: Vec<u64>,
19}
20
21impl Kmer {
22 fn new(sequence: String, position: usize, seq_length: usize) -> Self {
23 let mut positions = vec![0u64; seq_length];
24 positions[position] = 1;
25 Kmer {
26 sequence,
27 count: 1,
28 positions,
29 }
30 }
31
32 fn increment_count(&mut self, position: usize) {
33 self.count += 1;
34 if position >= self.positions.len() {
36 self.positions.resize(position + 1, 0);
37 }
38 self.positions[position] += 1;
39 }
40}
41
42pub struct KmerContent {
43 kmers: HashMap<String, Kmer>,
44 longest_sequence: usize,
45 total_kmer_counts: Vec<Vec<u64>>,
48 skip_count: u64,
49 kmer_size: usize,
50 limits: Limits,
51 nogroup: bool,
52 expgroup: bool,
53 computed: Option<ComputedKmerResults>,
55}
56
57struct ComputedKmerResults {
58 enriched_kmers: Vec<EnrichedKmer>,
59 groups: Vec<BaseGroup>,
60}
61
62struct EnrichedKmer {
64 sequence: String,
65 count: u64,
68 p_value: f32,
69 max_obs_exp: f32,
70 max_position_group: String,
71 obs_exp_per_group: Vec<f32>,
74}
75
76impl KmerContent {
77 pub fn new(limits: &Limits, kmer_size: u8, nogroup: bool, expgroup: bool) -> Self {
78 let ks = kmer_size as usize;
79 KmerContent {
80 kmers: HashMap::with_capacity(4usize.pow(ks as u32)),
81 longest_sequence: 0,
82 total_kmer_counts: Vec::new(),
83 skip_count: 0,
84 kmer_size: ks,
85 limits: limits.clone(),
86 nogroup,
87 expgroup,
88 computed: None,
89 }
90 }
91
92 fn add_kmer_count(&mut self, position: usize, kmer_length: usize, kmer: &str) -> bool {
96 if position >= self.total_kmer_counts.len() {
97 let old_len = self.total_kmer_counts.len();
99 self.total_kmer_counts
100 .resize_with(position + 1, || vec![0u64; self.kmer_size]);
101 for i in old_len..self.total_kmer_counts.len() {
103 if self.total_kmer_counts[i].len() < self.kmer_size {
104 self.total_kmer_counts[i].resize(self.kmer_size, 0);
105 }
106 }
107 }
108
109 if kmer.contains('N') {
111 return true;
112 }
113
114 self.total_kmer_counts[position][kmer_length - 1] += 1;
116 false
117 }
118
119 fn calculate_enrichment(&mut self) {
121 if self.computed.is_some() {
122 return;
123 }
124
125 let group_length = if self.longest_sequence >= self.kmer_size {
127 (self.longest_sequence - self.kmer_size) + 1
128 } else {
129 0
130 };
131
132 let groups = BaseGroup::make_base_groups(group_length, self.nogroup, self.expgroup);
133
134 let mut uneven_kmers: Vec<(String, u64, f32, Vec<f32>, f32)> = Vec::new();
135
136 for kmer in self.kmers.values() {
137 let kmer_len = kmer.sequence.len();
138
139 let mut total_kmer_count: u64 = 0;
141 for pos_counts in &self.total_kmer_counts {
142 if kmer_len - 1 < pos_counts.len() {
143 total_kmer_count += pos_counts[kmer_len - 1];
144 }
145 }
146
147 if total_kmer_count == 0 {
148 continue;
149 }
150
151 let expected_proportion = kmer.count as f32 / total_kmer_count as f32;
153
154 let mut obs_exp_positions = vec![0.0f32; groups.len()];
155 let mut binomial_p_values = vec![1.0f32; groups.len()];
156
157 for (g, group) in groups.iter().enumerate() {
158 let mut total_group_count: u64 = 0;
159 let mut total_group_hits: u64 = 0;
160
161 let lower = group.lower_count; let upper = group.upper_count; for p in lower..=upper {
166 if p < self.total_kmer_counts.len()
167 && kmer_len - 1 < self.total_kmer_counts[p].len()
168 {
169 total_group_count += self.total_kmer_counts[p][kmer_len - 1];
170 }
171 if p < kmer.positions.len() {
172 total_group_hits += kmer.positions[p];
173 }
174 }
175
176 let predicted = expected_proportion * total_group_count as f32;
177 if predicted > 0.0 {
179 obs_exp_positions[g] = total_group_hits as f32 / predicted;
180 }
181
182 if total_group_hits as f32 > predicted && total_group_count > 0 {
184 let p_val = binomial_p_value(
186 total_group_count,
187 expected_proportion as f64,
188 total_group_hits,
189 );
190 binomial_p_values[g] = (p_val * 4.0f64.powi(kmer_len as i32)) as f32;
191 }
192 }
193
194 let mut lowest_p_value: f32 = 1.0;
196 for i in 0..binomial_p_values.len() {
197 if binomial_p_values[i] < 0.01
198 && obs_exp_positions[i] > 5.0
199 && binomial_p_values[i] < lowest_p_value
200 {
201 lowest_p_value = binomial_p_values[i];
202 }
203 }
204
205 if lowest_p_value < 0.01 {
206 uneven_kmers.push((
207 kmer.sequence.clone(),
208 kmer.count,
209 lowest_p_value,
210 obs_exp_positions,
211 0.0, ));
213 }
214 }
215
216 for entry in &mut uneven_kmers {
218 entry.4 = entry.3.iter().cloned().fold(0.0f32, f32::max);
219 }
220 uneven_kmers.sort_by(|a, b| b.4.partial_cmp(&a.4).unwrap_or(std::cmp::Ordering::Equal));
222
223 uneven_kmers.truncate(20);
225
226 let enriched_kmers: Vec<EnrichedKmer> = uneven_kmers
227 .iter()
228 .map(|(seq, count, p_value, obs_exp, max_oe)| {
229 let mut max_pos = 0;
231 let mut max_val = 0.0f32;
232 for (i, &v) in obs_exp.iter().enumerate() {
233 if v > max_val {
234 max_val = v;
235 max_pos = i;
236 }
237 }
238 let max_position_group = if !groups.is_empty() {
239 groups[max_pos].label()
240 } else {
241 String::new()
242 };
243 EnrichedKmer {
244 sequence: seq.clone(),
245 count: *count * 5,
247 p_value: *p_value,
248 max_obs_exp: *max_oe,
249 max_position_group,
250 obs_exp_per_group: obs_exp.clone(),
251 }
252 })
253 .collect();
254
255 self.computed = Some(ComputedKmerResults {
256 enriched_kmers,
257 groups,
258 });
259 }
260
261 fn ensure_calculated(&self) -> &ComputedKmerResults {
262 static DEFAULT: ComputedKmerResults = ComputedKmerResults {
263 enriched_kmers: Vec::new(),
264 groups: Vec::new(),
265 };
266 self.computed.as_ref().unwrap_or(&DEFAULT)
267 }
268}
269
270fn binomial_p_value(n: u64, p: f64, k: u64) -> f64 {
273 use statrs::distribution::Binomial;
274 use statrs::distribution::DiscreteCDF;
275
276 if n == 0 || p <= 0.0 || p >= 1.0 {
277 return 1.0;
278 }
279
280 match Binomial::new(p, n) {
281 Ok(binom) => {
282 1.0 - binom.cdf(k)
284 }
285 Err(_) => 1.0,
286 }
287}
288
289impl KmerContent {
290 fn build_chart_svg(&self) -> Option<String> {
296 let computed = self.computed.as_ref()?;
297
298 if computed.enriched_kmers.is_empty() {
299 return None;
300 }
301
302 let num_series = computed.enriched_kmers.len().min(6);
304
305 let x_categories: Vec<String> = computed.groups.iter().map(|g| g.label()).collect();
306
307 let mut data: Vec<Vec<f64>> = Vec::with_capacity(num_series);
308 let mut series_names: Vec<String> = Vec::with_capacity(num_series);
309 let mut max_y: f64 = 0.0;
310
311 for k in 0..num_series {
312 let kmer = &computed.enriched_kmers[k];
313 let values: Vec<f64> = kmer.obs_exp_per_group.iter().map(|&v| v as f64).collect();
314 for &v in &values {
315 if v > max_y {
316 max_y = v;
317 }
318 }
319 data.push(values);
320 series_names.push(kmer.sequence.clone());
321 }
322
323 let min_y = 0.0;
325 if max_y < 1.0 {
327 max_y = 1.0;
328 }
329
330 Some(render_line_graph(&LineGraphData {
331 width: scaled_chart_width(x_categories.len()),
332 data,
333 min_y,
334 max_y,
335 x_label: "Position in read (bp)".to_string(),
336 series_names,
337 x_categories,
338 title: "Log2 Obs/Exp".to_string(),
340 }))
341 }
342}
343
344impl QCModule for KmerContent {
345 fn cost_hint(&self) -> u32 {
346 12
349 }
350
351 fn process_sequence(&mut self, sequence: &Sequence) {
352 self.computed = None;
353
354 self.skip_count += 1;
356 if !self.skip_count.is_multiple_of(50) {
357 return;
358 }
359
360 let seq_str = std::str::from_utf8(&sequence.sequence).unwrap_or("");
362 let seq = if seq_str.len() > 500 {
363 &seq_str[..500]
364 } else {
365 seq_str
366 };
367
368 if seq.len() > self.longest_sequence {
369 self.longest_sequence = seq.len();
370 }
371
372 let kmer_size = self.kmer_size;
373
374 if seq.len() >= kmer_size {
376 for i in 0..=(seq.len() - kmer_size) {
377 let kmer = &seq[i..i + kmer_size];
378
379 if self.add_kmer_count(i, kmer_size, kmer) {
383 continue;
384 }
385
386 if let Some(existing) = self.kmers.get_mut(kmer) {
387 existing.increment_count(i);
388 } else {
389 let seq_kmer_length = (seq.len() - kmer_size) + 1;
390 self.kmers.insert(
391 kmer.to_string(),
392 Kmer::new(kmer.to_string(), i, seq_kmer_length),
393 );
394 }
395 }
396 }
397 }
398
399 fn finalize(&mut self) {
400 self.calculate_enrichment();
401 }
402
403 fn name(&self) -> &str {
404 "Kmer Content"
405 }
406
407 fn description(&self) -> &str {
408 "Identifies short sequences which have uneven representation"
409 }
410
411 fn reset(&mut self) {
412 self.kmers.clear();
413 self.total_kmer_counts.clear();
414 self.longest_sequence = 0;
415 self.skip_count = 0;
416 self.computed = None;
417 }
418
419 fn raises_error(&self) -> bool {
420 let threshold = self.limits.threshold("kmer\terror", 5.0);
421 let computed = self.ensure_calculated();
422 computed
424 .enriched_kmers
425 .first()
426 .is_some_and(|k| -(k.p_value as f64).log10() > threshold)
427 }
428
429 fn raises_warning(&self) -> bool {
430 let threshold = self.limits.threshold("kmer\twarn", 2.0);
431 let computed = self.ensure_calculated();
432 computed
434 .enriched_kmers
435 .first()
436 .is_some_and(|k| -(k.p_value as f64).log10() > threshold)
437 }
438
439 fn ignore_filtered_sequences(&self) -> bool {
440 true
441 }
442
443 fn ignore_in_report(&self) -> bool {
444 self.limits.threshold("kmer\tignore", 1.0) > 0.0
446 }
447
448 fn chart_image_name(&self) -> Option<&str> {
450 Some("kmer_profiles")
451 }
452 fn chart_alt_text(&self) -> Option<&str> {
453 Some("Kmer graph")
454 }
455 fn generate_chart_svg(&self) -> Option<String> {
456 self.build_chart_svg()
457 }
458
459 fn write_text_report(&self, writer: &mut dyn io::Write) -> io::Result<()> {
460 let computed = self.ensure_calculated();
461
462 writeln!(
464 writer,
465 "#Sequence\tCount\tPValue\tObs/Exp Max\tMax Obs/Exp Position"
466 )?;
467
468 for kmer in &computed.enriched_kmers {
469 writeln!(
470 writer,
471 "{}\t{}\t{}\t{}\t{}",
472 kmer.sequence, kmer.count, kmer.p_value, kmer.max_obs_exp, kmer.max_position_group
473 )?;
474 }
475
476 Ok(())
477 }
478}