Skip to content

feat(eval): truth-based variant-representation and methylation-correlation metrics - #20

Open
nh13 wants to merge 9 commits into
mainfrom
nh/eval-variants-meth
Open

feat(eval): truth-based variant-representation and methylation-correlation metrics#20
nh13 wants to merge 9 commits into
mainfrom
nh/eval-variants-meth

Conversation

@nh13

@nh13 nh13 commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Extends holodeck eval to score an aligner's BAM against holodeck's own truth along three axes, all reading only holodeck-produced outputs (golden BAM, truth VCF, cpg-truth bedGraph) — no external variant or methylation callers.

  • Placement (always, <prefix>.eval.txt): unchanged accuracy-by-MAPQ-bin, but --truth now makes the golden BAM the truth source (exact and indel-aware) instead of only the encoded read name. This implements the previously-stubbed --truth flag.
  • Variant representation (--variants truth.vcf + --truth golden.bam, <prefix>.variants.tsv): for every single-base substitution a read should carry — derived from the truth VCF's phased per-haplotype genotypes within the read's golden span — walk the mapped read's CIGAR to the variant position and check whether the observed base matches the alternate allele. Reports the represented fraction with the read's MAPQ and alignment score per substitution class, plus per-read MD/NM concordance against the golden tags. Reads mapped to the wrong locus simply fail to represent their variants, so mismapping is captured without special-casing.
  • Methylation correlation (--cpg-truth truth.bedGraph, <prefix>.meth.tsv): walk each mapped read's Bismark XM:Z string alongside the CIGAR, tally per-CpG methylated/unmethylated calls, and correlate the aligner methylation level against the truth bedGraph (Pearson r + RMSE).

Methylation framing

With --meth, variant results break down by substitution class relative to the read's bisulfite conversion direction (XG, falling back to XR). The C->T cell on a CT-strand read (G->A on GA) is intrinsically confounded with bisulfite conversion and is labelled as such rather than scored as a real signal; the discriminating classes are the mirror (T->C / A->G) and the transversions. A read lacking a conversion-direction tag is counted as unclassified instead of being silently mislabelled.

Structure

The eval command moves into an eval/ module: placement (existing, extracted), cigar (CIGAR geometry helpers), golden (golden-BAM truth index), variants (substitution classifier + per-haplotype expected SNVs + scoring pass), and meth (XM tally + correlation). The two refactor commits (move, extract) are behavior-preserving and worth reading first.

Testing

CIGAR geometry, the substitution classifier, per-haplotype expected-SNV queries, the bedGraph parser, and the Pearson/RMSE math are unit-tested with programmatically built inputs. Two end-to-end integration tests drive the new metrics through real simulate output: one confirms a golden BAM scored as its own mapped BAM represents every variant (and reports MD/NM as NA, since non-methylation golden BAMs carry no such tags), the other confirms golden XM calls correlate strongly with the cpg-truth bedGraph. cargo ci-test (456 tests), cargo ci-lint (clippy pedantic), and cargo fmt --check all pass.

Status

Draft. The metrics are being exercised end-to-end in the bwa-mem3-bench accuracy benchmark before this is marked ready; expect possible small follow-ups from that integration.

Summary by CodeRabbit

  • New Features
    • Expanded holodeck eval to provide placement accuracy plus opt-in variant representation, methylation correlation, and reference-based NM/MD concordance.
    • Added new outputs: *.variants.tsv (per substitution class) and *.meth.tsv (per-CpG correlation), alongside the always-produced *.eval.txt.
  • Bug Fixes
    • Improved bisulfite-aware scoring by validating conversions consistently and computing concordance using a CIGAR-aware, strand-aware edit distance when --reference is set.
  • Documentation
    • Updated README and CHANGELOG with revised flags, required combinations, and example commands.
  • Tests
    • Added integration tests covering perfect variant scoring, NM/MD concordance, and methylation correlation.

@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Note

Reviews paused

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 20620bef-320c-4206-aeed-69c60815c48d

📥 Commits

