fastqc_rust/modules/
per_tile_quality.rs1use std::collections::HashMap;
5use std::io;
6
7use crate::config::{Limits, LimitsExt};
8use crate::modules::QCModule;
9use crate::report::charts::tile_graph::{render_tile_graph, TileGraphData};
10use crate::sequence::Sequence;
11use crate::utils::base_group::BaseGroup;
12use crate::utils::format::java_format_double;
13use crate::utils::quality_count::QualityCount;
14use crate::utils::{phred, quality_count};
15
16pub struct PerTileQualityScores {
17 per_tile_quality_counts: HashMap<i32, Vec<QualityCount>>,
18 current_length: usize,
19 total_count: u64,
20 split_position: i32,
23 ignore_in_report: bool,
24 nogroup: bool,
25 expgroup: bool,
26 known_encoding: Option<phred::PhredEncoding>,
28 limits: Limits,
29}
30
31impl PerTileQualityScores {
32 pub fn new(limits: &Limits, nogroup: bool, expgroup: bool) -> Self {
33 PerTileQualityScores {
34 per_tile_quality_counts: HashMap::new(),
35 current_length: 0,
36 total_count: 0,
37 split_position: -1,
38 ignore_in_report: false,
39 nogroup,
40 expgroup,
41 known_encoding: None,
42 limits: limits.clone(),
43 }
44 }
45
46 fn calculate(&self) -> Option<TileCalculatedData> {
47 if self.per_tile_quality_counts.is_empty() {
48 return None;
49 }
50
51 let all_counts: Vec<&QualityCount> = self
53 .per_tile_quality_counts
54 .values()
55 .flat_map(|v| v.iter())
56 .collect();
57 let (min_char, _max_char) = quality_count::calculate_offsets(all_counts);
58 let offset = phred::resolve(self.known_encoding, min_char as u16)
60 .map(|e| e.offset)
61 .unwrap_or(phred::PhredEncoding::SANGER.offset);
62
63 let groups = BaseGroup::make_base_groups(self.current_length, self.nogroup, self.expgroup);
64
65 let mut tile_numbers: Vec<i32> = self.per_tile_quality_counts.keys().copied().collect();
66 tile_numbers.sort();
67
68 let mut means = vec![vec![0.0f64; groups.len()]; tile_numbers.len()];
69 let mut x_labels = Vec::with_capacity(groups.len());
70
71 for (t, &tile) in tile_numbers.iter().enumerate() {
72 for (i, group) in groups.iter().enumerate() {
73 if t == 0 {
74 x_labels.push(group.label());
75 }
76 let min_base = group.lower_count;
77 let max_base = group.upper_count;
78 means[t][i] = self.get_mean(tile, min_base, max_base, offset);
79 }
80 }
81
82 let mut average_qualities_per_group = vec![0.0f64; groups.len()];
84 for tile_means in means.iter().take(tile_numbers.len()) {
85 for (avg, &m) in average_qualities_per_group
86 .iter_mut()
87 .zip(tile_means.iter())
88 {
89 *avg += m;
90 }
91 }
92 for avg in &mut average_qualities_per_group {
93 *avg /= tile_numbers.len() as f64;
94 }
95
96 let mut max_deviation: f64 = 0.0;
97 for (i, &avg) in average_qualities_per_group.iter().enumerate() {
99 for tile_means in means.iter_mut().take(tile_numbers.len()) {
100 tile_means[i] -= avg;
101 if tile_means[i].abs() > max_deviation {
102 max_deviation = tile_means[i].abs();
103 }
104 }
105 }
106
107 Some(TileCalculatedData {
108 tiles: tile_numbers,
109 means,
110 x_labels,
111 max_deviation,
112 })
113 }
114
115 fn get_mean(&self, tile: i32, min_base: usize, max_base: usize, offset: u8) -> f64 {
117 let quality_counts = match self.per_tile_quality_counts.get(&tile) {
118 Some(qc) => qc,
119 None => return 0.0,
120 };
121
122 let mut count = 0;
123 let mut total = 0.0;
124
125 for qc in &quality_counts[min_base..=max_base] {
126 if qc.get_total_count() > 0 {
127 count += 1;
128 total += qc.get_mean(offset);
129 }
130 }
131
132 if count > 0 {
133 total / count as f64
134 } else {
135 0.0
136 }
137 }
138}
139
140impl PerTileQualityScores {
141 fn build_chart_svg(&self) -> Option<String> {
142 let data = self.calculate()?;
143
144 let color_scale_max = self.limits.threshold("tile\terror", 5.0);
146
147 Some(render_tile_graph(&TileGraphData {
148 x_labels: data.x_labels,
149 tiles: data.tiles,
150 tile_base_means: data.means,
151 color_scale_max,
152 }))
153 }
154}
155
156impl QCModule for PerTileQualityScores {
157 fn cost_hint(&self) -> u32 {
158 1
159 }
160
161 fn process_sequence(&mut self, sequence: &Sequence) {
162 if self.total_count == 0 && self.limits.is_ignored("tile") {
164 self.ignore_in_report = true;
165 }
166
167 if self.ignore_in_report {
168 return;
169 }
170
171 if sequence.quality.is_empty() {
173 return;
174 }
175
176 self.total_count += 1;
177
178 if self.total_count > 10000 && !self.total_count.is_multiple_of(10) {
180 return;
181 }
182
183 if self.split_position < 0 {
186 let field_count = sequence.id.split(':').count();
187 if field_count >= 7 {
188 self.split_position = 4;
190 } else if field_count >= 5 {
191 self.split_position = 2;
193 } else {
194 self.ignore_in_report = true;
196 return;
197 }
198 }
199
200 let tile = match sequence
201 .id
202 .split(':')
203 .nth(self.split_position as usize)
204 .and_then(|f| f.parse::<i32>().ok())
205 {
206 Some(t) => t,
207 None => {
208 self.ignore_in_report = true;
209 return;
210 }
211 };
212
213 let qual = &sequence.quality;
214
215 if self.current_length < qual.len() {
217 for qc_vec in self.per_tile_quality_counts.values_mut() {
218 qc_vec.resize_with(qual.len(), QualityCount::new);
219 }
220 self.current_length = qual.len();
221 }
222
223 if !self.per_tile_quality_counts.contains_key(&tile) {
225 if self.per_tile_quality_counts.len() > 2500 {
226 crate::progress::log_line("Too many tiles (>2500) so giving up trying to do per-tile qualities since we're probably parsing the file wrongly");
228 self.ignore_in_report = true;
229 self.per_tile_quality_counts.clear();
230 return;
231 }
232
233 let mut quality_counts = Vec::new();
234 quality_counts.resize_with(self.current_length, QualityCount::new);
235 self.per_tile_quality_counts.insert(tile, quality_counts);
236 }
237
238 let quality_counts = self.per_tile_quality_counts.get_mut(&tile).unwrap();
239
240 for (i, &q) in qual.iter().enumerate() {
241 quality_counts[i].add_value(q);
242 }
243 }
244
245 fn set_phred_encoding(&mut self, encoding: phred::PhredEncoding) {
246 self.known_encoding = Some(encoding);
247 }
248
249 fn name(&self) -> &str {
250 "Per tile sequence quality"
251 }
252
253 fn description(&self) -> &str {
254 "Shows the per tile Quality scores of all bases at a given position in a sequencing run"
255 }
256
257 fn reset(&mut self) {
258 self.total_count = 0;
259 self.per_tile_quality_counts.clear();
260 self.current_length = 0;
261 self.split_position = -1;
262 self.ignore_in_report = false;
263 }
264
265 fn raises_error(&self) -> bool {
266 let threshold = self.limits.threshold("tile\terror", 5.0);
267 self.calculate()
268 .is_some_and(|data| data.max_deviation > threshold)
269 }
270
271 fn raises_warning(&self) -> bool {
272 let threshold = self.limits.threshold("tile\twarn", 2.0);
273 self.calculate()
274 .is_some_and(|data| data.max_deviation > threshold)
275 }
276
277 fn ignore_filtered_sequences(&self) -> bool {
278 true
279 }
280
281 fn ignore_in_report(&self) -> bool {
282 self.ignore_in_report || self.limits.is_ignored("tile") || self.current_length == 0
284 }
285
286 fn write_text_report(&self, writer: &mut dyn io::Write) -> io::Result<()> {
287 let data = match self.calculate() {
288 Some(d) => d,
289 None => return Ok(()),
290 };
291
292 writeln!(writer, "#Tile\tBase\tMean")?;
294
295 for (t, &tile) in data.tiles.iter().enumerate() {
296 for i in 0..data.means[t].len() {
297 writeln!(
298 writer,
299 "{}\t{}\t{}",
300 tile,
301 data.x_labels[i],
302 java_format_double(data.means[t][i]),
303 )?;
304 }
305 }
306
307 Ok(())
308 }
309
310 fn chart_image_name(&self) -> Option<&str> {
312 Some("per_tile_quality")
313 }
314 fn chart_alt_text(&self) -> Option<&str> {
315 Some("Per tile sequence quality")
316 }
317 fn generate_chart_svg(&self) -> Option<String> {
318 self.build_chart_svg()
319 }
320}
321
322struct TileCalculatedData {
323 tiles: Vec<i32>,
324 means: Vec<Vec<f64>>,
325 x_labels: Vec<String>,
326 max_deviation: f64,
327}