Conversation
…fire Hard-clipped supplementary alignments keep the full-length read's nuc/msp/m6a tags, so coordinates run past the clipped SEQ and, flipped for a reverse strand, wrap below zero as a u32. ft fire panicked slicing SEQ with them (#136), and ft extract printed misplaced positions for forward reads and u32-wrapped ones for reverse reads. The reader now treats such a record as untagged: one warning, annotations cleared, record kept. Every command agrees and the record passes through ft fire unchanged. Closes #136
mrvollger
force-pushed
the
fix/136-fire-coords-out-of-range-main
branch
from
September 18, 2026 14:26
353e7a2 to
10d1295
Compare
Review found convert-tags, strip-basemods, ddda-to-m6a and predict-m6a parse through ma_io::read_record directly and so still serialized the overrunning legacy coordinates into an MA tag. The check now runs inside read_record, so every parser path agrees. The warning prints for the first 10 records and then drops to debug, since ONT BAMs can hold many hard-clipped supplementary reads.
This was referenced Sep 18, 2026
mrvollger
added a commit
that referenced
this pull request
Sep 18, 2026
Twin of #148 for main, rebased onto #147 so the two do not conflict on the release-plz.toml comment. Merging this brings in #147's commit too; #147 can then be closed as included. Same content as #148 plus the fix from #150: `semver_check = false` lives in the `[workspace]` table. **Do not merge until #132 (the 0.13.1 release PR on `release/v0.13`) is merged.** release-plz finds its open release PR by branch prefix only. A push to main with the semver check off would run release-plz-pr, find #132 by its `release-plz-` branch name, and force-push 0.14.0 content onto it. Today the only thing preventing that is main's run dying in the semver check, which this PR removes. After #132: merge this first on main, then #141 and #138. release-plz then opens a fresh 0.14.0 release PR. --------- Co-authored-by: Mitchell R. Vollger <mvollger@gmail.com>
mrvollger
added a commit
that referenced
this pull request
Sep 18, 2026
Forward-port to main of the 0.13.1 fix for #140, which shipped from `release/v0.13` in #142. ## Cause `--haps` was parsed on `CallPeaksOptions`, but `call_peaks_for_chrom` hardcoded `haps: false` when it built the pileup. The pileup never made H1/H2 tracks, so every peak printed the empty-track placeholder (`0 0 -1.0 0 0`) for both haplotypes. ## Fix `haps` moves from `CallPeaksOptions` into `PeakCallingParams`, which is what `call_peaks_for_chrom` receives. Both structs are flattened, so the flag name and help text are unchanged; `--haps` now appears among the peak-calling options in `--help` instead of last. union-peaks sets it false because BED intervals carry no HP tag. The second commit keeps `--haps` from costing more than it must. Per-haplotype tracks triple the pileup memory per chromosome, and about a third of that was FIRE element tracking on the H1/H2 tracks that nothing reads (only `all_data.fire_elements` feeds peak boundaries). The haplotype tracks are now built without it. ## Check `NAPA.bam` carries HP tags. `ft call-peaks --haps --min-fire-frac 0.5` on it: | | coverage | coverage_H1 | coverage_H2 | |---|---|---|---| | before | 95 | 0 | 0 | | after | 95 | 45 | 14 | A regression test pins these three values. The call-peaks snapshot and pileup tests pass. ## Order Merge into main after #149 and before #138. release-plz opens the 0.14.0 release PR on the first push to main after #149. Fixes #140 --------- Co-authored-by: Mitchell R. Vollger <mvollger@gmail.com>
…nals Review of #138 found the check detected stale tags only by side effect (coordinates that happened to overrun SEQ), missed legacy-tagged records whose copied coordinates fit, left a second guard in BamChunk with a different policy, and never told users the aligner flag that avoids it. read_record now decides before parsing, per tag family: MA by its own read length, legacy ns/nl/as/al by any hard clip or missing SEQ, MM/ML by MN when present else hard clips; bounds stay as a backstop. A stale record is cleared, its frame reset to SEQ, and write_record strips every stale tag so ft fire, convert-tags and add-nucleosomes all leave an honest untagged read. The BamChunk skip is gone, so plain minimap2 output no longer loses whole records. The warning explains the cause once, names the remedy (pbmm2, dorado aligner -Y, minimap2 -Y -y, or -F 2048), and a total prints at exit. ft qc says the same instead of suggesting add-nucleosomes. README and --help state the input requirement. Not reframed: MM/ML cannot be recovered without the clipped bases.
- A matching MA read length now vouches for the MM/ML next to it, so fibertools' own hard-clipped output (ddda-to-m6a, predict-m6a) is no longer rejected for lacking MN. - write_record_with_basemods writes MN, the SAM spec's frame for MM/ML. - ft fire always writes the model back for records it cannot score, so a record dropped by the parsed-model backstop leaves without stale tags. - The dorado remedy is spelled `dorado aligner --mm2-opts "-Y"`. - SEQ-less MM/ML records count as stale instead of spamming parser warnings. - One warning carries the explanation and the first record; the qc warning no longer repeats the remedy; "1 record" is singular. - The MM/ML fixture's supplementary carries only MM/ML/MN, so the MN branch is exercised end to end.
…d lift them MA-family annotations are molecular coordinates of the full read, so on a hard-clipped supplementary alignment they are still right; only the liftover has to know that SEQ starts at the leading hard clip. MM/ML are SEQ-relative and cannot be recovered without the clipped bases. The frame is decided from the record: an MA read length equal to SEQ plus both hard clips (or legacy ns/nl/as/al on any hard clip) is the full-read frame. molecular-annotation's AlignedBlocks carries a query offset, set by from_record from that rule; lifts subtract it, SEQ-relative projections (ft center) subtract it too, and pyMA mirrors the rule. In that frame the reader keeps nuc/msp/fire under the full read length, never parses MM/ML, and the writer strips MM/ML/MN. Such reads have no m6A, so they are NotCallable: ft fire writes them back unscored, add-nucleosomes and footprint skip them, qc counts them, and pileup FIRE denominators exclude them under --callable-fibers. Tags that match neither SEQ nor the full read are still dropped as before. An MN tag that disagrees with SEQ next to a matching MA tag drops only the base mods. Full-frame records are warned about once, only when m6A is actually dropped, with a total at exit; a second run over ft's own output is silent. extract --all reports fiber_length in the annotation frame.
hard_clips and query_span called record.cigar() on every record. A bare Record::new() (used by unit tests) has no data, and rust-htslib's cigar() slices that null pointer, which trips a debug assertion; release builds passed, CI's debug build did not. Records with no CIGAR have no clips.
This branch has not been deployed
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.
Fixes #136. Third round of the same class (#31 in 2023, the BamChunk guard narrowing in 2025, #136 now), so this PR is built to end it, not only to stop the panic.
Cause
Aligners that hard-clip supplementary alignments (minimap2 and dorado aligner without
-Y) copy the full-length read's tags onto the clipped record.ft firesliced SEQ with those coordinates and panicked;ft extractprinted misplaced positions for forward reads and u32-wrapped ones for reverse reads;ft convert-tagswrote them into an MA tag.The design
MA-family annotations (nuc, msp, fire, fiberseq_callable) are molecular coordinates of the full read, so on a hard-clipped record they are still correct. Only the liftover has to know that SEQ starts
H_leadbases into that frame. MM/ML encode positions as skips over specific bases, so without the clipped bases they cannot be recovered.Frame rule, decided from the record before anything is parsed (
ma_io::record_frame):ns/nl/as/alon any hard clip: full-read frame, read length taken as SEQ plus clips (no producer writes them after clipping). Without SEQ, stale.Liftover.
AlignedBlocksin molecular-annotation carries a query offset;from_recordsets it from the rule above; reference lifts subtract it and SEQ-relative projections (ft center) subtract it too. The reverse-strand flip already uses the tag's read length, so both strands come out right with no special case. pyMA mirrors the rule inpysam_utils.from_recordandset_aligned_blocks(query_offset=).No m6A means NotCallable.
ft firecannot score these reads and writes them back unscored;add-nucleosomesandfootprintskip them;qccounts them;pileup --callable-fibersexcludes them from FIRE denominators. Defaultpileupcounts them in coverage exactly as it counts any Untagged read today.Writers. In the full-read frame the MA tag is rewritten with the full read length and MM/ML/MN are stripped, so a second run over
ft's own output is silent.write_record_with_basemodsnow writesMN, the SAM spec's frame for MM/ML, soft predict-m6a,ft ddda-to-m6aandft strip-basemodsoutput carries the signal other tools use to detect a later hard clip. That adds one tag to those outputs for everyone; it is the one judgement call here.Telling the user. One warning per run, only when m6A is actually dropped, plus a total at exit:
Stale records get a parallel pair of messages.
ft qcsays the same instead of suggestingft add-nucleosomes. README and the input BAM--helptext state the requirement. The old guard inbio_io.rsthat deleted hard-clipped MM/ML records outright is gone.Breaking
ft firepanicked or passed stale tags through,ft extractprinted wrong coordinates, and records with MM/ML were deleted from output with a per-record warning.extract --allreportsfiber_lengthin the annotation frame (the full read on such records), matching the molecular columns and molecular-mode BED12.MN.AlignedBlocks::with_query_offset,MolecularAnnotations::query_offset,hard_clips,query_span,full_read_query_offset; pyMAset_aligned_blocksgainsquery_offset.Tests
from_recordin every frame. pyMA:TestFullReadFrame.ma_io.rsunit tests for each branch of the frame rule, the legacy assignment, the callable marker, MN disagreement, SEQ-less hard clips, and the writers.ont_hardclip_supplementary.bam(legacy tags, dorado shape),ont_hardclip_mmml.bam(MM/ML/MN copied, no MA, the plain minimap2 shape: stale), and newont_hardclip_full_frame.bam(MA-tagged primaries plus forward and reverse hard-clipped supplementaries with expected reference coordinates computed independently with pysam).extract(molecular and lifted coordinates, fiber_length),fire(kept calls, stripped MM/ML, silent second run),convert-tags,qc(NotCallable counts),pileup(nucleosome coverage includes them, FIRE coverage does not under--callable-fibers),center, andadd-nucleosomes.Follow-ups (separate PRs)
ft qc: a distinct row for full-frame and stale-frame reads rather than folding into NotCallable/Untagged.ft validate: report hard-clipped records and their frames.-Yand why; the bug template should ask for the alignment command.pileupcoverage: decide whether to skip them there too.Order
Merge into main after #149 and #141 (both merged). release-plz refreshes the 0.14.0 release PR #152 with this entry marked breaking; molecular-annotation goes to 0.0.4 in the same release.
Reviewed by three adversarial agent panels (46, 45 and 36 agents); the last one's confirmed findings are in the final commit.