1use std::collections::HashMap;
5use std::io;
6use std::sync::{Arc, Mutex};
7
8use crate::config::{Limits, LimitsExt};
9use crate::modules::overrepresented_seqs::OverRepresentedData;
10use crate::modules::QCModule;
11use crate::report::charts::line_graph::{render_line_graph, LineGraphData};
12use crate::report::charts::CHART_WIDTH;
13use crate::sequence::Sequence;
14use crate::utils::format::java_format_double;
15use crate::utils::java_hashmap;
16
17pub struct DuplicationLevel {
18 shared_data: Arc<Mutex<OverRepresentedData>>,
19 limits: Limits,
20 computed: Option<ComputedLevels>,
22}
23
24struct ComputedLevels {
25 total_percentages: [f64; 16],
26 percent_different_seqs: f64,
27}
28
29const LABELS: [&str; 16] = [
31 "1", "2", "3", "4", "5", "6", "7", "8", "9", ">10", ">50", ">100", ">500", ">1k", ">5k", ">10k",
32];
33
34impl DuplicationLevel {
35 pub fn new(shared_data: Arc<Mutex<OverRepresentedData>>, limits: &Limits) -> Self {
36 DuplicationLevel {
37 shared_data,
38 limits: limits.clone(),
39 computed: None,
40 }
41 }
42
43 pub fn calculate_levels(&mut self) {
46 if self.computed.is_some() {
47 return;
48 }
49
50 let data = self.shared_data.lock().unwrap_or_else(|e| e.into_inner());
51
52 let mut total_percentages = [0.0f64; 16];
53
54 let mut collated_counts: HashMap<u64, u64> = HashMap::new();
56 for &count in data.sequences.values() {
57 *collated_counts.entry(count).or_insert(0) += 1;
58 }
59
60 let mut corrected_counts: Vec<(u64, f64)> = collated_counts
62 .into_iter()
63 .map(|(dup_level, num_observations)| {
64 let corrected = get_corrected_count(
65 data.count_at_unique_limit,
66 data.count,
67 dup_level,
68 num_observations,
69 );
70 (dup_level, corrected)
71 })
72 .collect();
73 sort_in_java_hashmap_order(&mut corrected_counts);
74
75 let mut dedup_total: f64 = 0.0;
77 let mut raw_total: f64 = 0.0;
78
79 for &(dup_level, count) in &corrected_counts {
80 dedup_total += count;
81 raw_total += count * dup_level as f64;
82
83 let temp_dup_slot = dup_level as i64 - 1;
85
86 let dup_slot: usize = if !(0..=9999).contains(&temp_dup_slot) {
88 15
89 } else if temp_dup_slot > 4999 {
90 14
91 } else if temp_dup_slot > 999 {
92 13
93 } else if temp_dup_slot > 499 {
94 12
95 } else if temp_dup_slot > 99 {
96 11
97 } else if temp_dup_slot > 49 {
98 10
99 } else if temp_dup_slot > 9 {
100 9
101 } else {
102 temp_dup_slot as usize
103 };
104
105 total_percentages[dup_slot] += count * dup_level as f64;
106 }
107
108 for tp in &mut total_percentages {
110 *tp = *tp / raw_total * 100.0;
111 }
112
113 let percent_different_seqs = if raw_total == 0.0 {
115 100.0
116 } else {
117 (dedup_total / raw_total) * 100.0
118 };
119
120 self.computed = Some(ComputedLevels {
121 total_percentages,
122 percent_different_seqs,
123 });
124 }
125
126 fn ensure_calculated(&self) -> &ComputedLevels {
127 static DEFAULT: ComputedLevels = ComputedLevels {
130 total_percentages: [0.0; 16],
131 percent_different_seqs: 100.0,
132 };
133 self.computed.as_ref().unwrap_or(&DEFAULT)
134 }
135}
136
137fn sort_in_java_hashmap_order(entries: &mut [(u64, f64)]) {
140 let capacity = java_hashmap::table_capacity(entries.len());
141 entries.sort_unstable_by_key(|&(key, _)| {
142 (
143 java_hashmap::bucket(java_hashmap::long_hash(key), capacity),
144 key,
145 )
146 });
147}
148
149fn get_corrected_count(
153 count_at_limit: u64,
154 total_count: u64,
155 duplication_level: u64,
156 number_of_observations: u64,
157) -> f64 {
158 if count_at_limit == total_count {
160 return number_of_observations as f64;
161 }
162
163 if total_count - number_of_observations < count_at_limit {
165 return number_of_observations as f64;
166 }
167
168 let mut p_not_seeing_at_limit: f64 = 1.0;
171
172 let limit_of_caring =
174 1.0 - (number_of_observations as f64 / (number_of_observations as f64 + 0.01));
175
176 for i in 0..count_at_limit {
177 p_not_seeing_at_limit *=
178 ((total_count - i) - duplication_level) as f64 / (total_count - i) as f64;
179
180 if p_not_seeing_at_limit < limit_of_caring {
181 p_not_seeing_at_limit = 0.0;
182 break;
183 }
184 }
185
186 let p_seeing_at_limit = 1.0 - p_not_seeing_at_limit;
188 number_of_observations as f64 / p_seeing_at_limit
189}
190
191impl DuplicationLevel {
192 fn build_chart_svg(&self) -> String {
193 let computed = self.ensure_calculated();
194 let max_count = 100.0_f64;
196
197 let labels: Vec<String> = LABELS.iter().map(|&l| l.to_string()).collect();
198
199 let pct_str = format!("{:.2}", computed.percent_different_seqs);
201 let pct_str = pct_str.trim_end_matches('0').trim_end_matches('.');
202 let title = format!("Percent of seqs remaining if deduplicated {}%", pct_str);
203
204 render_line_graph(&LineGraphData {
205 width: CHART_WIDTH,
206 data: vec![computed.total_percentages.to_vec()],
207 min_y: 0.0,
208 max_y: max_count,
209 x_label: "Sequence Duplication Level".to_string(),
210 series_names: vec!["% Total sequences".to_string()],
211 x_categories: labels,
212 title,
213 })
214 }
215}
216
217impl QCModule for DuplicationLevel {
218 fn cost_hint(&self) -> u32 {
219 1
221 }
222
223 fn process_sequence(&mut self, _sequence: &Sequence) {
224 }
238
239 fn finalize(&mut self) {
240 self.calculate_levels();
241 }
242
243 fn name(&self) -> &str {
244 "Sequence Duplication Levels"
245 }
246
247 fn description(&self) -> &str {
248 "Plots the number of sequences which are duplicated to different levels"
249 }
250
251 fn reset(&mut self) {
252 self.computed = None;
253 }
254
255 fn raises_error(&self) -> bool {
256 let threshold = self.limits.threshold("duplication\terror", 50.0);
257 let computed = self.ensure_calculated();
258 computed.percent_different_seqs < threshold
260 }
261
262 fn raises_warning(&self) -> bool {
263 let threshold = self.limits.threshold("duplication\twarn", 70.0);
264 let computed = self.ensure_calculated();
265 computed.percent_different_seqs < threshold
267 }
268
269 fn ignore_filtered_sequences(&self) -> bool {
270 self.limits.is_ignored("duplication")
272 }
273
274 fn ignore_in_report(&self) -> bool {
275 self.limits.is_ignored("duplication")
276 }
277
278 fn write_text_report(&self, writer: &mut dyn io::Write) -> io::Result<()> {
279 let computed = self.ensure_calculated();
280
281 writeln!(
283 writer,
284 "#Total Deduplicated Percentage\t{}",
285 java_format_double(computed.percent_different_seqs)
286 )?;
287 writeln!(writer, "#Duplication Level\tPercentage of total")?;
289
290 for (i, (label_str, tp)) in LABELS
292 .iter()
293 .zip(computed.total_percentages.iter())
294 .enumerate()
295 {
296 let label = if i == 15 {
297 format!("{}+", label_str)
298 } else {
299 label_str.to_string()
300 };
301 writeln!(writer, "{}\t{}", label, java_format_double(*tp))?;
302 }
303
304 Ok(())
305 }
306
307 fn chart_image_name(&self) -> Option<&str> {
309 Some("duplication_levels")
310 }
311 fn chart_alt_text(&self) -> Option<&str> {
312 Some("Duplication level graph")
313 }
314 fn generate_chart_svg(&self) -> Option<String> {
315 Some(self.build_chart_svg())
316 }
317}
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322
323 fn java_order(keys: impl IntoIterator<Item = u64>) -> Vec<u64> {
324 let mut entries: Vec<(u64, f64)> = keys.into_iter().map(|k| (k, 0.0)).collect();
325 sort_in_java_hashmap_order(&mut entries);
326 entries.into_iter().map(|(k, _)| k).collect()
327 }
328
329 #[test]
330 fn test_java_hashmap_order() {
331 assert_eq!(
333 java_order([32, 3, 17, 1, 2, 4, 5, 6, 7, 8, 9, 10]),
334 [32, 1, 17, 2, 3, 4, 5, 6, 7, 8, 9, 10]
335 );
336 assert_eq!(
338 java_order((1..=12).chain([40])),
339 [1, 2, 3, 4, 5, 6, 7, 8, 40, 9, 10, 11, 12]
340 );
341 }
342
343 #[test]
344 fn test_levels_match_java_past_unique_limit() {
345 let mut data = OverRepresentedData::new();
347 data.count = 2_000_000;
348 data.count_at_unique_limit = 150_000;
349 let levels = [(1, 90_000), (17, 200), (32, 100)]
350 .into_iter()
351 .chain((2..=12).map(|level| (level, 1_000)));
352 for (level, n) in levels {
353 for _ in 0..n {
354 let key = format!("S{}", data.sequences.len());
355 data.sequences.insert(key, level);
356 }
357 }
358
359 let mut module = DuplicationLevel::new(Arc::new(Mutex::new(data)), &Limits::new());
360 module.calculate_levels();
361 let computed = module.ensure_calculated();
362
363 assert_eq!(computed.percent_different_seqs, 88.61009166696547);
364 assert_eq!(
365 computed.total_percentages[..10],
366 [
367 86.21369673475012,
368 0.9952516712377281,
369 1.033504541142911,
370 1.072685751499248,
371 1.1127913683889874,
372 1.153816362824301,
373 1.1957546322473545,
374 1.238599026439594,
375 1.2823413776257666,
376 4.701558533844009,
377 ]
378 );
379 assert!(computed.total_percentages[10..].iter().all(|&p| p == 0.0));
380 }
381}