Skip to content

Commit fc1a045

Browse files
feat(dna): reimplement Qualimap bamqc
Writes genome_results.txt, ten of the raw data tables and an HTML summary. genome_results.txt matches Qualimap 2.3 on every line but four, and the clipping profile, nucleotide content and mapping quality histogram match byte for byte. Several of Qualimap's rules are surprising and none were guessable from its output alone. The reference is split into ceil(len / ceil(len / 400)) windows, so 397 rather than 400. Coverage counts every primary mapped record with no filtering at all, counts deletions, and does not correct mate overlaps, which is why it reports 16.77 where mosdepth reports 6.20. The global mean mapping quality is the mean of the per-window means with empty windows contributing zero, hence 2.4178 rather than about 60, while the per-position histogram truncates that mean instead of rounding it. Mismatches are NM less inserted bases only. Base composition is counted in reference orientation while the clipped span selecting which positions count is taken in sequencing orientation; mixing the two is what Qualimap does and matching it is the only way the composition agrees. Four residuals are documented rather than papered over. The mean mapping quality and the coverage standard deviation differ in the fourth decimal because Qualimap accumulates them per window. About five reference positions of 40001 sit one deeper here, which carries into the coverage histogram and the fractions derived from it. The homopolymer indel classification differs outright: Qualimap reads a reference context this does not reconstruct, and reports two polyC indels that no read-derived rule produces, since the deleted bases are not in the read. Qualimap's GC content distribution and duplication rate histogram are not written. The first is computed over a 679-read subsample whose selection rule is undocumented, the second uses a definition that is not a read-start count. Emitting tables under those names with different numbers would be worse than leaving them out. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent a96e30c commit fc1a045

28 files changed

Lines changed: 4513 additions & 6 deletions

AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ src/
8282
hs_metrics.rs — Picard CollectHsMetrics reimplementation (targeted mode)
8383
insert_size.rs — Picard CollectInsertSizeMetrics reimplementation
8484
intervals.rs — BED interval parsing and merging for targeted mode
85+
qualimap.rs — Qualimap bamqc accumulation (windows, coverage, composition)
86+
qualimap_output.rs — genome_results.txt, the raw data tables and the HTML report
8587
wgs_metrics.rs — Picard CollectWgsMetrics reimplementation
8688
mosdepth/
8789
mod.rs — Per-contig summarisation feeding the mosdepth outputs

CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@
1010
alignment with one worker per contig, plus Picard-compatible
1111
CollectWgsMetrics, CollectInsertSizeMetrics and CollectGcBiasMetrics.
1212
Passing `--targets` switches on targeted mode and Picard-compatible
13-
CollectHsMetrics. Validated for exact parity against mosdepth 0.3.14,
14-
samtools 1.24 and Picard 3.4.0.
13+
CollectHsMetrics. Qualimap-compatible `bamqc` output rounds it out, with
14+
`genome_results.txt`, the raw data tables and an HTML summary. Validated
15+
against mosdepth 0.3.14, samtools 1.24, Picard 3.4.0 and Qualimap 2.3.
1516

1617
### Changed
1718

