Repository navigation
One row per fragment: merge mates, decode both strands, record output format version - #96
Merged
Merged
Conversation
… format version
Each row of the output matrix is now one sequenced DNA fragment instead of
one read, and every read is decoded on the base that actually carries its
call. This fixes two defects that each lost about half of the data.
Fixes
- Biscuit / bwameth / gem3: a read was discarded when its alignment
orientation disagreed with its bisulfite strand tag, on the premise that
it was an uninformative 'daughter strand'. Both mates of a pair carry the
same strand tag and the same conversion pattern, so this threw away read 2
of every pair. Both mates are now used.
- Bismark: the XM call string was read at the C of every CpG. Bismark writes
the call for a bottom-strand (XG:Z:GA) alignment on the G, so every
bottom-strand call was recorded as -1 (no data). The call is now read at
the informative base; the strand comes from XG, or is inferred from the
bases the calls sit on if XG is absent.
- A CpG is covered by a read when its informative base (C for the top
strand, G for the bottom strand) lies within the alignment; an informative
base deleted from the read is recorded as -1.
Fragment rows
- The two mates of a pair are merged into one row when both pass the read
filters and map to the same chromosome within 100 kb. A CpG covered by
both is stored once: the shared call if they agree, the available call if
only one has one, -1 if they disagree.
- Rows are ordered by chromosome, then by where each fragment's first
CpG-covering read appears in the BAM. Serial and parallel output remain
identical.
- --filter-non-converted still evaluates each read, and a failing read now
removes its whole fragment. --filter-em-overconversion is evaluated on the
merged fragment.
- tlen is the fragment length (absolute TLEN) rather than a signed per-read
value.
Output format
- metadata.json gains format_version (2) and row_unit ('fragment'). Files
without format_version are format 1 (one row per read). bam2tensor-inspect
reports the format and labels rows accordingly.
Verification
- tests/data/sarscov2: the same 557 read pairs aligned by Bismark 0.23.0 and
by bwa-meth 0.2.0 (subset of nf-core/test-datasets, MIT). The two files
extract to identical matrices, and both match an independent decode.
- tests/test_fragments.py: one hand-built case per fragment-assembly rule.
- tests/test_simulated.py: fragment-level truth for both dialects and both
strands; the Bismark bottom-strand xfail is now a passing test.
Docs
- README and CLAUDE.md describe fragment rows, strands and format versions.
- Two README analysis snippets densified the matrix, which conflates
'unmethylated' with 'not covered'; both now use the stored entries.
Coverage reportClick to see where and how coverage changed
This report was generated by python-coverage-comment-action |
||||||||||||||||||||||||||||||||||||||||||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Each row of the output matrix is now one sequenced DNA fragment instead of one read, and every read is decoded on the base that actually carries its call. This fixes two defects that each lost about half of the data, and makes the two aligner families behave the same way.
This changes the output. Files written by this code are not comparable row-for-row with earlier ones. They are marked with
"format_version": 2inmetadata.json.The two defects
1. Biscuit / bwameth / gem3: read 2 of every pair was discarded. A read was skipped when its alignment orientation disagreed with its bisulfite strand tag, on the premise that it was an uninformative "daughter strand". Both mates of a pair carry the same strand tag, and BAM stores both in top-strand orientation, so the skipped mate shows the same conversion pattern as the kept one.
Checked on
tests/test.bam, which is real paired-end bwa-meth EM-seq data (16,686 usable pairs):2. Bismark: every bottom-strand call was recorded as
-1. TheXMcall string was read at the C of each CpG. Bismark writes the call for a bottom-strand (XG:Z:GA) alignment on the G. Confirmed on real Bismark v0.23.0 output: all 7,230 top-strand CpG calls sit on the C, all 7,409 bottom-strand calls sit on the G.Before and after, on real aligner output
The same paired-end FASTQs aligned by Bismark and by bwa-meth (from nf-core/test-datasets, full files):
-1-1The last row is the strongest check. One file is decoded from Bismark's call string and
XGtag, the other from the read bases and bwa-meth'sYDtag. They now agree on every one of 12,818 calls.The remaining
-1calls in this dataset are mates disagreeing in their overlap. These reads were simulated with each mate converted independently, so they disagree far more often than real mates do (0.3% in the real cfDNA data above).How reads become rows
-1if one says methylated and the other unmethylated.-1.--threadsoutput stay identical.--filter-non-convertedstill evaluates each read against the threshold, as the NEB tool does. A failing read now removes its whole fragment, since incomplete conversion is a property of the molecule.--filter-em-overconversionis evaluated on the merged fragment.tlenis the fragment length (absolute TLEN). The BAM's sign only says which mate is leftmost.Output format flag
metadata.jsongainsformat_version(now2) androw_unit("fragment"). Files withoutformat_versionare format 1.bam2tensor-inspectprints the format and labels rowsFragmentsorReadsaccordingly. The README documents both versions and how to check.Tests
tests/data/sarscov2/: 557 read pairs aligned by Bismark and by bwa-meth, with their reference. A subset of nf-core/test-datasets, redistributed under its MIT licence (licence, provenance and the subsetting script are in that directory, about 180 KB in total).tests/test_real_aligners.pyrequires the two files to extract to identical matrices and to match an independent decode.tests/test_fragments.py: one hand-built case per rule above.tests/test_simulated.py: fragment-level truth for both dialects and both strands, serial and parallel, plus a check that the same simulated molecules give the same matrix in either dialect. The Bismark bottom-strandxfailfrom Robustness fixes: clear errors, atomic writes, no silent filter no-ops; simulated-truth tests #93 is now an ordinary passing test.344 tests pass.
nox: all 12 sessions pass locally.Also in this PR
Two analysis snippets in the README densified the matrix with
.toarray(), which makes an uncovered CpG indistinguishable from an unmethylated one. On the fixture above the per-CpG example reported 1.7% methylation for a site that is 72.7% methylated. Both snippets now use the stored entries, and the over-conversion replay snippet was checked to reproduce--filter-em-overconversionexactly.Performance
Same harness as #95. Where both versions decode every read (Bismark-style BAM), extraction takes about 12% longer and uses less memory. For Biscuit-style BAMs the old code skipped half the reads, so the new code does more work for the extra data: 56% more time for 75% more calls.
Not verified, and not done
XBfollows the same logic asYD, per its documented meaning, but I have no real gem3 output to check it against./1and/2) cannot be paired and stay as separate rows.Note added after merge. The upstream nf-core reads turned out to be a non-directional library. The "bwa-meth BAM" rows in the before-and-after table count every fragment bwa-meth aligned, including about 3,700 complementary-strand pairs that bwa-meth tags with a
YDvalue contradicting their bases. Calls for those pairs are unreliable in both the old and the new code, because bwa-meth assumes a directional library. The cross-aligner row (4,277 of 4,277) covers exactly the directional pairs, for which the tags are right, and the bundled fixture contains only those. See #97 for details and the documentation of this limit.