Repository navigation
Add haplotype DMR (differential methylation region) calling - #204
Conversation
The module doesn't exist in nf-core/modules yet; this vendors it from a PR open against nf-core/modules (nf-core/modules#13021, with its own companion test-data PR nf-core/test-datasets#2289 for the bedmethyl fixture). modules.json records the source as YannVRB/modules.git@add-modkit-dmr rather than the canonical nf-core/modules.git, matching what `nf-core modules install --git-remote` produces for an unmerged module -- to be re-pointed at the canonical repo/branch/sha (via `nf-core modules update`) once that PR merges. Thin wrapper around `modkit dmr pair`: compares methylation between a pair of bgzip+tabix bedMethyl inputs over a set of regions and reports differentially methylated regions. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wit78XR11V9AMAgY7gGrMT
--skip_dmr (default false, only takes effect when --modkit_phased is also set). dmr_cpg_islands_bed/dmr_gencode_gene_bed per genome in igenomes.config, both genome-wide, pre-cleaned (bin column stripped from UCSC's cpgIslandExt dump; gene-level BED derived from GENCODE/CAT-Liftoff GTFs) and chr-named consistently -- see the add-dmr-reference-fixtures branch on IntGenomicsLab/test-datasets (pending PR) for full provenance. Temporarily points at the YannVRB fork branch, to be re-pointed at IntGenomicsLab/test-datasets/main/... once that PR merges (marked with TODOs). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wit78XR11V9AMAgY7gGrMT
…pendencies Wires the vendored modkit/dmr module into a new subworkflows/local/dmr.nf: MODKIT_PILEUP's phased hp1/hp2 bedMethyl -> HTSLIB_BGZIPTABIX -> CpG-island dual-haplotype-coverage restriction (new local DMR_HAPLOTYPE_REGIONS module, porting NCP's dmr_calling.sh pattern) -> MODKIT_DMR -> nearest-gene annotation via bedtools closest (new local DMR_NEAREST_GENE module). Runs automatically whenever --modkit_phased is set, gated by a new --skip_dmr. Adds GRCh38/CHM13 dmr_cpg_islands_bed/dmr_gencode_gene_bed igenomes.config entries (currently pointing at the IntGenomicsLab/test-datasets fork branch pending that PR's merge) and vendors three canonical nf-core/modules (htslib/bgziptabix, bedtools/intersect, bedtools/closest) plus the not-yet-merged modkit/dmr module. Fixes found via local testing (not committed as a pipeline-level nf-test yet -- a properly-sourced, public methylation-tagged fixture is still needed; none of lrsomatic's existing CI BAMs carry any MM/ML tags): - HTSLIB_BGZIPTABIX output filename collision between a sample's hp1/hp2 outputs -- ext.prefix now includes meta.haplotype. - DMR_NEAREST_GENE's `sort -k1,1V` isn't supported by BusyBox sort (bundled in the bedtools:2.31.1 biocontainer, which ships a coreutils-minimal sort) -- switched to plain `-k1,1 -k2,2n`. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wit78XR11V9AMAgY7gGrMT
Cosmetic only -- IntGenomicsLab/test-datasets#4 (add-dmr-reference-fixtures) is open but not yet merged, so the URLs themselves are unchanged. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wit78XR11V9AMAgY7gGrMT
…asets IntGenomicsLab/test-datasets#4 merged -- dmr_cpg_islands_bed/dmr_gencode_gene_bed for both GRCh38 and CHM13 now point at IntGenomicsLab/test-datasets/main instead of the YannVRB fork branch. Verified all four URLs resolve (HTTP 200) before committing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wit78XR11V9AMAgY7gGrMT
ljwharbers
left a comment
There was a problem hiding this comment.
Thanks for this, @YannVRB. Haplotype DMR is a good fit for --modkit_phased, and the channel wiring holds up. I checked that the _hp1/_hp2.bed.gz selection matches MODKIT_PILEUP's phased output names, that the haplotype tag is added to and then stripped from meta, and that the join/multiMap afterwards is correct. I also downloaded all four reference BEDs. They use chr naming, and the GENCODE files are sorted lexicographically, which matches the sort -k1,1 -k2,2n in DMR_NEAREST_GENE. Thanks also for re-pointing the reference BEDs at IntGenomicsLab/test-datasets/main in a39317d. All four URLs resolve.
I'm requesting changes for the items below. The main problem is that the step runs by default whenever --modkit_phased is set, yet CI never runs it and it can crash on inputs that currently work.
Blocking
1. CI never ran any of the new code, and the vendored modkit/dmr test can't pass.
nf-test-changes found the modkit/dmr, htslib/bgziptabix, bedtools/closest and bedtools/intersect tests, but the PR run filters on --tag small, which drops all four. That's why CI is green: none of them ran. You closed nf-core/test-datasets#2289 in favour of generating the fixture in-test, which works upstream (nf-core/modules#13021 at d886465 now runs a setup { run("MODKIT_PILEUP") } on test.sorted.phased.bam). But the copy vendored here is still pinned at 9d93dfa, and it reads genomics/homo_sapiens/nanopore/bedmethyl/test_hp1.bed.gz, which returns 404. Please re-vendor from the current #13021 head and update the git_sha in modules.json. Since manascripts has just asked for changes upstream, the best time is after you've addressed that. Their point, that the log output isn't deterministic, matches the test.log md5 I was going to raise. Asserting process.out.log.size() == 1 and snapshotting the other outputs, as they suggest, fixes it in both places.
2. We can have a pipeline-level test now: a public phased, methylation-tagged fixture already exists.
genomics/homo_sapiens/nanopore/bam/test.sorted.phased.bam is on nf-core/test-datasets, and this repo already uses it in the MODKIT_PILEUP --phased --modified-bases 5mC 5hmC module test. Your updated upstream module test already shows the pattern: a setup { run("MODKIT_PILEUP") } on that BAM. A subworkflows/local/dmr.nf nf-test that does the same, with a small chr22 CpG-island BED and a gene BED, tagged small, would cover the whole chain in PR CI. I'd rather have that in this PR than as a follow-up.
3. With --modkit_phased, any genome other than GRCh38/CHM13 hits a hard failure.
getGenomeAttribute('dmr_cpg_islands_bed') returns null for a custom --fasta/--genome, and file(null, checkIfExists: true) then fails. Users who never asked for DMR get this, because the step is on by default. The two params also aren't declared in nextflow.config or nextflow_schema.json, so users can't point to their own BEDs. Suggested fix: declare dmr_cpg_islands_bed / dmr_gencode_gene_bed as params with schema entries, let an explicit value override the genome default, and skip DMR with a log.warn (or give a clear error) when neither is available.
4. Default modkit_args vs DMR on dual-modification data.
Your follow-up notes say the default --cpg --modified-bases 5mC is incompatible with modkit dmr pair on real 5mC+5hmC SUP data. If that's true, most --modkit_phased runs on standard ONT data would now fail, or silently produce something wrong, unless the user knows to pass --skip_dmr. What exactly goes wrong, and which modkit_args did your end-to-end run use? I think this should be fixed in this PR, for example by deriving the MODKIT_DMR args from the pileup args. The alternative is to make DMR opt-in until it's fixed.
Should fix
modules.jsonstill points atYannVRB/modules@add-modkit-dmr. Vendoring ahead of nf-core/modules#13021 is fine. Please just keep thegit_shain sync with that PR (see 1) and switch it to nf-core once it merges.- The vendored
bedtools/closestandbedtools/intersectare never used. They add about 790 lines, including tests and snapshots. Either useBEDTOOLS_CLOSESTin place of the local DMR_NEAREST_GENE, which is onlysortplusbedtools closest, or remove both modules and theirmodules.jsonentries. - DMR_HAPLOTYPE_REGIONS unpacks and sorts whole-genome bedMethyl twice (see the inline comment).
- Docs:
docs/usage.md(--skip_dmrand the reference BEDs),docs/output.md(methylation/<type>/dmr/*.nearestGene.bedand its columns: 17 modkit dmr columns, then the gene BED6, then the distance), and a CHANGELOG entry.
Nits
-D ain DMR_NEAREST_GENE (see inline comment).- MODKIT_DMR uses the 0.6.1 biocontainer, while MODKIT_PILEUP uses the patched 0.6.4 image. That's harmless, since DMR only reads bedMethyl, but a one-line comment would stop someone from "aligning" them later.
- MODKIT_DMR gets
--refwithout a.fai. Please check whether modkit re-indexes the 3 GB FASTA in every task. If it does, passfaithrough. - The GENCODE BEDs contain duplicate records at identical coordinates (e.g.
hsa-mir-1253/MIR1253) and some bareENSG…names. With-t first, which of the duplicates is reported depends on file order. - The checklist mentions a "2.14.1 linter bug", but this repo is on template 4.1.0 and the CI
nf-corelint job passed. You can probably tick that box. - Consider publishing the MODKIT_DMR log, for debugging.
| MODKIT_PILEUP.out.bedgz, | ||
| ch_fasta, | ||
| ch_fai, | ||
| file(params.dmr_cpg_islands_bed, checkIfExists: true), |
There was a problem hiding this comment.
For any genome that isn't GRCh38/CHM13 (custom --fasta/--genome), params.dmr_cpg_islands_bed is null here, so file(null, checkIfExists: true) fails the run for every --modkit_phased user. Can you declare both params in nextflow.config and the schema, let an explicit value override the genome default, and skip DMR with a log.warn (or raise a clear error) when neither is set?
| zcat ${hp1_bedmethyl} | awk 'BEGIN{OFS="\\t"}{print \$1,\$2,\$3}' | sort -k1,1 -k2,2n -u > hp1.cpg.bed | ||
| zcat ${hp2_bedmethyl} | awk 'BEGIN{OFS="\\t"}{print \$1,\$2,\$3}' | sort -k1,1 -k2,2n -u > hp2.cpg.bed | ||
|
|
||
| bedtools intersect -u -a ${cpg_islands_bed} -b hp1.cpg.bed | sort -k1,1 -k2,2n > islands.hp1.bed | ||
| bedtools intersect -u -a ${cpg_islands_bed} -b hp2.cpg.bed | sort -k1,1 -k2,2n > islands.hp2.bed | ||
| bedtools intersect -u -a islands.hp1.bed -b islands.hp2.bed | bedtools sort -g genome.txt -i - > ${prefix}.regions.bed |
There was a problem hiding this comment.
On a whole-genome run this zcat | awk | sort -us tens of millions of CpG rows per haplotype. As the PR notes, the container ships BusyBox sort, which sorts in memory, and the process is process_single (6 GB). bedtools intersect -u doesn't need sorted input, and the staged .tbi files are never used. tabix -R ${cpg_islands_bed} hp1.bed.gz (islands sorted), or bedtools intersect -sorted -g genome.txt -u -a islands -b hp1.bed.gz, would stream the data instead of loading it all into memory.
Also, "covered" currently means at least one bedMethyl row in each haplotype, at any depth. Should there be a minimum valid coverage (column 10)?
| task.ext.when == null || task.ext.when | ||
|
|
||
| script: | ||
| def args = task.ext.args ?: '-D a -t first' |
There was a problem hiding this comment.
The DMR regions come from unstranded CpG islands, so modkit's column 6 (strand) is ., and -D a doesn't do anything meaningful. -D b, which reports distance relative to the gene's strand so negative means upstream of the gene, is probably what you want. Please document the sign convention in output.md.
| "git_sha": "feef37435aea56816adf4b3bde1fc76aac327a8d", | ||
| "installed_by": ["modules"] | ||
| }, | ||
| "bedtools/closest": { |
There was a problem hiding this comment.
bedtools/closest and bedtools/intersect are installed but nothing includes them. Either use BEDTOOLS_CLOSEST in place of DMR_NEAREST_GENE, or drop both modules and these entries.
| """ | ||
| input[0] = [ | ||
| [ id: 'test' ], | ||
| file(params.modules_testdata_base_path + 'genomics/homo_sapiens/nanopore/bedmethyl/test_hp1.bed.gz', checkIfExists: true), |
There was a problem hiding this comment.
This fixture returns 404: nf-core/test-datasets#2289 was closed, and upstream #13021 now generates the bedMethyl in-test with setup { run("MODKIT_PILEUP") } on test.sorted.phased.bam. This vendored copy (9d93dfa) predates that change. Please re-vendor from the current #13021 head. When you do, also pick up the fix for the non-deterministic log snapshot that manascripts asked for upstream. PR CI didn't catch this because the --tag small filter skips this test.
… modules Per ljwharbers's review of PR IntGenomicsLab#204: Re-vendored modkit/dmr from nf-core/modules#13021's current head (d886465c) -- the previously-vendored test still pointed at test_hp1.bed.gz/test_hp2.bed.gz, which 404s now that the upstream PR moved to generating its bedmethyl fixture in-test instead. This is why CI never actually exercised the new code. git_sha in modules.json updated to match. bedtools/closest and bedtools/intersect were vendored but never included anywhere -- the local DMR modules shell out to the bedtools CLI directly instead of delegating to these processes. Removed both, plus their modules.json entries. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wit78XR11V9AMAgY7gGrMT
… when unset Per ljwharbers's review: with --modkit_phased, any genome other than GRCh38/CHM13 hit a hard failure, because these two params only ever got a value via getGenomeAttribute() -- there was no schema entry, no nextflow.config default, and (the real bug) no way for an explicit --dmr_cpg_islands_bed/--dmr_gencode_gene_bed to survive: getGenomeAttribute() unconditionally overwrote whatever the user passed. - nextflow.config: both default to null. - nextflow_schema.json: both declared under reference_genome_options as optional file-path params. - workflows/lrsomatic.nf: params.dmr_cpg_islands_bed ?: getGenomeAttribute(...) instead of a bare overwrite, matching the 'explicit wins' pattern already used for VEP plugin resources in this same file. The DMR invocation block now checks both are present before running DMR; when they're not (no built-in default and no override), it logs a warning and skips DMR instead of crashing the run. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wit78XR11V9AMAgY7gGrMT
…eshold Per ljwharbers's review: on a whole-genome run, decompressing and sort -u'ing every bedMethyl row genome-wide (tens of millions of rows per haplotype) to find CpG-island overlaps is memory- and IO-heavy. Both hp1/hp2_bedmethyl are already bgzip+tabix indexed by the time this module sees them (HTSLIB_BGZIPTABIX runs just before it) -- query positions inside the CpG islands directly via tabix -R instead, which only reads the rows that can possibly matter. Also addresses the reviewer's second question on this module: 'covered' previously meant any bedMethyl row at all, regardless of depth. Added a minimum valid-coverage threshold (bedMethyl column 10, Nvalid_cov), configurable via ext.args and defaulting to 1 (unchanged behaviour) -- the actual right default for production use is a domain call for the maintainers, not something to silently pick here. Container/environment.yml updated: plain bedtools:2.31.1 has no tabix binary. Reused nf-core/modules' pints/caller container (real, in production use, confirmed via its own environment.yml to bundle both bedtools and htslib) rather than hand-construct an unverifiable Wave/mulled tag. Verified via the new subworkflows/local/tests/dmr.nf.test: real tabix -R output, versions.yml correctly reports both bedtools 2.31.1 and tabix 1.21. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wit78XR11V9AMAgY7gGrMT
Per ljwharbers's review: -D a reports signed distance relative to the input (DMR regions, unstranded, always '.') rather than the reference (genes, which do have a meaningful strand) -- -D b is the flag that actually makes the sign meaningful here. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wit78XR11V9AMAgY7gGrMT
Per ljwharbers's review: a public, phased, methylation-tagged fixture already exists (nf-core/test-datasets' test.sorted.phased.bam, the same one modkit/pileup's and modkit/dmr's own module tests use) -- no need for confidential data or a new fixture to get real CI coverage of the full chain. Chains MODKIT_PILEUP (--phased --modified-bases 5mC 5hmC) -> HTSLIB_BGZIPTABIX -> DMR_HAPLOTYPE_REGIONS -> MODKIT_DMR -> DMR_NEAREST_GENE via a setup block, against a small synthetic chr22:0-40001 CpG-island/gene-model pair (the exact region modkit/dmr's own module test already proves has real dual-haplotype coverage). Tagged 'small' -- .github/workflows/nf-test.yml only runs 'small'-tagged tests on pull_request, so an untagged test would never actually run in CI, which was the reviewer's core point (this exact class of gap already happened once with the vendored module test). Needed a dedicated subworkflows/local/tests/nextflow.config: this subworkflow-level nf-test session does not reliably pick up conf/modules.config's DMR-scoped withName blocks (confirmed directly -- HTSLIB_BGZIPTABIX's ext.prefix/ext.args2 and MODKIT_DMR's ext.args did not apply even with the 'DMR:' prefix present in the process name), so the real, non-stub behaviour this test exercises is set explicitly rather than inherited. Verified real, non-fabricated output: real per-haplotype methylation counts/fractions/deltas from the public BAM, correctly annotated with the test's synthetic gene entry -- not just workflow.success. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wit78XR11V9AMAgY7gGrMT
…it as pileup Per ljwharbers's review nits: - "MODKIT_DMR gets --ref without a .fai. Please check whether modkit re-indexes the 3 GB FASTA in every task." Confirmed by direct testing: modkit dmr pair has no --fai flag, looks for <fasta>.fai next to --ref automatically, and builds one from scratch if it's not there (mtime proof: with a pre-built .fai present, it's read unchanged rather than rewritten). Re-vendored modkit/dmr from nf-core/modules#13021 (now fixed upstream) to accept fai as a third element of its fasta input tuple; subworkflows/local/dmr.nf now joins fasta.first() with fai.first() before calling MODKIT_DMR, instead of passing fasta alone. - "Container version mismatch: MODKIT_DMR uses 0.6.1 vs PILEUP's 0.6.4." MODKIT_PILEUP already runs a patched modkit build (ghcr.io/ljwharbers/modkit:0.6.4-pacbiofix-6e0afa2, see its own modkit-pileup.diff) for unrelated PacBio pileup fixes; MODKIT_DMR was still on the stock 0.6.1 biocontainer. Patched to the same build for one consistent modkit binary across the whole DMR chain, following the exact same pattern (container swap + a conda/mamba guard, since the patch only exists in the container) already established for pileup. Recorded as modules/nf-core/modkit/dmr/modkit-dmr.diff + a "patch" entry in modules.json, matching pileup's own convention for a locally-modified vendored module. Verified: DMR_NEAREST_GENE output is byte-identical to the unpatched run (the container swap doesn't change dmr pair's calculation, as expected -- the PacBio fix is pileup-specific), and the fai is confirmed staged correctly (genome.fasta.fai symlinked next to genome.fasta in the task work dir). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wit78XR11V9AMAgY7gGrMT
Per ljwharbers's review nit. The raw *.bed stays unpublished (it's an intermediate DMR_NEAREST_GENE's annotated output supersedes), but the log is worth having without needing -resume when modkit dmr pair itself fails (e.g. the invalid-bedmethyl-data class of failure already found in this PR's own testing). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wit78XR11V9AMAgY7gGrMT
prek's end-of-file-fixer hook was failing CI on PR IntGenomicsLab#204 because modules.json lacked a terminating newline. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Resolves conflicts in conf/igenomes.config, modules.json, nextflow_schema.json, and workflows/lrsomatic.nf. All four were unrelated additions on both branches (DMR params/imports on this branch vs. CoRAL/Padfoot/ReConPlot and routine module version bumps on dev); kept both sides' content throughout. modules.json was rebuilt from dev's version with this branch's two unique entries (modkit/dmr, htslib/bgziptabix) re-added, rather than hand-splicing the interleaved diff hunks. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wit78XR11V9AMAgY7gGrMT
CI's pre-commit job was failing with "No such file or directory" errors reading hook binaries from ~/.cache/prek — a corrupted cache entry from j178/prek-action's built-in caching. Disabling cache forces a fresh hook install on every run instead of restoring the broken cache. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wit78XR11V9AMAgY7gGrMT
Disabling the prek-action cache (previous commit) did not fix the "Failed to extract metadata from entry for hook ...: No such file or directory" failure -- it reproduced identically on a fresh, uncached run, pointing at prek's own built-in-hook self-extraction rather than stale GitHub Actions caching. Running pre-commit directly (installed via uv) avoids that code path entirely. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wit78XR11V9AMAgY7gGrMT
Collapse single-element installed_by arrays onto one line to match prettier's expected formatting, as flagged by the pre-commit job. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wit78XR11V9AMAgY7gGrMT
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wit78XR11V9AMAgY7gGrMT
SAVANA_CNA (sample1/sample2, exit 2) and SAVANA_TO (sample3, exit 1) both legitimately fail-but-are-ignored on this tiny test dataset, producing breakpoints_double.csv content that no longer matches the md5s recorded in tests/default.nf.test.snap. Confirmed deterministic across two independent CI runs on identical code, so this isn't flakiness -- the snapshot was simply stale. Pre-existing on dev (inherited via the dev merge, unrelated to the DMR haplotype feature in this branch); the SAVANA ignored-error behavior itself is worth reporting separately. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wit78XR11V9AMAgY7gGrMT
modkit dmr pair rejects bedMethyl rows where valid_coverage doesn't
equal canonical+modified counts ("invalid bedMethyl record(s), invalid
data, valid coverage (N) is not equal to the sum of canonical and
modified counts (M)"). This happens whenever a read's modification
call isn't one of the codes requested via --modified-bases: it gets
bucketed into N_other instead of N_canonical/N_modified. Real ONT SUP
basecalling models call both 5mC and 5hmC, but the pipeline's default
modkit_args only requests 5mC, so any --modkit_phased run on real
dual-modification data crashed MODKIT_DMR.
Fix: when --modkit_phased is set and DMR isn't skipped, append 5hmC to
--modified-bases if it names 5mC without it, regardless of what
modkit_args the user supplied for the rest of the pileup. This only
changes behavior for the phased/DMR path; an unphased run, or a
--skip_dmr run, keeps the user's modkit_args exactly as given.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wit78XR11V9AMAgY7gGrMT
|
What goes wrong: modkit dmr pair has its own internal consistency check on each bedMethyl row: valid_coverage must equal N_canonical + N_modified. It doesn't, whenever a read's modification call isn't one of the codes named in --modified-bases, that read gets bucketed into N_other instead, and dmr pair rejects the row outright: encountered invalid bedMethyl record(s), invalid data, valid coverage (2) is not equal to the sum of canonical and modified counts (1), [BedMethylLine { ... raw_mod_code: Code('m') ... }] Which modkit_args the end-to-end run used: the pipeline default, --cpg --modified-bases 5mC. Real ONT SUP-basecalled data calls both 5mC and 5hmC at the same CpG sites, so with only 5mC requested, every read with a 5hmC call lands in N_other, and MODKIT_DMR crashes on the first such position. This isn't PacBio-specific (the schema's existing N_other note was written with PacBio's conflicting-probability case in mind), it reproduces identically on dual-modification ONT data, since the mechanism (uncounted mod code -> N_other) is the same. Fix, pushed in 515b522: rather than make DMR opt-in, conf/modules.config's MODKIT_PILEUP block now derives the actual pileup args instead of passing modkit_args straight through: when --modkit_phased is set and DMR isn't skipped, it appends 5hmC to --modified-bases if that list names 5mC without it, regardless of what modkit_args the user supplied otherwise. An unphased run, or a --skip_dmr run, is untouched; the user's modkit_args passes through exactly as given. Also updated modkit_args's help_text in nextflow_schema.json to document this. This matches what subworkflows/local/tests/dmr.nf.test already does explicitly in its own local config (ext.args = '--phased --modified-bases 5mC 5hmC') — that test's real (non-fabricated) DMR output is itself evidence this is the right pair of codes for the production path, not just the test fixture. |
nf-core/modules#13021 merged as a squash commit (0f9aa70f), so the vendored fbbd19f sha from the YannVRB/modules.git fork branch isn't actually reachable from nf-core/modules master even though the squash carried its content over unchanged (verified: main.nf, meta.yml, and both test files are byte-identical between the two commits). Switch modules.json's modkit/dmr entry to the nf-core/modules.git repo at the squash commit instead of the fork, per ljwharbers's review comment to do this once the upstream PR merges. The existing modkit-dmr.diff patch still applies as-is since the underlying content didn't change. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wit78XR11V9AMAgY7gGrMT
Per ljwharbers's review: docs/usage.md (--skip_dmr, --dmr_cpg_islands_bed, --dmr_gencode_gene_bed, and the automatic 5hmC inclusion for DMR), docs/output.md (the dmr/ output directory and *.nearestGene.bed's column layout), and a CHANGELOG entry for PR IntGenomicsLab#204 were all still missing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wit78XR11V9AMAgY7gGrMT
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ublished pileups The 5hmC append ran on every --modkit_phased run without --skip_dmr, including runs where DMR is then skipped for lack of CpG-island/gene BEDs. It now mirrors the workflow's DMR gate (explicit value, else the --genome default). usage.md claimed the published modkit_pileup files reflect --modkit_args exactly as given. They don't: DMR reads the same MODKIT_PILEUP task that publishes them, so they gain 5hmC rows. Corrected usage.md and the schema help text, and moved the CHANGELOG entry to Changed. Also: - output.md: --base C scores 5mC and 5hmC together - DMR_HAPLOTYPE_REGIONS: read the coverage threshold from ext.min_valid_coverage, not ext.args - DMR skip warning no longer prints --genome 'null' for a custom --fasta Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lts apply nextflow.config declares dmr_cpg_islands_bed/dmr_gencode_gene_bed (as null), and Nextflow silently ignores reassigning a param a config already defines. So `params.dmr_* = params.dmr_* ?: getGenomeAttribute(...)` never took the GRCh38/CHM13 default: a --genome GRCh38 --modkit_phased run warned "no default for --genome 'GRCh38'" and skipped DMR unless both BEDs were passed explicitly. Verified on Nextflow 26.04.3 with a test-profile run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
modkit dmr pair exits with an error on an empty regions file ("zero non-comment lines
in regions"). A BAM without MM/ML tags gives empty pileups and so an empty
DMR_HAPLOTYPE_REGIONS output, and a --modkit_phased test-profile run died on
MODKIT_DMR. Such samples are now filtered out with a warning. Added an nf-test for
that path (an island on a contig with no reads), next to the existing real-data
test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…shot The vendored module test's setup fed MODKIT_PILEUP upstream's three inputs, but our patched modkit/pileup takes fasta and fai separately plus a BED, so the test could not run here (PR CI skips it, as it has no `small` tag). The snapshot also recorded modkit 0.6.1 from the stock biocontainer, while the patch runs the 0.6.4 image. Re-recorded from a real Apptainer run (3/3 pass) and regenerated the module patch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ljwharbers
left a comment
There was a problem hiding this comment.
Thanks @YannVRB, this addresses all four points from the first review: modkit/dmr now points at the merged #13021, dmr.nf.test runs in PR CI, the params are declared and skip cleanly on other genomes, and the 5hmC/N_other problem is handled.
While checking it on Mindwell (Apptainer, real test-profile run with --modkit_phased), I found two more bugs. I pushed fixes for them directly to the branch, plus a few smaller fixes:
- The genome-default BEDs were never applied (843ec83).
nextflow.configdeclaresdmr_cpg_islands_bed/dmr_gencode_gene_bedasnull, and Nextflow silently ignores a laterparams.x = ...in the workflow for a param a config already defines (checked on 26.04.3). So--genome GRCh38 --modkit_phasedprinted "no default for --genome 'GRCh38'" and skipped DMR unless both BEDs were passed by hand. The othergetGenomeAttributeparams only work because they aren't declared. The BEDs are now resolved into local variables. - An empty regions BED killed the run (ed5e7e2). A BAM without MM/ML tags gives empty pileups, so no island is covered in both haplotypes.
modkit dmr pairthen exits with "zero non-comment lines in regions", and the whole pipeline stops. Those samples are now skipped with a warning, and there is a seconddmr.nf.testcase for this. - Docs about the 5hmC append (1e2bbda).
usage.mdsaid the publishedmodkit_pileupfiles keep--modkit_argsexactly as given. They don't: DMR reads the same MODKIT_PILEUP task that publishes them, so they gain a 5hmC row per CpG. I corrected usage.md and the schema help, and moved the CHANGELOG entry toChanged. The config append is now also skipped when DMR itself will be skipped for lack of BEDs. I also noted in output.md that--base Cscores 5mC and 5hmC together. - The vendored modkit/dmr module test couldn't run here (8ebea39). Its setup passed upstream's 3-input MODKIT_PILEUP call to our 4-input patched pileup, and the snapshot recorded modkit 0.6.1 while the patch runs 0.6.4. I fixed the setup, re-recorded the snapshot from a real run (3/3 pass) and regenerated the patch.
- Smaller fixes: the DMR_HAPLOTYPE_REGIONS coverage threshold now reads
ext.min_valid_coverageinstead of overloadingext.args; the skip warning no longer prints--genome 'null'; and current dev is merged in.
Checks run locally:
- default pipeline snapshot: unchanged after the dev merge;
dmr.nf.test: 2/2 pass;- modkit/dmr module test: 3/3 pass;
- phased test-profile run: completes;
- pre-commit and
nf-core pipelines lint: clean.
The only modules lint failure is oras_singularity_tag, which dev's modkit/pileup shares. I've left the linting.yml prek→pre-commit change as it is.
From my side this is good to go once CI is green on the new head.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The table (stdout) and sys.exit message (stderr) interleave nondeterministically, and with docker pull noise the message can fall past the lines Nextflow keeps in its error report, flaking the module test on cold CI runners. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PR checklist
What
Adds haplotype-vs-haplotype differential methylation region (DMR) calling, discussed with @ljwharbers: when --modkit_phased is set, lrsomatic now automatically runs a DMR comparison between a sample's two haplotypes at CpG islands, annotated with the nearest gene. Tumor/normal DMR is explicitly out of scope for this PR (deferred; cross-sample DMR stays in a downstream consumer pipeline).
New subworkflow subworkflows/local/dmr.nf:
MODKIT_PILEUP (hp1/hp2 bedMethyl)
-> HTSLIB_BGZIPTABIX (tabix-index each haplotype)
-> DMR_HAPLOTYPE_REGIONS (restrict to CpG islands covered in both haplotypes; new local module, queries via tabix -R rather than unpacking the whole genome, ports the NCP downstream pipeline's dmr_calling.sh pattern)
-> MODKIT_DMR (nf-core modkit/dmr, now pointed at the merged nf-core/modules#13021)
-> DMR_NEAREST_GENE (nearest-gene annotation via bedtools closest -D b; new local module)
Runs automatically whenever --modkit_phased is set; --skip_dmr (default false) turns it off independently. On any genome other than GRCh38/CHM13, DMR is skipped with a log.warn unless --dmr_cpg_islands_bed/--dmr_gencode_gene_bed are supplied explicitly.
Real bug fixed, not just worked around: the pipeline's default --modkit_args (--cpg --modified-bases 5mC) crashes modkit dmr pair on real dual-modification (5mC+5hmC) SUP-basecalled ONT data, since reads carrying an unrequested modification code land in N_other and fail dmr pair's valid_coverage == N_canonical + N_modified consistency check. Fixed by deriving the actual pileup args in conf/modules.config: when --modkit_phased is set and DMR will run, 5hmC is appended automatically if --modified-bases names 5mC without it, regardless of what --modkit_args the user supplied otherwise.
Full writeup in the PR comments.
New reference data
dmr_cpg_islands_bed / dmr_gencode_gene_bed are declared in conf/igenomes.config for both GRCh38 and CHM13, pointed at IntGenomicsLab/test-datasets/main (merged via IntGenomicsLab/test-datasets#4).
Testing
That data is private and not included in this PR (see Testing disclosure note below), but the subworkflow-level test above now gives public, CI-visible coverage of the same chain.
Private-data disclosure: the full real-data end-to-end run above was done against confidential data that is not, and will not be, committed to this repo or referenced by path/name anywhere in it; the public subworkflow-level test is a separate, independently-verified real (non-fabricated) run against public fixtures only.
Bugs found and fixed via real end-to-end testing (not stubs)
Follow-ups (not blocking, tracked separately)