Skip to content

fix: make dupRadar and Qualimap outputs reproducible run-to-run - #142

Open
BenjaminDEMAILLE wants to merge 1 commit into
seqeralabs:mainfrom
BenjaminDEMAILLE:fix/deterministic-output-ordering
Open

fix: make dupRadar and Qualimap outputs reproducible run-to-run#142
BenjaminDEMAILLE wants to merge 1 commit into
seqeralabs:mainfrom
BenjaminDEMAILLE:fix/deterministic-output-ordering

Conversation

@BenjaminDEMAILLE

@BenjaminDEMAILLE BenjaminDEMAILLE commented Aug 14, 2026

Copy link
Copy Markdown

Fixes #141.

What

Adds a deterministic tie-breaker to four sorts whose input order came from a HashMap.

slice::sort_by is stable, so tied elements keep their input order. When that input is a HashMap iterated into a Vec, the order is randomised per process by RandomState, and it reached the output files.

site symptom
qualimap/output.rs junction motifs which tied 4-mers make the top-11 list, and in what order
qualimap/report.rs junction motifs same list in the HTML report
dupradar/plots.rs scatter dedup <circle> draw order in the SVG, and therefore the PNG bytes
qualimap/output.rs compute_bias which transcripts survive truncate(1000), i.e. the reported bias values

The first three are cosmetic (no number changes). The fourth is the one that can actually move a reported metric; I did not catch it firing in practice, but it is the same bug and worth closing.

Tie-breakers chosen: motif string, pixel coordinate, and the transcript's flat index (assigned in GTF order). flat_idx was carrying #[allow(dead_code)]; it now has a use.

Faithfulness to upstream

Upstream Qualimap orders these lists by Java HashMap iteration, i.e. arbitrarily. A deterministic tie-break is no less faithful than what RustQC does today, and it is reproducible.

Verification

Three consecutive runs of the patched binary on the same 4M-read BAM:

STABLE  qualimap/rnaseq_qc_results.txt
STABLE  dupradar/<sample>_duprateExpDens.svg
STABLE  dupradar/<sample>_duprateExpDens.png

Content is unchanged from main — sorting the old and new files yields identical multisets, only the order of tied entries moved. Every other output file is byte-identical to main (modulo the output directory path that some headers embed).

qualimapReport.html still differs between runs by exactly two lines:

< <td class="column1">Analysis date: </td><td class="column2">2026/08/14 02:25:32</td>
> <td class="column1">Analysis date: </td><td class="column2">2026/08/14 02:26:01</td>

That is the report timestamp and its footer copy — intentional metadata, matching upstream Qualimap. Everything else in the report is now stable.

cargo test --release passes (200 + 12 + 18 + 2). cargo clippy -- -D warnings clean.


🤖 Generated with Claude Code

Part of #143.

Fixes seqeralabs#141.

Three outputs differed between two runs of the same binary on the same
input. In each case a HashMap was collected into a Vec and sorted with a
comparator that has ties; `sort_by` is stable, so tied elements kept
HashMap iteration order, which `RandomState` randomises per process.

- `qualimap/output.rs`: junction motifs sorted by percentage, then
  truncated to the top 11. Many 4-mers tie, so which ones appear and in
  what order varied. Tie-break on the motif.
- `qualimap/report.rs`: the same motif list in the HTML report. Same fix.
- `dupradar/plots.rs`: the density scatter deduplicates points per pixel
  into a HashMap, then sorts by density so dense points draw last. Points
  of equal density came out in a different sequence each run, changing the
  SVG element order and the PNG bytes. Keep the pixel coordinate alongside
  the point and tie-break on it.

Also fixes the same pattern in `compute_bias()`, which had a worse
consequence: the transcript list is sorted by mean coverage and truncated
to the top 1000, so ties at that boundary decided which transcripts fed
the 5'/3' bias calculation. Tie-break on the transcript's flat index,
assigned in GTF order. `flat_idx` is now used rather than dead code.

Upstream Qualimap orders these by Java HashMap iteration, i.e. arbitrarily,
so a deterministic tie-break is no less faithful than the current
behaviour.

Verified with three consecutive runs on a 4M-read BAM: `rnaseq_qc_results.txt`,
the scatter `.svg` and `.png` are now byte-identical across runs. The
contents are unchanged from before — sorting the old and new files gives
identical multisets, only the order of tied entries moved.

`qualimapReport.html` still differs between runs by exactly two lines: the
"Analysis date" timestamp and its copy in the footer. That is intentional
report metadata and matches upstream Qualimap.

`cargo test --release` passes (200 + 12 + 18 + 2).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Three outputs are not reproducible run-to-run (HashMap iteration order reaching output)

1 participant