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}