docs/astro.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ export default defineConfig({
7878
{ label: "Overview", slug: "dna/overview" },
7979
{ label: "mosdepth", slug: "dna/mosdepth" },
8080
{ label: "Picard metrics", slug: "dna/picard" },
81+
{ label: "Qualimap bamqc", slug: "dna/qualimap" },
8182
],
8283
},
8384
{
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
---
2+
title: Qualimap bamqc
3+
description: The bamqc outputs RustQC produces, how Qualimap's coverage differs from every other tool here, and which figures are not reproduced.
4+
---
5+
6+
import { Aside } from "@astrojs/starlight/components";
7+
8+
<Aside type="note" title="About Qualimap">
9+
Qualimap examines alignment data to detect biases in sequencing and mapping,
10+
and reports them as tables and plots.
11+
12+
[Documentation](http://qualimap.conesalab.org/) | Okonechnikov K, Conesa A, García-Alcalde F. *Bioinformatics*. 2016;32(2):292-294.
13+
</Aside>
14+
15+
RustQC writes `qualimap/genome_results.txt`, the
16+
`raw_data_qualimapReport/` tables and an HTML summary.
17+
18+
<Aside type="caution" title="Qualimap's coverage is not the others' coverage">
19+
Qualimap counts every primary mapped record with no duplicate, mapping quality
20+
or base quality filtering, counts deletions as covered, and does **not**
21+
correct mate overlaps. On the project's test data it reports a mean coverage of
22+
16.77 where mosdepth reports 6.20 and `CollectWgsMetrics` reports 3.53. All
23+
three are right about different questions. Do not compare them to each other.
24+
</Aside>
25+
26+
## Two figures that surprise people
27+
28+
**Mean mapping quality reads about 2.4, not about 60.** It is the mean of the
29+
per-window means, and a window with no reads contributes zero. On a targeted or
30+
low-coverage library, most windows are empty, so the figure is closer to the
31+
fraction of the genome covered than to the quality of the alignments. The
32+
per-position histogram, which only counts covered positions, is the one to read
33+
for that.
34+
35+
**Base composition is reported in reference orientation.** Reverse-strand reads
36+
are reverse-complemented before counting, so the A and T columns are not the
37+
counts of A and T in the sequencer's output.
38+
39+
## What matches Qualimap and what does not
40+
41+
`genome_results.txt` matches on every line but four, and three of the raw
42+
tables match byte for byte. The residuals, each with its cause:
43+
44+
| Figure | Difference |
45+
| --- | --- |
46+
| `mean mapping quality` | fourth decimal; 393 of 397 windows match exactly |
47+
| `std coverageData` | fourth decimal, same cause |
48+
| `homopolymer indels` | differs outright, see below |
49+
| coverage histogram and what derives from it | about five reference positions of 40001 sit one deeper |
50+
| `genome_fraction_coverage` | last two digits of the double, Qualimap accumulates per window |
51+
| `insert_size_histogram` | one extra row: Qualimap trims the largest insert from the plotted table while still counting it in the statistics |
52+
53+
Qualimap classifies an indel as a homopolymer indel against a reference context
54+
RustQC does not reconstruct. It reports two polyC indels on the test data, and
55+
no rule derived from the read alone produces them, because the deleted bases
56+
are not in the read. RustQC uses a run-of-four rule instead, so this one figure
57+
will differ.
58+
59+
Qualimap's GC content distribution and duplication rate histogram are not
60+
written at all. The first is computed over a 679-read subsample whose selection
61+
rule is not documented; the second uses a definition that does not match a
62+
read-start-position count. Emitting tables under those names with different
63+
numbers would be worse than leaving them out.
64+
65+
## The HTML report
66+
67+
RustQC writes its own summary page rather than a copy of Qualimap's, which
68+
ships a bundle of images, CSS and JavaScript. It carries the same numbers as
69+
`genome_results.txt`; the raw tables remain the machine-readable source.

src/citations.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,9 @@ pub fn write_dna_citations(
148148
if config.samtools.enabled {
149149
write_citation(&mut w, &SAMTOOLS_DNA)?;
150150
}
151+
if config.qualimap.enabled {
152+
write_citation(&mut w, &QUALIMAP)?;
153+
}
151154
if config.preseq.enabled {
152155
write_citation(&mut w, &PRESEQ)?;
153156
}

src/config.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -988,6 +988,10 @@ pub struct DnaConfig {
988988
#[serde(default)]
989989
pub hs_metrics: HsMetricsConfig,
990990

991+
/// Qualimap bamqc configuration.
992+
#[serde(default)]
993+
pub qualimap: BamqcConfig,
994+
991995
/// preseq lc_extrap library complexity extrapolation configuration.
992996
///
993997
/// Reuses the same type as the `rna` pipeline; the implementation is shared.
@@ -1032,6 +1036,38 @@ impl Default for WgsMetricsConfig {
10321036
}
10331037
}
10341038

1039+
/// Configuration for the Qualimap-compatible bamqc report.
1040+
///
1041+
/// Named apart from the `rna` pipeline's [`QualimapConfig`], which configures
1042+
/// a different Qualimap analysis entirely: gene body coverage rather than
1043+
/// bamqc.
1044+
///
1045+
/// Example:
1046+
/// ```yaml
1047+
/// qualimap:
1048+
/// enabled: true
1049+
/// num_windows: 400
1050+
/// ```
1051+
#[derive(Debug, Deserialize)]
1052+
#[serde(default)]
1053+
pub struct BamqcConfig {
1054+
/// Whether to produce the bamqc outputs. Defaults to true.
1055+
pub enabled: bool,
1056+
/// Target number of windows the reference is split into. The realised
1057+
/// count is usually a little lower, because the window width is rounded up
1058+
/// first.
1059+
pub num_windows: usize,
1060+
}
1061+
1062+
impl Default for BamqcConfig {
1063+
fn default() -> Self {
1064+
Self {
1065+
enabled: true,
1066+
num_windows: 400,
1067+
}
1068+
}
1069+
}
1070+
10351071
/// Configuration for the Picard-compatible GC bias metrics.
10361072
///
10371073
/// Requires a reference FASTA: the analysis bins reference windows by GC.

src/dna/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,6 @@ pub mod hs_metrics;
1010
pub mod insert_size;
1111
pub mod intervals;
1212
pub mod mosdepth;
13+
pub mod qualimap;
14+
pub mod qualimap_output;
1315
pub mod wgs_metrics;

0 commit comments

Comments
 (0)