Reviewing files that changed from the base of the PR and between 94c063e and 6d2fa7f.

📒 Files selected for processing (10)
  • CHANGELOG.md
  • README.md
  • src/commands/eval/cigar.rs
  • src/commands/eval/edits.rs
  • src/commands/eval/golden.rs
  • src/commands/eval/meth.rs
  • src/commands/eval/mod.rs
  • src/commands/eval/placement.rs
  • src/commands/eval/variants.rs
  • tests/test_eval.rs
✅ Files skipped from review due to trivial changes (1)
  • README.md
🚧 Files skipped from review as they are similar to previous changes (8)
  • src/commands/eval/edits.rs
  • src/commands/eval/cigar.rs
  • src/commands/eval/mod.rs
  • src/commands/eval/variants.rs
  • tests/test_eval.rs
  • src/commands/eval/placement.rs
  • src/commands/eval/golden.rs
  • src/commands/eval/meth.rs

📝 Walkthrough

Walkthrough

holodeck eval now runs placement scoring always, with optional variant-representation and methylation-correlation modes. Golden BAM truth, optional reference-based NM/MD concordance, and new TSV outputs are wired through the command, docs, and tests.

Changes

Eval scoring stack

Layer / File(s) Summary
CLI wiring and docs
src/commands/eval/mod.rs, README.md, CHANGELOG.md
Eval now validates --variants/--truth, dispatches placement, variant, and methylation runs, and the docs describe the new flags and output files.
Golden truth, CIGAR, and genomic edits
src/commands/eval/cigar.rs, src/commands/eval/golden.rs, src/commands/eval/edits.rs
CIGAR helpers, golden BAM truth loading, and bisulfite-aware edit recomputation provide the per-read geometry and reference concordance inputs used by eval.
Placement accuracy scoring
src/commands/eval/placement.rs
Primary alignments are binned by MAPQ, compared against truth positions within wiggle, and written to <prefix>.eval.txt.
Variant truth and reporting
src/commands/eval/variants.rs, tests/test_eval.rs
Variant truth is loaded from VCF, substitutions are classified and scored against mapped reads, and integration tests cover variant representation and NM/MD outputs.
Methylation correlation
src/commands/eval/meth.rs, tests/test_eval.rs
XM-tag CpG tallies are joined with CpG-truth bedGraph values to write methylation correlation output, with integration coverage for the reported correlation.

Sequence Diagram(s)

sequenceDiagram
  participant Eval
  participant GoldenLoad as "golden::load"
  participant PlacementRun as "placement::run"
  participant VariantsRun as "variants::run"
  participant RefCache
  participant MethRun as "meth::run"

  opt `--truth`
    Eval->>GoldenLoad: load golden BAM
  end
  Eval->>PlacementRun: run placement scoring
  opt `--variants`
    Eval->>VariantsRun: run variant scoring
    opt `--reference`
      VariantsRun->>RefCache: contig lookups
    end
  end
  opt `--cpg-truth`
    Eval->>MethRun: run methylation correlation
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • fg-labs/holodeck#4: Shares the encoded-read-name truth-position handling that the placement path still falls back to.
  • fg-labs/holodeck#11: Introduces the methylation-truth and golden-BAM tag conventions consumed by the new methylation and bisulfite-aware eval paths.

Suggested reviewers

  • tfenne

Poem

I hop through BAMs with twitchy nose,
and count the truths in tidy rows.
From golden hops to CpG glows,
my whiskers grin as eval grows. 🐰

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: new truth-based variant-representation and methylation-correlation metrics in eval.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@nh13
nh13 force-pushed the nh/eval-variants-meth branch 2 times, most recently from e632077 to 94c063e Compare June 26, 2026 21:59
@nh13
nh13 marked this pull request as ready for review June 26, 2026 22:01
@nh13
nh13 requested a review from tfenne June 26, 2026 22:01
@nh13

nh13 commented Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (1)
src/commands/eval/meth.rs (1)

218-218: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add //! docs to the test module.

