Skip to main content

fastqc_rust/modules/
mod.rs

1pub mod adapter_content;
2pub mod basic_stats;
3pub mod duplication_level;
4pub mod gc_content;
5pub mod kmer_content;
6pub mod n_content;
7pub mod overrepresented_seqs;
8pub mod per_base_quality;
9pub mod per_base_sequence_content;
10pub mod per_sequence_quality;
11pub mod per_tile_quality;
12pub mod sequence_length_distribution;
13
14use std::fmt;
15use std::io;
16use std::sync::{Arc, Mutex};
17
18use crate::config::{FastQCConfig, Limits, LimitsExt};
19use crate::sequence::Sequence;
20use crate::utils::phred::PhredEncoding;
21
22/// Status of a QC module after analysis, matching Java FastQC's pass/warn/fail icons.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum ModuleStatus {
25    Pass,
26    Warn,
27    Fail,
28}
29
30impl fmt::Display for ModuleStatus {
31    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32        // These exact strings appear in the text report summary section
33        match self {
34            ModuleStatus::Pass => write!(f, "PASS"),
35            ModuleStatus::Warn => write!(f, "WARN"),
36            ModuleStatus::Fail => write!(f, "FAIL"),
37        }
38    }
39}
40
41/// Trait that all QC analysis modules must implement.
42///
43/// Mirrors the `QCModule` Java interface method-for-method
44/// (minus Swing GUI methods like `getResultsPanel()`).
45pub trait QCModule: Send {
46    /// Process a single sequence record, accumulating statistics.
47    fn process_sequence(&mut self, sequence: &Sequence);
48
49    /// A rough, static hint of this module's per-sequence processing cost
50    /// relative to the other modules. It is used *only* to balance the modules
51    /// across worker threads in the parallel analysis pipeline, so that the few
52    /// expensive modules don't pile onto one thread. The values are approximate
53    /// and never affect results — a wrong hint only costs a little parallel
54    /// efficiency, never correctness. Keep them in proportion to a profile of
55    /// the default modules on short reads; the default suits a near-free module.
56    fn cost_hint(&self) -> u32 {
57        1
58    }
59
60    /// The display name of this module as shown in the report.
61    fn name(&self) -> &str;
62
63    /// A longer description of what this module checks.
64    fn description(&self) -> &str;
65
66    /// Reset the module to its initial state.
67    fn reset(&mut self);
68
69    /// Set the source filename for this module.
70    /// BasicStats uses this to display the filename in the report.
71    /// Other modules ignore it.
72    fn set_filename(&mut self, _name: &str) {}
73
74    /// Attach a shared snapshot sink that the module publishes partial results
75    /// to while it runs, so the terminal progress display can show live
76    /// statistics. Only BasicStats implements this; every other module ignores
77    /// it, and nothing about the analysis changes when no sink is attached.
78    fn attach_live_stats(&mut self, _live: Arc<basic_stats::LiveStats>) {}
79
80    /// Tell the module the quality encoding specified by the input format,
81    /// when there is one (BAM/SAM, where quality is Phred+33 by construction).
82    /// Modules that would otherwise infer the encoding from the lowest quality
83    /// character use this instead; other modules ignore it. Never called for
84    /// formats like FASTQ where the encoding must be inferred.
85    fn set_phred_encoding(&mut self, _encoding: PhredEncoding) {}
86
87    /// Finalize calculations after all sequences have been processed.
88    ///
89    /// In Java, modules lazily compute results in synchronized
90    /// methods called from getResultsPanel()/makeReport()/raisesError()/raisesWarning().
91    /// In Rust, we separate the mutable computation phase (finalize) from the
92    /// immutable reporting phase so that raises_error/raises_warning/write_text_report
93    /// can take &self.
94    fn finalize(&mut self) {}
95
96    /// Whether the module results should trigger a failure status.
97    fn raises_error(&self) -> bool;
98
99    /// Whether the module results should trigger a warning status.
100    fn raises_warning(&self) -> bool;
101
102    /// Whether this module should skip sequences flagged as filtered.
103    fn ignore_filtered_sequences(&self) -> bool;
104
105    /// Whether this module should be excluded from the report.
106    /// Matches `ignoreInReport()` - used e.g. by PerTileQuality when
107    /// there are no tile IDs in the data.
108    fn ignore_in_report(&self) -> bool;
109
110    /// The current status of this module.
111    fn status(&self) -> ModuleStatus {
112        if self.raises_error() {
113            ModuleStatus::Fail
114        } else if self.raises_warning() {
115            ModuleStatus::Warn
116        } else {
117            ModuleStatus::Pass
118        }
119    }
120
121    /// Return the base filename for this module's chart image (without extension).
122    /// Matches the filenames used in Images/ directory of the zip archive,
123    /// e.g. "per_base_quality" for per_base_quality.png and per_base_quality.svg.
124    fn chart_image_name(&self) -> Option<&str> {
125        None
126    }
127
128    /// Return the alt text for this module's chart image, if it has one.
129    /// Modules that produce charts should override this to return Some("...").
130    /// The default `write_html_report` uses this to call `write_chart_and_table`
131    /// automatically, so modules with charts only need to implement this method
132    /// instead of overriding `write_html_report`.
133    fn chart_alt_text(&self) -> Option<&str> {
134        None
135    }
136
137    /// Generate the SVG chart content for this module, if applicable.
138    /// In Java, writeDefaultImage() renders the Swing JPanel to both SVG
139    /// and PNG. Here we generate SVG first, then convert to PNG via resvg.
140    fn generate_chart_svg(&self) -> Option<String> {
141        None
142    }
143
144    /// Write this module's section to the text report.
145    fn write_text_report(&self, writer: &mut dyn io::Write) -> io::Result<()>;
146
147    /// Write this module's HTML content (table and/or chart image) to the report.
148    ///
149    /// In Java, each module's makeReport() calls writeTable() which
150    /// calls writeXhtmlTable() then writeTextTable(). Some modules also call
151    /// writeDefaultImage() to embed a chart. The default implementation here
152    /// checks for `chart_alt_text()` -- if present, it renders the chart and table
153    /// via `write_chart_and_table`; otherwise it renders just an HTML table from
154    /// the text report output, matching writeXhtmlTable().
155    fn write_html_report(&self, writer: &mut dyn io::Write, png: bool) -> io::Result<()> {
156        // Modules with charts show only the chart in HTML, not the data table.
157        // The data table only goes into fastqc_data.txt.
158        if let Some(alt_text) = self.chart_alt_text() {
159            return crate::report::html::write_chart(self, alt_text, png, writer);
160        }
161        // Default: render the text report data as an HTML table
162        let mut text_buf = Vec::new();
163        self.write_text_report(&mut text_buf)?;
164        let text = String::from_utf8(text_buf)
165            .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
166        crate::report::html::write_default_html_table(&text, writer)
167    }
168}
169
170/// Create the standard set of QC modules based on configuration.
171///
172/// Module order matches `ModuleFactory.java` instantiation order,
173/// which determines the order they appear in the report.
174/// The order is: BasicStats, PerBaseQuality, PerTileQuality, PerSequenceQuality,
175/// PerBaseContent, PerSequenceGC, NContent, SequenceLength, DuplicationLevel,
176/// OverRepresentedSeqs, AdapterContent, KmerContent.
177///
178/// Note: DuplicationLevel is listed BEFORE OverRepresentedSeqs in the report,
179/// but shares data with it. In Java, OverRepresentedSeqs creates DuplicationLevel
180/// and adds it to the module list before itself.
181pub fn create_modules(config: &FastQCConfig, limits: &Limits) -> Vec<Box<dyn QCModule>> {
182    let mut modules: Vec<Box<dyn QCModule>> = Vec::new();
183
184    let ng = config.nogroup;
185    let eg = config.expgroup;
186
187    // Load adapter and contaminant lists
188    let adapters = config.load_adapters().unwrap_or_default();
189    let contaminants = config.load_contaminants().unwrap_or_default();
190
191    // 1. BasicStats
192    modules.push(Box::new(basic_stats::BasicStats::new(limits)));
193
194    // 2. PerBaseQualityScores
195    if limits.is_module_enabled("quality_base") {
196        modules.push(Box::new(per_base_quality::PerBaseQualityScores::new(
197            limits, ng, eg,
198        )));
199    }
200
201    // 3. PerTileQualityScores
202    if limits.is_module_enabled("tile") {
203        modules.push(Box::new(per_tile_quality::PerTileQualityScores::new(
204            limits, ng, eg,
205        )));
206    }
207
208    // 4. PerSequenceQualityScores
209    if limits.is_module_enabled("quality_sequence") {
210        modules.push(Box::new(
211            per_sequence_quality::PerSequenceQualityScores::new(limits),
212        ));
213    }
214
215    // 5. PerBaseSequenceContent
216    if limits.is_module_enabled("sequence") {
217        modules.push(Box::new(
218            per_base_sequence_content::PerBaseSequenceContent::new(limits, ng, eg),
219        ));
220    }
221
222    // 6. PerSequenceGCContent
223    if limits.is_module_enabled("gc_sequence") {
224        modules.push(Box::new(gc_content::PerSequenceGCContent::new(limits)));
225    }
226
227    // 7. NContent
228    if limits.is_module_enabled("n_content") {
229        modules.push(Box::new(n_content::NContent::new(limits, ng, eg)));
230    }
231
232    // 8. SequenceLengthDistribution
233    if limits.is_module_enabled("sequence_length") {
234        modules.push(Box::new(
235            sequence_length_distribution::SequenceLengthDistribution::new(limits, ng),
236        ));
237    }
238
239    // 9 & 10. DuplicationLevel and OverRepresentedSeqs (shared data)
240    // OverRepresentedSeqs creates DuplicationLevel in its constructor.
241    // DuplicationLevel appears BEFORE OverRepresentedSeqs in the module list.
242    let shared_data = Arc::new(Mutex::new(overrepresented_seqs::OverRepresentedData::new()));
243
244    if limits.is_module_enabled("duplication") {
245        modules.push(Box::new(duplication_level::DuplicationLevel::new(
246            shared_data.clone(),
247            limits,
248        )));
249    }
250
251    if limits.is_module_enabled("overrepresented") {
252        modules.push(Box::new(overrepresented_seqs::OverRepresentedSeqs::new(
253            limits,
254            config.dup_length,
255            &contaminants,
256            shared_data.clone(),
257        )));
258    }
259
260    // 11. AdapterContent
261    if limits.is_module_enabled("adapter") {
262        modules.push(Box::new(adapter_content::AdapterContent::new(
263            limits, &adapters, ng, eg,
264        )));
265    }
266
267    // 12. KmerContent
268    // Kmer is ignored by default (limits.txt: kmer ignore 1)
269    if limits.is_module_enabled("kmer") {
270        modules.push(Box::new(kmer_content::KmerContent::new(
271            limits,
272            config.kmer_size,
273            ng,
274            eg,
275        )));
276    }
277
278    modules
279}