1use std::io;
5
6use crate::config::{Limits, LimitsExt};
7use crate::modules::QCModule;
8use crate::report::charts::quality_boxplot::{render_quality_boxplot, QualityBoxPlotData};
9use crate::report::charts::scaled_chart_width;
10use crate::sequence::Sequence;
11use crate::utils::base_group::BaseGroup;
12use crate::utils::format::java_format_double;
13use crate::utils::phred;
14use crate::utils::quality_count::{self, QualityCount};
15
16pub struct PerBaseQualityScores {
17 quality_counts: Vec<QualityCount>,
18 nogroup: bool,
19 expgroup: bool,
20 known_encoding: Option<phred::PhredEncoding>,
22 limits: Limits,
23}
24
25impl PerBaseQualityScores {
26 pub fn new(limits: &Limits, nogroup: bool, expgroup: bool) -> Self {
27 PerBaseQualityScores {
28 quality_counts: Vec::new(),
29 nogroup,
30 expgroup,
31 known_encoding: None,
32 limits: limits.clone(),
33 }
34 }
35
36 fn calculate(&self) -> CalculatedData {
37 let (min_char, _max_char) = quality_count::calculate_offsets(&self.quality_counts);
38 let offset = phred::resolve(self.known_encoding, min_char as u16)
40 .map(|e| e.offset)
41 .unwrap_or(phred::PhredEncoding::SANGER.offset);
42
43 let groups =
44 BaseGroup::make_base_groups(self.quality_counts.len(), self.nogroup, self.expgroup);
45
46 let mut means = vec![0.0f64; groups.len()];
47 let mut medians = vec![0.0f64; groups.len()];
48 let mut lower_quartile = vec![0.0f64; groups.len()];
49 let mut upper_quartile = vec![0.0f64; groups.len()];
50 let mut lowest = vec![0.0f64; groups.len()];
51 let mut highest = vec![0.0f64; groups.len()];
52 let mut x_labels = Vec::with_capacity(groups.len());
53
54 for (i, group) in groups.iter().enumerate() {
55 x_labels.push(group.label());
56 let min_base = group.lower_count;
59 let max_base = group.upper_count;
60 lowest[i] = self.get_percentile(min_base, max_base, offset, 10);
61 highest[i] = self.get_percentile(min_base, max_base, offset, 90);
62 means[i] = self.get_mean(min_base, max_base, offset);
63 medians[i] = self.get_percentile(min_base, max_base, offset, 50);
64 lower_quartile[i] = self.get_percentile(min_base, max_base, offset, 25);
65 upper_quartile[i] = self.get_percentile(min_base, max_base, offset, 75);
66 }
67
68 CalculatedData {
69 means,
70 medians,
71 lower_quartile,
72 upper_quartile,
73 lowest,
74 highest,
75 x_labels,
76 }
77 }
78
79 fn get_percentile(&self, min_base: usize, max_base: usize, offset: u8, percentile: u8) -> f64 {
83 let mut count = 0;
84 let mut total = 0.0;
85
86 for i in min_base..=max_base {
89 if self.quality_counts[i].get_total_count() > 100 {
91 count += 1;
92 total += self.quality_counts[i].get_percentile(offset, percentile);
93 }
94 }
95
96 if count > 0 {
97 total / count as f64
98 } else {
99 f64::NAN
100 }
101 }
102
103 fn get_mean(&self, min_base: usize, max_base: usize, offset: u8) -> f64 {
106 let mut count = 0;
107 let mut total = 0.0;
108
109 for i in min_base..=max_base {
110 if self.quality_counts[i].get_total_count() > 0 {
112 count += 1;
113 total += self.quality_counts[i].get_mean(offset);
114 }
115 }
116
117 if count > 0 {
118 total / count as f64
119 } else {
120 0.0
122 }
123 }
124}
125
126impl PerBaseQualityScores {
127 fn build_chart_svg(&self) -> String {
129 let data = self.calculate();
130 let (min_char, max_char) = quality_count::calculate_offsets(&self.quality_counts);
131 let encoding = phred::resolve(self.known_encoding, min_char as u16)
133 .unwrap_or(phred::PhredEncoding::SANGER);
134 let (offset, encoding_name) = (encoding.offset, encoding.name);
135
136 let title = format!(
138 "Quality scores across all bases ({} encoding)",
139 encoding_name
140 );
141
142 let mut max_y = (max_char as i32 - offset as i32).max(0) as f64;
146 if max_y < 35.0 {
147 max_y = 35.0;
148 }
149 let y_interval = 2.0;
150 let min_y = 0.0;
151
152 render_quality_boxplot(&QualityBoxPlotData {
153 width: scaled_chart_width(data.x_labels.len()),
154 means: data.means,
155 medians: data.medians,
156 lower_quartile: data.lower_quartile,
157 upper_quartile: data.upper_quartile,
158 lowest: data.lowest,
159 highest: data.highest,
160 min_y,
161 max_y,
162 y_interval,
163 x_labels: data.x_labels,
164 title,
165 })
166 }
167}
168
169impl QCModule for PerBaseQualityScores {
170 fn cost_hint(&self) -> u32 {
171 10
172 }
173
174 fn process_sequence(&mut self, sequence: &Sequence) {
175 let qual = &sequence.quality;
176
177 if self.quality_counts.len() < qual.len() {
179 self.quality_counts
180 .resize_with(qual.len(), QualityCount::new);
181 }
182
183 for (i, &q) in qual.iter().enumerate() {
184 self.quality_counts[i].add_value(q);
185 }
186 }
187
188 fn set_phred_encoding(&mut self, encoding: phred::PhredEncoding) {
189 self.known_encoding = Some(encoding);
190 }
191
192 fn name(&self) -> &str {
193 "Per base sequence quality"
194 }
195
196 fn description(&self) -> &str {
197 "Shows the Quality scores of all bases at a given position in a sequencing run"
198 }
199
200 fn reset(&mut self) {
201 self.quality_counts.clear();
202 }
203
204 fn raises_error(&self) -> bool {
205 let data = self.calculate();
206 let lq_error = self.limits.threshold("quality_base_lower\terror", 5.0);
207 let median_error = self.limits.threshold("quality_base_median\terror", 20.0);
208
209 for i in 0..data.lower_quartile.len() {
210 if data.lower_quartile[i].is_nan() {
211 continue;
213 }
214 if data.lower_quartile[i] < lq_error || data.medians[i] < median_error {
215 return true;
216 }
217 }
218 false
219 }
220
221 fn raises_warning(&self) -> bool {
222 let data = self.calculate();
223 let lq_warn = self.limits.threshold("quality_base_lower\twarn", 10.0);
224 let median_warn = self.limits.threshold("quality_base_median\twarn", 25.0);
225
226 for i in 0..data.lower_quartile.len() {
227 if data.lower_quartile[i].is_nan() {
228 continue;
229 }
230 if data.lower_quartile[i] < lq_warn || data.medians[i] < median_warn {
231 return true;
232 }
233 }
234 false
235 }
236
237 fn ignore_filtered_sequences(&self) -> bool {
238 true
239 }
240
241 fn ignore_in_report(&self) -> bool {
242 self.limits.is_ignored("quality_base") || self.quality_counts.is_empty()
244 }
245
246 fn write_text_report(&self, writer: &mut dyn io::Write) -> io::Result<()> {
247 let data = self.calculate();
248
249 writeln!(
251 writer,
252 "#Base\tMean\tMedian\tLower Quartile\tUpper Quartile\t10th Percentile\t90th Percentile"
253 )?;
254
255 for i in 0..data.means.len() {
256 writeln!(
259 writer,
260 "{}\t{}\t{}\t{}\t{}\t{}\t{}",
261 data.x_labels[i],
262 java_format_double(data.means[i]),
263 java_format_double(data.medians[i]),
264 java_format_double(data.lower_quartile[i]),
265 java_format_double(data.upper_quartile[i]),
266 java_format_double(data.lowest[i]),
267 java_format_double(data.highest[i]),
268 )?;
269 }
270
271 Ok(())
272 }
273
274 fn chart_image_name(&self) -> Option<&str> {
276 Some("per_base_quality")
277 }
278 fn chart_alt_text(&self) -> Option<&str> {
279 Some("Per base quality graph")
280 }
281 fn generate_chart_svg(&self) -> Option<String> {
282 Some(self.build_chart_svg())
283 }
284}
285
286struct CalculatedData {
288 means: Vec<f64>,
289 medians: Vec<f64>,
290 lower_quartile: Vec<f64>,
291 upper_quartile: Vec<f64>,
292 lowest: Vec<f64>,
293 highest: Vec<f64>,
294 x_labels: Vec<String>,
295}