mod tests is a module, so it should carry the same inner-module documentation as the rest of the Rust modules in this PR. As per coding guidelines, **/*.rs: Add module-level //! documentation on all modules.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/eval/meth.rs` at line 218, The test module declaration is
missing inner-module documentation. Add a `//!` doc comment to the `mod tests`
module so it follows the same module-level documentation pattern used elsewhere
in this Rust file; place the documentation directly above the `mod tests` block
and keep it focused on describing the purpose of the tests module.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@README.md`:
- Around line 454-466: Update the `--truth` row in the options table so it
matches what `eval` actually consumes from the golden BAM via `golden::load`:
describe it as providing per-read true span, sequence, and bisulfite strand, and
remove the claims about haplotype and MD/NM tags. Keep the wording consistent
with the `eval`/`mod.rs` behavior and mention that MD/NM concordance is derived
from the reference when `--reference` is supplied.

In `@src/commands/eval/edits.rs`:
- Around line 107-130: RefCache currently reuses a single SmallRng seeded with
0, so ambiguity resolution in load_contig can vary based on contig access order.
Update RefCache::contig to create a fresh deterministic SmallRng for each first
load, seeding it from a stable FNV-1a hash of the contig name before calling
Fasta::load_contig. Keep the existing caching behavior, but remove the
order-dependent shared RNG state in RefCache::new and RefCache::contig.

In `@src/commands/eval/meth.rs`:
- Around line 145-159: The bedGraph parsing in meth.rs is ignoring the end
column in the row tuple and treating every record as a single CpG at start,
which allows merged or malformed intervals to slip through. Update the parsing
logic in the bedGraph reader to validate the end value alongside start, and
reject rows unless the interval length is exactly one CpG as expected before
inserting into truth. Use the existing parsing flow around the start0/rate
conversion and the truth.entry insertion to keep the check localized and surface
bad input early.

In `@src/commands/eval/mod.rs`:
- Around line 66-69: Update the doc comment for the meth flag in the eval
command so it no longer says it interprets reads as bisulfite/EM-seq; instead,
describe that --meth only breaks --variants results down by bisulfite
substitution class (conversion, mirror, transversion, other), while the actual
bisulfite/EM-seq context comes from the aligner output or golden BAM tags. Keep
the clarification attached to the meth field in src/commands/eval/mod.rs so the
CLI help matches behavior.

In `@src/commands/eval/placement.rs`:
- Around line 176-187: Update the mapq_bin function so MAPQ 255 is treated as
unknown rather than falling into the 60+ bucket. Add an explicit 255 case in the
match, and either return a distinct NA/unknown bin key or handle it separately
where the MAPQ buckets are aggregated in the placement evaluation flow so it
does not get counted with high-confidence alignments.

In `@src/commands/eval/variants.rs`:
- Line 32: The variant classification logic in eval variants is pulling class
meth labels from mapped BAM tags instead of the truth alignment, which can
misclassify variants as unclassified. Update the classification path in the eval
variants code to use truth_aln.conv_dir, matching the reference-based NM/MD
handling, and remove the dependence on XG/XR tags read from the mapped record in
the affected classification block.

---

Nitpick comments:
In `@src/commands/eval/meth.rs`:
- Line 218: The test module declaration is missing inner-module documentation.
Add a `//!` doc comment to the `mod tests` module so it follows the same
module-level documentation pattern used elsewhere in this Rust file; place the
documentation directly above the `mod tests` block and keep it focused on
describing the purpose of the tests module.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4526ef3f-cecb-4df5-8689-8cf56affb342

📥 Commits

Reviewing files that changed from the base of the PR and between 04632a3 and 94c063e.

📒 Files selected for processing (11)
  • CHANGELOG.md
  • README.md
  • src/commands/eval.rs
  • src/commands/eval/cigar.rs
  • src/commands/eval/edits.rs
  • src/commands/eval/golden.rs
  • src/commands/eval/meth.rs
  • src/commands/eval/mod.rs
  • src/commands/eval/placement.rs
  • src/commands/eval/variants.rs
  • tests/test_eval.rs
💤 Files with no reviewable changes (1)
  • src/commands/eval.rs

Comment thread README.md
Comment thread src/commands/eval/edits.rs Outdated
Comment thread src/commands/eval/meth.rs Outdated
Comment thread src/commands/eval/mod.rs Outdated
Comment thread src/commands/eval/placement.rs Outdated
Comment thread src/commands/eval/variants.rs Outdated
nh13 added 8 commits June 26, 2026 16:15
The eval command is about to grow optional truth-aware metrics (variant
representation, methylation-level correlation, golden-BAM placement), so the
existing MAPQ-binned placement scoring moves out of the command struct into a
focused `placement` submodule. This leaves `eval/mod.rs` as a thin orchestrator
over the shared `Eval` options and gives each future metric its own file.

Pure code movement: the placement loop, MAPQ binning, and TSV row formatting
are unchanged, and `placement::run` reproduces the previous `execute` behavior.
The only edit is renaming the final log line to "Placement results written to"
so it reads correctly once sibling metrics write their own tables.
`holodeck eval --variants truth.vcf --truth golden.bam` adds an accuracy axis
beyond placement: for every simulated substitution a read should carry, does
the aligned read actually represent the alternate base, and with what MAPQ and
alignment score?

Truth is taken entirely from holodeck's own outputs. The golden BAM supplies
each read's true span and source haplotype (hp:i); the truth VCF's phased
genotypes say which single-base substitutions that haplotype carries within the
span. For each expected substitution the pass walks the *mapped* read's CIGAR
to the variant's reference position and compares the observed base to the
alternate allele. Reads mapped to the wrong locus simply fail to represent
their variants, so mismapping is captured without special-casing. Per-read
MD:Z / NM:i tags are compared against the golden tags to report tag-level
concordance over variant-bearing reads. Results land in `<prefix>.variants.tsv`.

With `--meth`, results break down by substitution class relative to the read's
bisulfite conversion direction (XG, falling back to XR). The C->T cell on a
CT-strand read (G->A on GA) is intrinsically confounded with conversion and is
labelled as such rather than scored as a real signal; the discriminating
classes are the mirror (T->C / A->G) and the transversions. This is the axis on
which a methylation-aware scoring mode should match the genomic truth without
over- or under-penalizing.

Adds eval submodules cigar (CIGAR geometry, unit-tested), golden (truth index),
and variants (classifier, per-haplotype expected SNVs, the scoring pass). The
classifier and expected-SNV queries are covered by unit tests with
programmatically built records; the end-to-end pass is exercised by a later
integration test.
`holodeck eval --cpg-truth truth.bedGraph` reports how well an aligner's
methylation calls reproduce the simulated truth. For every mapped read the
Bismark `XM:Z` string is walked alongside the CIGAR, tallying each `Z`/`z` CpG
call at its reference position; the aligner methylation level at a site
(`n_methylated / coverage`) is then correlated against the truth level
(`rate / 100`) from the bedGraph that `simulate --cpg-truth-bedgraph` writes.
Pearson r and RMSE over the shared, covered CpG sites land in
`<prefix>.meth.tsv`, with `NA` reported where a statistic is undefined.

The XM walk reuses the CIGAR geometry helpers; a new `for_each_aligned` visitor
yields (read offset, reference position) for each aligned base so insertions
and soft-clips never misplace a call onto the reference. Tallying, bedGraph
parsing, the Pearson/RMSE math, and the join are unit-tested with
programmatically built inputs.
`--truth` previously only warned that it was unimplemented and fell back to
read names. It now supplies the placement truth too: when a golden BAM is
given, each read's true contig and start come from its golden record rather
than the encoded name. This is exact and indel-aware (the golden alignment's
true start already reflects haplotype indels), and it lets placement be scored
for any aligner output whose reads carry holodeck names — even after a tool
rewrites or trims them — as long as the golden BAM is present.

The golden BAM is loaded once in `execute` and shared between the placement and
variant passes, so `--truth` and `--variants` together read it a single time.
With no `--truth`, placement is unchanged and still parses encoded read names.
Two integration tests drive the new metrics through real holodeck output
rather than hand-built BAMs. The first simulates single-end reads carrying
homozygous-alt SNVs with a golden BAM, then evaluates the golden BAM as the
mapped BAM: every variant-bearing read is perfectly placed, so all expected
substitutions must be represented, and — since a non-methylation golden BAM
carries no MD/NM tags — MD/NM concordance must report NA rather than 0%. The
second simulates EM-seq reads with a methylation golden BAM and a cpg-truth
bedGraph and confirms the golden XM calls correlate strongly with the truth.

Documents the expanded eval surface (the --truth / --variants / --cpg-truth /
--meth options and the .variants.tsv / .meth.tsv outputs) in the README, and
records the feature under CHANGELOG [Unreleased].
…VCF phasing

Variant representation inferred which haplotype carried the alt from the truth
VCF genotype (`alt_by_hap[read's haplotype]`), assigning an unphased het alt to
haplotype 1 deterministically. But `simulate` assigns unphased genotypes to
haplotypes by a random permutation, so eval's guess disagreed with the
simulator on roughly half of heterozygous sites — it checked the reference-copy
reads for an alt that was actually on the other copy, scoring them as misses.
The effect was large: a byte-identical golden-as-mapped run scored ~74%
representation instead of 100%, and every aligner inherited the same artifact.

Resolve the allele a read truly carries from the golden read's own sequence
instead. The golden BAM is the per-read oracle: at each truth substitution site
within a read's true span, the golden read's base is exactly what the simulator
placed on the copy that read was sequenced from. A read showing the reference
base is (correctly) not expected to carry the alt. This makes representation
correct whether or not the truth VCF is phased — golden-as-mapped now scores
100%, and a real aligner's shortfall reflects only its actual mismapping.

`GoldenInfo` carries the read sequence and CIGAR (new `base_at` resolver); the
now-unused `hp:i` haplotype field is dropped from the eval-side record.
`--meth` methylation correlation aborted the whole eval when a mapped BAM had
no XM tags. But a plain bisulfite aligner (e.g. bwameth) emits no per-base
methylation calls at all — calling is a separate extractor (MethylDackel) step
— so its BAM legitimately has no XM, and bailing made it impossible to score
such an aligner's placement and variant representation alongside callers that
do emit XM.

Warn and report NA methylation correlation (n_cpg 0) instead of erroring, so
the placement and variant-representation axes still complete for these BAMs.
NM/MD concordance compared the aligner's raw NM:i/MD:Z tags against the golden
tags. Those tags are convention-dependent for bisulfite data: an aligner may
score edits against the original 4-letter reference (every unmethylated C->T is
a "mismatch"), against a C->T-converted reference (conversions match), or in a
bisulfite-aware convention. Two correctly-placed reads therefore disagree on
their tags purely by convention — D3 (scoring vs the original reference) matched
the golden ~1% of the time while bwameth (vs the converted reference) matched
~75%, measuring which convention each picked rather than alignment quality.

Add `--reference <fasta>` and recompute concordance as a convention-independent
genomic edit distance: walk each read against the reference, exclude bisulfite
conversions using the read's TRUE strand (from the golden truth, so it works
for aligners like bwameth that emit no XG), and compare the resulting
non-conversion mismatch + indel profiles of the aligned and golden reads. D3
and bwameth now both land at ~96-100% on the same scale. The new `edits` module
holds the genomic-edit walk and an on-demand reference-contig cache; the golden
record now carries its true conversion strand instead of the raw NM/MD tags.
Without `--reference`, NM/MD concordance is reported as NA.
@nh13
nh13 force-pushed the nh/eval-variants-meth branch 2 times, most recently from 94c063e to 6d2fa7f Compare June 26, 2026 23:17
@nh13

nh13 commented Jun 27, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

nh13 added a commit to fg-labs/bwa-mem3-bench that referenced this pull request Jun 27, 2026
The accuracy benchmark needs holodeck to generate simulated truth datasets and
to score aligner output via `holodeck eval`. holodeck is a public fg-labs Rust
crate, so the builder stage cargo-installs it from a pinned git ref rather than
vendoring it (the minibwa submodule pattern exists only because lh3/minibwa is
private). It is built with the +stable toolchain already installed for tricord
— the +toolchain override takes precedence over holodeck's rust-toolchain.toml
pin, so the image isn't coupled to holodeck's exact channel. holodeck's `built`
build-dependency links libgit2, so libgit2-dev joins the builder apt set
(build-only; the resulting binary does not link it).

HOLODECK_REPO / HOLODECK_REF join docker/build-arg-defaults.env, exposed via
bwa_mem3_bench.holodeck_ref() (factored alongside minibwa_sha() onto a shared
_build_arg_default helper) and passed through `cli build` with an optional
--holodeck-ref override. The ref is pinned to the fg-labs/holodeck#20 draft-PR
SHA while that PR is validated end-to-end here; it moves to a tagged release
once holodeck merges.
nh13 added a commit to fg-labs/bwa-mem3-bench that referenced this pull request Jun 27, 2026
The accuracy benchmark needs holodeck to generate simulated truth datasets and
to score aligner output via `holodeck eval`. holodeck is a public fg-labs Rust
crate, so the builder stage cargo-installs it from a pinned git ref rather than
vendoring it (the minibwa submodule pattern exists only because lh3/minibwa is
private). It is built with the +stable toolchain already installed for tricord
— the +toolchain override takes precedence over holodeck's rust-toolchain.toml
pin, so the image isn't coupled to holodeck's exact channel. holodeck's `built`
build-dependency links libgit2, so libgit2-dev joins the builder apt set
(build-only; the resulting binary does not link it).

HOLODECK_REPO / HOLODECK_REF join docker/build-arg-defaults.env, exposed via
bwa_mem3_bench.holodeck_ref() (factored alongside minibwa_sha() onto a shared
_build_arg_default helper) and passed through `cli build` with an optional
--holodeck-ref override. The ref is pinned to the fg-labs/holodeck#20 draft-PR
SHA while that PR is validated end-to-end here; it moves to a tagged release
once holodeck merges.
nh13 added a commit to fg-labs/bwa-mem3-bench that referenced this pull request Jun 27, 2026
The accuracy benchmark needs holodeck to generate simulated truth datasets and
to score aligner output via `holodeck eval`. holodeck is a public fg-labs Rust
crate, so the builder stage cargo-installs it from a pinned git ref rather than
vendoring it (the minibwa submodule pattern exists only because lh3/minibwa is
private). It is built with the +stable toolchain already installed for tricord
— the +toolchain override takes precedence over holodeck's rust-toolchain.toml
pin, so the image isn't coupled to holodeck's exact channel. holodeck's `built`
build-dependency links libgit2, so libgit2-dev joins the builder apt set
(build-only; the resulting binary does not link it).

HOLODECK_REPO / HOLODECK_REF join docker/build-arg-defaults.env, exposed via
bwa_mem3_bench.holodeck_ref() (factored alongside minibwa_sha() onto a shared
_build_arg_default helper) and passed through `cli build` with an optional
--holodeck-ref override. The ref is pinned to the fg-labs/holodeck#20 draft-PR
SHA while that PR is validated end-to-end here; it moves to a tagged release
once holodeck merges.
nh13 added a commit to fg-labs/bwa-mem3-bench that referenced this pull request Jun 28, 2026
* feat(docker): install holodeck for the truth-based accuracy benchmark

The accuracy benchmark needs holodeck to generate simulated truth datasets and
to score aligner output via `holodeck eval`. holodeck is a public fg-labs Rust
crate, so the builder stage cargo-installs it from a pinned git ref rather than
vendoring it (the minibwa submodule pattern exists only because lh3/minibwa is
private). It is built with the +stable toolchain already installed for tricord
— the +toolchain override takes precedence over holodeck's rust-toolchain.toml
pin, so the image isn't coupled to holodeck's exact channel. holodeck's `built`
build-dependency links libgit2, so libgit2-dev joins the builder apt set
(build-only; the resulting binary does not link it).

HOLODECK_REPO / HOLODECK_REF join docker/build-arg-defaults.env, exposed via
bwa_mem3_bench.holodeck_ref() (factored alongside minibwa_sha() onto a shared
_build_arg_default helper) and passed through `cli build` with an optional
--holodeck-ref override. The ref is pinned to the fg-labs/holodeck#20 draft-PR
SHA while that PR is validated end-to-end here; it moves to a tagged release
once holodeck merges.

* feat(sim): deterministic holodeck truth-dataset generator

scripts/gen_holodeck_dataset.sh produces one accuracy-benchmark truth dataset
from a reference FASTA: mutate -> [methylate] -> simulate, fully seeded. It
emits the truth VCF (the SNVs eval scores), the simulated FASTQs, the golden
BAM (eval's --truth), and — for the meth kinds — the coverage-weighted
cpg-truth bedGraph (eval's --cpg-truth).

Four kinds cover the matrix: wgs-place / meth-place sweep the genome at low
coverage for placement and MAPQ calibration; wgs-vars / meth-vars put depth
over a target BED to exercise variant representation. The reference is always
full hg38 — depth is bounded by the BED, never by a reduced reference, so
off-target mismapping stays observable. Verified with shellcheck.

The generated datasets are large and live in S3 (staged under each sim sample's
source prefix), never committed; this is the reproducer that builds them.

* feat(workflow): sim sample class + eval.smk accuracy rule + targets

Wire the truth-based accuracy axis into the workflow. A new `truth: true`
flag on a sample marks its S3 source prefix as also holding the holodeck
truth artifacts (golden.bam, truth.vcf, and cpg-truth.bedGraph for meth);
the new eval_accuracy rule grades an aligner's BAM directly against that
truth via `holodeck eval`, emitting placement (.eval.txt), per-read variant
representation (.variants.tsv), and methylation-level correlation (.meth.tsv)
in one invocation — no variant caller or methylation extractor in the path.

The eval rule's `tool` wildcard routes to each arm's BAM cache (fg-labs under
runs/, the bwa-mem2/bwameth baseline under baseline/, minibwa under minibwa/).
The `--meth-scoring genomic` D3 arm is expressed as a separate sample sharing
its FASTQs and truth with its collapsed sibling, since the align rules key
flags off the sample name; those `*-genomic` samples are fg-labs-arm-only.

Accuracy is a property of the aligner build and is ~arch-invariant, so the
accuracy targets pin each chemistry to a single arch (non-meth -> c6a, meth
-> m7i) instead of sweeping SIMD tiers like the timing matrix. The new
`accuracy` (full hg38) and `accuracy_smoke` (chr22-slice) targets enumerate
the arm matrix per dataset. Truth samples are excluded from `rule all` and
`baseline_all` — they have no upstream-concordance question.

* feat(storage): accuracy table + ingest_accuracy

Add a v4 schema migration introducing the `accuracy` table — truth-based
holodeck eval results, one row per (run, sim-sample, arch, rep, aligner arm).
Distinct from `comparisons` (tool-vs-tool agreement), this is graded against
simulation truth: placement + MAPQ calibration (the per-bin table as JSON plus
the ALL-row rates as headline columns), per-read variant representation (the
per-class accumulators as JSON plus the MD/NM concordance footers), and
methylation-level correlation (NULL for non-meth). All arms of a run share the
run's fg_labs_sha — the eval outputs live under runs/<sha>/ — so the `tool`
column disambiguates fg-labs / baseline / minibwa. Being a new table, the
migration needs no ALTER: connect()'s unconditional executescript creates it.

ingest_accuracy walks runs/<sha>/<sample>/<arch>/rep-N/eval/<tool>.{eval.txt,
variants.tsv,meth.tsv}, parsing holodeck's exact TSV formats (the literal "NA"
maps to NULL; the empty .meth.tsv placeholder the eval rule writes for non-meth
samples parses to no correlation). cli collect calls it after ingest_run, since
accuracy outputs share the runs/ tree.

* feat(report): bench accuracy (placement + variant-class honesty + meth-r)

Add the `bench accuracy` report over the `accuracy` table. It renders three
markdown sections per sim dataset, one row per aligner arm: placement + MAPQ
calibration (the cross-tool bwa-mem3 / minibwa / baseline comparison),
variant representation + methylation correlation (variant-bearing reads, MD/NM
concordance, per-CpG Pearson r / RMSE), and per-class AS/MAPQ honesty — the
genomic-vs-collapsed headline, with the conversion-direction class flagged
`confounded` since no mode resolves it from a single read.

Headline metrics are averaged across reps; the per-class table takes the
lowest rep per cell (the JSON blobs aren't meaningfully averaged). NULL cells
(non-meth MD/NM, non-meth methylation, NA mean_as) render as an em-dash. Wired
as `cli bench accuracy --fg-labs-sha <sha> [--out file.md]`, mirroring
`bench speedup`.

* chore(accuracy): pin holodeck #21, align full -place to ~0.5x, add chr22 vars target

- HOLODECK_REF -> 35e4b47 (fg-labs/holodeck#21, per-contig parallel methylate,
  stacked on #20 so it carries the truth-aware eval work too).
- gen_holodeck_dataset.sh: -place coverage 1 -> 0.5 to hit the spec's ~5M
  read-pair genome-wide placement scale (no -place smoke uses this script).
- scripts/sim-targets/chr22.bed: whole-chr22 target for the full -vars datasets
  (single region, ~34Mb non-N => ~3.9M pairs at 30x; larger than the smoke's
  3x100kb).

* docs(accuracy): committed wrapper + runbook for simulated truth datasets

scripts/gen_all_sim_datasets.sh is the single source of truth for the dataset
matrix the accuracy benchmark consumes (name -> kind -> coverage -> target ->
chemistry), driving the per-dataset gen_holodeck_dataset.sh and optionally
staging the canonical files to S3. docs/data-setup.md gains a 'Simulated truth
datasets (holodeck)' section documenting the matrix, canonical filenames, the
S3 layout, and seed/holodeck-SHA provenance for reproducibility.

* chore(accuracy): bump holodeck pin to efae5ce (PR #21 meth perf fix)

PR #21 (per-contig parallel methylate) also carries efae5ce, an O(1) per-CpG
variant-lookup fix that makes methylate --vcf and em-seq simulate fast on a
whole genome (methylate --vcf 43 min -> 31 s; em-seq simulate chr1-stall ->
~9 s/contig; output byte-identical). Pin HOLODECK_REF to that tip so the bench
image and eval use it.

* feat(accuracy): add minibwa as the 4th meth arm (bisulfite-aware, --meth)

The meth accuracy matrix had three arms (bwameth, D3-collapsed, D3-genomic);
add minibwa so all four aligners are graded on the same meth truth. minibwa has
a directional BS-seq mode: `map --meth` maps the EM-seq reads bisulfite-aware
(read1 C->T, read2 G->A) against the `.meth.mbw` index built by
`index --meth` (which lives alongside the plain DNA index, not under
hg38-meth, so we strip the -meth suffix to reach it and stage .l2b + .meth.mbw).
It is scored on placement + variant representation; methylation-level
correlation is NA (minibwa emits no XM tags even in --meth mode — same as the
bwameth arm; only bwa-mem3 calls methylation natively).

_accuracy_targets now requests the minibwa arm for every primary (non-genomic)
sim sample, meth included; the genomic D3 sibling stays fg-labs-only (its
baseline/minibwa arms would be byte-identical to the collapsed sample's).
minibwa runs on the meth arch (m7i) so the 4-way wall-time comparison is
arch-matched. The .meth.mbw sidecar is built once (index --meth, in-memory
path) and staged to references/hg38/.
@nh13

nh13 commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai pause

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
✅ Action performed

Reviews paused.

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.

2 participants