fastqc_rust/modules/
gc_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::CHART_WIDTH;
10use crate::sequence::Sequence;
11use crate::utils::base_counts::count_acgt;
12use crate::utils::format::java_format_double;
13
14struct GCModelValue {
16 percentage: usize,
17 increment: f64,
18}
19
20struct GCModel {
24 models: Vec<Vec<GCModelValue>>,
25}
26
27impl GCModel {
28 fn new(read_length: usize) -> Self {
29 let mut claiming_counts = vec![0usize; 101];
32
33 for pos in 0..=read_length {
34 let low_count = if pos == 0 { 0.0 } else { pos as f64 - 0.5 };
35 let high_count = (pos as f64 + 0.5).min(read_length as f64);
36 let low_count = low_count.max(0.0);
38
39 let low_percentage = ((low_count * 100.0) / read_length as f64).round() as usize;
40 let high_percentage = ((high_count * 100.0) / read_length as f64).round() as usize;
41
42 for cc in &mut claiming_counts[low_percentage..=high_percentage] {
43 *cc += 1;
44 }
45 }
46
47 let mut models = Vec::with_capacity(read_length + 1);
49
50 for pos in 0..=read_length {
51 let low_count = if pos == 0 { 0.0 } else { pos as f64 - 0.5 };
52 let high_count = (pos as f64 + 0.5).min(read_length as f64);
53 let low_count = low_count.max(0.0);
54
55 let low_percentage = ((low_count * 100.0) / read_length as f64).round() as usize;
56 let high_percentage = ((high_count * 100.0) / read_length as f64).round() as usize;
57
58 let mut values = Vec::with_capacity((high_percentage - low_percentage) + 1);
59 for (p, &cc) in (low_percentage..=high_percentage)
61 .zip(&claiming_counts[low_percentage..=high_percentage])
62 {
63 values.push(GCModelValue {
64 percentage: p,
65 increment: 1.0 / cc as f64,
66 });
67 }
68 models.push(values);
69 }
70
71 GCModel { models }
72 }
73
74 fn get_model_values(&self, gc_count: usize) -> &[GCModelValue] {
75 &self.models[gc_count]
76 }
77}
78
79pub struct PerSequenceGCContent {
80 gc_distribution: [f64; 101],
81 cached_models: Vec<Option<GCModel>>,
83 limits: Limits,
84 deviation_percent: Option<f64>,
86 theoretical_distribution: Option<[f64; 101]>,
88}
89
90impl PerSequenceGCContent {
91 pub fn new(limits: &Limits) -> Self {
92 PerSequenceGCContent {
93 gc_distribution: [0.0; 101],
94 cached_models: Vec::new(),
96 limits: limits.clone(),
97 deviation_percent: None,
98 theoretical_distribution: None,
99 }
100 }
101
102 fn truncate_length(len: usize) -> usize {
105 if len > 1000 {
106 (len / 1000) * 1000
107 } else if len > 100 {
108 (len / 100) * 100
109 } else {
110 len
111 }
112 }
113
114 fn calculate_distribution(&mut self) {
116 if self.deviation_percent.is_some() {
117 return;
118 }
119
120 let mut total_count: f64 = 0.0;
121 let mut first_mode: usize = 0;
122 let mut mode_count: f64 = 0.0;
123
124 for i in 0..101 {
125 total_count += self.gc_distribution[i];
126 if self.gc_distribution[i] > mode_count {
127 mode_count = self.gc_distribution[i];
128 first_mode = i;
129 }
130 }
131
132 let mut mode: f64 = 0.0;
136 let mut mode_duplicates: usize = 0;
137 let mut fell_off_top = true;
138
139 for i in first_mode..101 {
140 if self.gc_distribution[i]
141 > self.gc_distribution[first_mode] - (self.gc_distribution[first_mode] / 10.0)
142 {
143 mode += i as f64;
144 mode_duplicates += 1;
145 } else {
146 fell_off_top = false;
147 break;
148 }
149 }
150
151 let mut fell_off_bottom = true;
152 if first_mode > 0 {
153 for i in (0..first_mode).rev() {
154 if self.gc_distribution[i]
155 > self.gc_distribution[first_mode] - (self.gc_distribution[first_mode] / 10.0)
156 {
157 mode += i as f64;
158 mode_duplicates += 1;
159 } else {
160 fell_off_bottom = false;
161 break;
162 }
163 }
164 }
165
166 if fell_off_bottom || fell_off_top {
169 mode = first_mode as f64;
170 } else {
171 mode /= mode_duplicates as f64;
172 }
173
174 let mut stdev: f64 = 0.0;
176 for i in 0..101 {
177 stdev += (i as f64 - mode).powi(2) * self.gc_distribution[i];
178 }
179 stdev /= total_count - 1.0;
181 stdev = stdev.sqrt();
182
183 let mut deviation_percent: f64 = 0.0;
186 let mut theoretical = [0.0f64; 101];
187
188 for (i, (theo, &observed)) in theoretical
190 .iter_mut()
191 .zip(self.gc_distribution.iter())
192 .enumerate()
193 {
194 let lhs = 1.0 / (2.0 * std::f64::consts::PI * stdev * stdev).sqrt();
195 let rhs =
196 std::f64::consts::E.powf(-(((i as f64) - mode).powi(2)) / (2.0 * stdev * stdev));
197 *theo = lhs * rhs * total_count;
198
199 deviation_percent += (*theo - observed).abs();
200 }
201
202 deviation_percent /= total_count;
203 deviation_percent *= 100.0;
204
205 self.theoretical_distribution = Some(theoretical);
206 self.deviation_percent = Some(deviation_percent);
207 }
208
209 fn get_deviation_percent(&self) -> f64 {
210 self.deviation_percent.unwrap_or(0.0)
211 }
212}
213
214impl PerSequenceGCContent {
215 fn build_chart_svg(&self) -> String {
216 let gc_dist: Vec<f64> = self.gc_distribution.to_vec();
217 let theoretical = self.theoretical_distribution.unwrap_or([0.0; 101]).to_vec();
218
219 let max_val = gc_dist
221 .iter()
222 .chain(theoretical.iter())
223 .cloned()
224 .fold(0.0_f64, f64::max);
225
226 let max_y = max_val;
228
229 let x_categories: Vec<String> = (0..101).map(|i| format!("{}", i)).collect();
230
231 render_line_graph(&LineGraphData {
233 width: CHART_WIDTH,
234 data: vec![gc_dist, theoretical],
235 min_y: 0.0,
236 max_y,
237 x_label: "Mean GC content (%)".to_string(),
238 series_names: vec![
239 "GC count per read".to_string(),
240 "Theoretical Distribution".to_string(),
241 ],
242 x_categories,
243 title: "GC distribution over all sequences".to_string(),
244 })
245 }
246}
247
248impl QCModule for PerSequenceGCContent {
249 fn cost_hint(&self) -> u32 {
250 2
251 }
252
253 fn process_sequence(&mut self, sequence: &Sequence) {
254 self.deviation_percent = None;
256 self.theoretical_distribution = None;
257
258 let seq = &sequence.sequence;
259 let truncated_len = Self::truncate_length(seq.len());
260 if truncated_len == 0 {
261 return;
262 }
263
264 let [_, c, g, _] = count_acgt(&seq[..truncated_len]);
266 let gc_count = (c + g) as usize;
267
268 if truncated_len >= self.cached_models.len() {
270 self.cached_models.resize_with(truncated_len + 1, || None);
271 }
272
273 if self.cached_models[truncated_len].is_none() {
275 self.cached_models[truncated_len] = Some(GCModel::new(truncated_len));
276 }
277
278 let model = self.cached_models[truncated_len].as_ref().unwrap();
279 let values = model.get_model_values(gc_count);
280
281 for v in values {
282 self.gc_distribution[v.percentage] += v.increment;
283 }
284 }
285
286 fn name(&self) -> &str {
287 "Per sequence GC content"
288 }
289
290 fn description(&self) -> &str {
291 "Shows the distribution of GC contents for whole sequences"
292 }
293
294 fn reset(&mut self) {
295 self.gc_distribution = [0.0; 101];
296 self.deviation_percent = None;
297 self.theoretical_distribution = None;
298 }
299
300 fn finalize(&mut self) {
301 self.calculate_distribution();
302 }
303
304 fn raises_error(&self) -> bool {
305 self.get_deviation_percent() > self.limits.threshold("gc_sequence\terror", 30.0)
306 }
307
308 fn raises_warning(&self) -> bool {
309 self.get_deviation_percent() > self.limits.threshold("gc_sequence\twarn", 15.0)
310 }
311
312 fn ignore_filtered_sequences(&self) -> bool {
313 true
314 }
315
316 fn ignore_in_report(&self) -> bool {
317 self.limits.is_ignored("gc_sequence")
318 }
319
320 fn write_text_report(&self, writer: &mut dyn io::Write) -> io::Result<()> {
321 writeln!(writer, "#GC Content\tCount")?;
323 for i in 0..101 {
325 writeln!(
326 writer,
327 "{}\t{}",
328 i,
329 java_format_double(self.gc_distribution[i])
330 )?;
331 }
332 Ok(())
333 }
334
335 fn chart_image_name(&self) -> Option<&str> {
337 Some("per_sequence_gc_content")
338 }
339 fn chart_alt_text(&self) -> Option<&str> {
340 Some("Per sequence GC content graph")
341 }
342 fn generate_chart_svg(&self) -> Option<String> {
343 Some(self.build_chart_svg())
344 }
345}