Skip to main content

fastqc_rust/modules/
n_content.rs

1// Per Base N Content module
2// Corresponds to Modules/NContent.java
3
4use 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_N};
12use crate::utils::base_group::BaseGroup;
13use crate::utils::format::java_format_double;
14
15pub struct NContent {
16    n_counts: Vec<u64>,
17    not_n_counts: Vec<u64>,
18    nogroup: bool,
19    expgroup: bool,
20    limits: Limits,
21}
22
23impl NContent {
24    pub fn new(limits: &Limits, nogroup: bool, expgroup: bool) -> Self {
25        NContent {
26            n_counts: Vec::new(),
27            not_n_counts: Vec::new(),
28            nogroup,
29            expgroup,
30            limits: limits.clone(),
31        }
32    }
33
34    fn calculate(&self) -> NContentData {
35        let groups = BaseGroup::make_base_groups(self.n_counts.len(), self.nogroup, self.expgroup);
36
37        let mut x_categories = Vec::with_capacity(groups.len());
38        let mut percentages = vec![0.0f64; groups.len()];
39
40        for (i, group) in groups.iter().enumerate() {
41            x_categories.push(group.label());
42
43            let mut n_count: u64 = 0;
44            let mut total: u64 = 0;
45
46            // Java iterates `for (int bp=groups[i].lowerCount()-1;bp<groups[i].upperCount();bp++)`
47            // Our lower_count/upper_count are 0-based.
48            for bp in group.lower_count..=group.upper_count {
49                n_count += self.n_counts[bp];
50                total += self.n_counts[bp];
51                total += self.not_n_counts[bp];
52            }
53
54            // percentages[i] = 100 * (nCount / (double)total)
55            percentages[i] = 100.0 * (n_count as f64 / total as f64);
56        }
57
58        NContentData {
59            x_categories,
60            percentages,
61        }
62    }
63}
64
65impl NContent {
66    fn build_chart_svg(&self) -> String {
67        let data = self.calculate();
68
69        // minY=0, maxY=100 for percentage, matching Java's constructor
70        render_line_graph(&LineGraphData {
71            width: scaled_chart_width(data.x_categories.len()),
72            data: vec![data.percentages],
73            min_y: 0.0,
74            max_y: 100.0,
75            x_label: "Position in read (bp)".to_string(),
76            series_names: vec!["%N".to_string()],
77            x_categories: data.x_categories,
78            title: "N content across all bases".to_string(),
79        })
80    }
81}
82
83impl QCModule for NContent {
84    fn cost_hint(&self) -> u32 {
85        8
86    }
87
88    fn process_sequence(&mut self, sequence: &Sequence) {
89        let seq = &sequence.sequence;
90
91        // Grow arrays if needed
92        if self.n_counts.len() < seq.len() {
93            self.n_counts.resize(seq.len(), 0);
94            self.not_n_counts.resize(seq.len(), 0);
95        }
96
97        // Use lookup table to classify each byte without a multi-way match
98        for (i, &b) in seq.iter().enumerate() {
99            if BASE_INDEX[b as usize] as usize == IDX_N {
100                self.n_counts[i] += 1;
101            } else {
102                self.not_n_counts[i] += 1;
103            }
104        }
105    }
106
107    fn name(&self) -> &str {
108        "Per base N content"
109    }
110
111    fn description(&self) -> &str {
112        "Shows the percentage of bases at each position which are not being called"
113    }
114
115    fn reset(&mut self) {
116        self.n_counts.clear();
117        self.not_n_counts.clear();
118    }
119
120    fn raises_error(&self) -> bool {
121        let threshold = self.limits.threshold("n_content\terror", 20.0);
122        let data = self.calculate();
123        data.percentages.iter().any(|&p| p > threshold)
124    }
125
126    fn raises_warning(&self) -> bool {
127        let threshold = self.limits.threshold("n_content\twarn", 5.0);
128        let data = self.calculate();
129        data.percentages.iter().any(|&p| p > threshold)
130    }
131
132    fn ignore_filtered_sequences(&self) -> bool {
133        true
134    }
135
136    fn ignore_in_report(&self) -> bool {
137        self.limits.is_ignored("n_content")
138    }
139
140    fn write_text_report(&self, writer: &mut dyn io::Write) -> io::Result<()> {
141        let data = self.calculate();
142
143        // Header matches Java's makeReport
144        writeln!(writer, "#Base\tN-Count")?;
145
146        for i in 0..data.x_categories.len() {
147            writeln!(
148                writer,
149                "{}\t{}",
150                data.x_categories[i],
151                java_format_double(data.percentages[i]),
152            )?;
153        }
154
155        Ok(())
156    }
157
158    // Image filename matches Java's "per_base_n_content.png" in Images/
159    fn chart_image_name(&self) -> Option<&str> {
160        Some("per_base_n_content")
161    }
162    fn chart_alt_text(&self) -> Option<&str> {
163        Some("N content graph")
164    }
165    fn generate_chart_svg(&self) -> Option<String> {
166        Some(self.build_chart_svg())
167    }
168}
169
170struct NContentData {
171    x_categories: Vec<String>,
172    percentages: Vec<f64>,
173}