fastqc_rust/modules/
per_base_sequence_content.rs1use std::io;
5
6use crate::config::{Limits, LimitsExt};
7use crate::modules::QCModule;
8use crate::report::charts::line_graph::{render_line_graph, LineGraphData};
9use crate::report::charts::scaled_chart_width;
10use crate::sequence::Sequence;
11use crate::utils::base_counts::{BASE_INDEX, IDX_A, IDX_C, IDX_G, IDX_T};
12use crate::utils::base_group::BaseGroup;
13use crate::utils::format::java_format_double;
14
15pub struct PerBaseSequenceContent {
16 counts: Vec<[u64; 4]>,
20 nogroup: bool,
21 expgroup: bool,
22 limits: Limits,
23}
24
25impl PerBaseSequenceContent {
26 pub fn new(limits: &Limits, nogroup: bool, expgroup: bool) -> Self {
27 PerBaseSequenceContent {
28 counts: Vec::new(),
29 nogroup,
30 expgroup,
31 limits: limits.clone(),
32 }
33 }
34
35 fn calculate(&self) -> ContentData {
36 let groups = BaseGroup::make_base_groups(self.counts.len(), self.nogroup, self.expgroup);
37
38 let mut x_categories = Vec::with_capacity(groups.len());
39 let mut g_percent = vec![0.0f64; groups.len()];
40 let mut a_percent = vec![0.0f64; groups.len()];
41 let mut t_percent = vec![0.0f64; groups.len()];
42 let mut c_percent = vec![0.0f64; groups.len()];
43
44 for (i, group) in groups.iter().enumerate() {
45 x_categories.push(group.label());
46
47 let mut a_count: u64 = 0;
48 let mut c_count: u64 = 0;
49 let mut g_count: u64 = 0;
50 let mut t_count: u64 = 0;
51 let mut total: u64 = 0;
52
53 for bp in group.lower_count..=group.upper_count {
57 let c = &self.counts[bp];
58 a_count += c[IDX_A];
59 c_count += c[IDX_C];
60 g_count += c[IDX_G];
61 t_count += c[IDX_T];
62 total += c[IDX_A] + c[IDX_C] + c[IDX_G] + c[IDX_T];
63 }
64
65 g_percent[i] = (g_count as f64 / total as f64) * 100.0;
66 a_percent[i] = (a_count as f64 / total as f64) * 100.0;
67 t_percent[i] = (t_count as f64 / total as f64) * 100.0;
68 c_percent[i] = (c_count as f64 / total as f64) * 100.0;
69 }
70
71 ContentData {
73 x_categories,
74 t_percent,
75 c_percent,
76 a_percent,
77 g_percent,
78 }
79 }
80}
81
82impl PerBaseSequenceContent {
83 fn build_chart_svg(&self) -> String {
84 let data = self.calculate();
85
86 render_line_graph(&LineGraphData {
89 width: scaled_chart_width(data.x_categories.len()),
90 data: vec![
91 data.t_percent,
92 data.c_percent,
93 data.a_percent,
94 data.g_percent,
95 ],
96 min_y: 0.0,
97 max_y: 100.0,
98 x_label: "Position in read (bp)".to_string(),
99 series_names: vec![
100 "%T".to_string(),
101 "%C".to_string(),
102 "%A".to_string(),
103 "%G".to_string(),
104 ],
105 x_categories: data.x_categories,
106 title: "Sequence content across all bases".to_string(),
107 })
108 }
109}
110
111impl QCModule for PerBaseSequenceContent {
112 fn cost_hint(&self) -> u32 {
113 9
114 }
115
116 fn process_sequence(&mut self, sequence: &Sequence) {
117 let seq = &sequence.sequence;
118
119 if self.counts.len() < seq.len() {
121 self.counts.resize(seq.len(), [0; 4]);
122 }
123
124 for (i, &b) in seq.iter().enumerate() {
128 let idx = BASE_INDEX[b as usize] as usize;
129 if idx < 4 {
130 self.counts[i][idx] += 1;
131 }
132 }
133 }
134
135 fn name(&self) -> &str {
136 "Per base sequence content"
137 }
138
139 fn description(&self) -> &str {
140 "Shows the relative amounts of each base at each position in a sequencing run"
141 }
142
143 fn reset(&mut self) {
144 self.counts.clear();
145 }
146
147 fn raises_error(&self) -> bool {
148 let error_threshold = self.limits.threshold("sequence\terror", 20.0);
149 let data = self.calculate();
150
151 for i in 0..data.g_percent.len() {
153 let gc_diff = (data.c_percent[i] - data.g_percent[i]).abs();
154 let at_diff = (data.t_percent[i] - data.a_percent[i]).abs();
155
156 if gc_diff > error_threshold || at_diff > error_threshold {
157 return true;
158 }
159 }
160 false
161 }
162
163 fn raises_warning(&self) -> bool {
164 let warn_threshold = self.limits.threshold("sequence\twarn", 10.0);
165 let data = self.calculate();
166
167 for i in 0..data.g_percent.len() {
168 let gc_diff = (data.c_percent[i] - data.g_percent[i]).abs();
169 let at_diff = (data.t_percent[i] - data.a_percent[i]).abs();
170
171 if gc_diff > warn_threshold || at_diff > warn_threshold {
172 return true;
173 }
174 }
175 false
176 }
177
178 fn ignore_filtered_sequences(&self) -> bool {
179 true
180 }
181
182 fn ignore_in_report(&self) -> bool {
183 self.limits.is_ignored("sequence")
184 }
185
186 fn write_text_report(&self, writer: &mut dyn io::Write) -> io::Result<()> {
187 let data = self.calculate();
188
189 writeln!(writer, "#Base\tG\tA\tT\tC")?;
191
192 for i in 0..data.x_categories.len() {
193 writeln!(
195 writer,
196 "{}\t{}\t{}\t{}\t{}",
197 data.x_categories[i],
198 java_format_double(data.g_percent[i]),
199 java_format_double(data.a_percent[i]),
200 java_format_double(data.t_percent[i]),
201 java_format_double(data.c_percent[i]),
202 )?;
203 }
204
205 Ok(())
206 }
207
208 fn chart_image_name(&self) -> Option<&str> {
210 Some("per_base_sequence_content")
211 }
212 fn chart_alt_text(&self) -> Option<&str> {
213 Some("Per base sequence content")
214 }
215 fn generate_chart_svg(&self) -> Option<String> {
216 Some(self.build_chart_svg())
217 }
218}
219
220struct ContentData {
221 x_categories: Vec<String>,
222 t_percent: Vec<f64>,
223 c_percent: Vec<f64>,
224 a_percent: Vec<f64>,
225 g_percent: Vec<f64>,
226}