Skip to content

fix(clip): unmap fully-clipped reads and match fgbio unmap/mate fidelity (CLIP-01) - #492

Merged
nh13 merged 1 commit into
mainfrom
nh/fix-clip-unmap-when-fully-clipped
Jul 10, 2026
Merged

nh13 merged 1 commit into
mainfrom
nh/fix-clip-unmap-when-fully-clipped

Conversation

@nh13

@nh13 nh13 commented Jul 8, 2026 •

Copy link
Copy Markdown
Member

What & why

clip (and every internal caller of SamRecordClipper / RawRecordClipper) diverged from fgbio when clipping removes all of a read's aligned bases. This PR closes the gap and, in doing so, ports the full fgbio/htsjdk fidelity for the unmap and mate-info paths.

fgbio SamRecordClipper.clip{Start,End}OfAlignment computes numberOfClippableBases (the sum of alignment M/=/X and insertion I CIGAR op lengths) and, when the requested clip is >= that count, calls htsjdk SAMUtils.makeReadUnmapped and returns — the read becomes unmapped with an empty CIGAR, keeping its bases/qualities. fgumi instead clipped every aligned base, leaving the read mapped at a stale POS with an all-clip CIGAR (e.g. 50H) and an empty sequence — silent corruption. This is tracker finding CLIP-01.

Fix

1. Unmap when the whole alignment is clipped (CLIP-01).

  • Add the numberOfClippableBases guard to both SamRecordClipper and RawRecordClipper, in clip_start_of_alignment and clip_end_of_alignment: when clippable <= requested, unmap the read and return the clippable count, rather than proceeding to clip.
  • make_read_unmapped[_raw] mirrors htsjdk SAMUtils.makeReadUnmapped: clear reference / pos / mapq / CIGAR / TLEN and the duplicate, secondary, supplementary, and proper-pair flags; set UNMAPPED; reverse-complement bases + reverse quals (then clear the reverse flag) for reverse-strand reads; bases/quals are otherwise preserved.
  • Two fgumi unit tests that pinned the old all-soft-clip behavior (10S10M clip 10, 10M clip 10) are updated to assert the unmap.

2. Reorient strand-sensitive aux tags on unmap (htsjdk reverseComplement(true) fidelity).

htsjdk makeReadUnmapped reverse-complements a reverse-strand read via reverseComplement(true), which does more than SEQ/QUAL: it also reverse-complements the base-oriented tags in SAMRecord.TAGS_TO_REVERSE_COMPLEMENT (E2, SQ) and reverses the quality-oriented tags in TAGS_TO_REVERSE (OQ, U2). fgumi's make_read_unmapped[_raw] previously left these forward-oriented, so a reverse-strand read carrying e.g. OQ that clipping unmapped diverged from fgbio — squarely in the CLIP-01 path.

  • make_read_unmapped (typed) and make_read_unmapped_raw now reorient these tags for reverse-strand reads, driven by shared TAGS_TO_REVERSE_COMPLEMENT / TAGS_TO_REVERSE constants so the two paths stay in lockstep.
  • Only the Z-string encoding is handled (the sole form these tags take in practice); htsjdk additionally reverses array-encoded forms, but none of these tags is emitted as an array by real tooling. Documented inline.
  • Adds E2 / U2 / SQ to the SamTag catalog.

3. Full mate-info parity, including the one-mapped mate-reverse flag.

Because fgbio ClipBam then calls SamPairUtil.setMateInfo(r1, r2, true) on every pair, the clipper unmap surfaced a paired-read gap: fgumi's clip only refreshed the MC/MQ tags. This PR replaces that with set_mate_info_raw, a faithful port of htsjdk setMateInfo (all three branches — both mapped, both unmapped, one of each) covering mate ref/pos/strand, the mate-unmapped flag, MQ/MC tags, and TLEN. This makes paired output match fgbio when a mate unmaps, and also fixes stale mate-pos/TLEN in the ordinary both-mapped case (previously never updated after a clip shifted a start).

  • In the one-mapped branch, the mapped read's mate-reverse flag now reflects the unmapped read's actual strand (SamPairUtil.java:267) rather than an assumed false. The clipper's own unmap clears REVERSE, but a read that arrived unmapped-on-input may still carry it, and set_mate_info_raw runs on every pair, so such a read reaches this branch.

Parity evidence (fgbio 4.1.0 ClipBam vs fgumi clip, compare bams --mode content)

Reproducer: reports/fgbio-parity-fixtures/clip-01-unmap-fully-clipped.sh.

Scenario A — mapped fragment, 50M@chr1:100, --read-one-five-prime 60:

before after
fgumi FLAG=0 POS=150 CIGAR=50H (mapped, corrupt) FLAG=4 POS=0 CIGAR=* (unmapped)
vs fgbio DIFFER IDENTICAL

Scenario B — FR pair, both clipped past alignment (--read-one-five-prime 101 --read-two-five-prime 101, ClipBamTest.scala:621):

before after
fgumi mate-flags stale (MATE_REVERSE set, RNEXT=chr1) FLAG=77/141, RNEXT=* PNEXT=0 TLEN=0
vs fgbio DIFFER IDENTICAL

Also verified IDENTICAL to fgbio for normal paired clipping that does not unmap (overlap clip, fixed 3′ clip, clip-past-mate), confirming the setMateInfo change is safe on the common path.

Tests / CI

  • Clipper unmap cross-checks (crosscheck_unmap_*): start/end, boundary (clip == clippable), no-unmap regression guard, and negative-strand revcomp — on both SamRecordClipper and RawRecordClipper.
  • New tag-reorientation cross-checks: negative-strand reorients OQ/U2 (reversed) and E2 (reverse-complemented) identically on the typed and raw paths; positive-strand leaves them untouched.
  • test_set_mate_info_raw_all_branches (all three htsjdk branches) plus a new one-mapped case asserting the mate-reverse flag follows the unmapped read's actual strand.
  • cargo ci-fmt, cargo ci-lint, cargo ci-test (2217 tests) all green.

Refs: CLIP-01.

@nh13
nh13 had a problem deploying to github-actions July 8, 2026 22:18 — with GitHub Actions Failure
@coderabbitai

coderabbitai Bot commented Jul 8, 2026 •

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Fully clipped reads now become unmapped in typed and raw paths while preserving and normalizing sequence data. Paired clipping now synchronizes mate coordinates, flags, tags, and template lengths through set_mate_info_raw.

Changes

Clipper unmap behavior

Layer / File(s) Summary
Typed unmap helpers and wiring
crates/fgumi-sam/src/clipper.rs
Computes clippable bases, converts fully clipped typed records to unmapped state, and adds start/end early-unmap branches with reverse-strand tag handling.
Raw unmap helpers and wiring
crates/fgumi-sam/src/clipper.rs
Adds equivalent raw-byte behavior, including reverse-complemented sequence, reversed qualities, cleared alignment fields, and empty CIGAR.
Unmap behavior validation
crates/fgumi-sam/src/clipper.rs, crates/fgumi-sam/Cargo.toml, crates/fgumi-tag/src/tag.rs
Updates full-clip expectations and cross-checks typed/raw state, boundary counts, preserved bases and qualities, reverse-strand handling, and auxiliary tags.

Mate-pair info synchronization

Layer / File(s) Summary
Mate synchronization implementation
src/lib/commands/clip.rs
Replaces tag-only updates with mapped/mapped, mapped/unmapped, and unmapped/unmapped handling for coordinates, mate flags, MQ/MC, and TLEN.
Pipeline integration
src/lib/commands/clip.rs
Uses set_mate_info_raw(r1, r2) in single-threaded and multi-threaded paired clipping paths.
Mate-state validation
src/lib/commands/clip.rs
Tests all mate-state branches and their coordinate, flag, tag, and TLEN results.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: fully clipped reads are unmapped and mate handling is aligned with fgbio.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch nh/fix-clip-unmap-when-fully-clipped

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

@nh13
nh13 temporarily deployed to github-actions July 8, 2026 22:38 — with GitHub Actions Inactive
@codecov

codecov Bot commented Jul 8, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.50000% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.17%. Comparing base (f638abc) to head (6794f10).
⚠️ Report is 6 commits behind head on main.

Files with missing lines Patch % Lines
src/lib/commands/clip.rs 98.50% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #492      +/-   ##
==========================================
+ Coverage   91.12%   91.17%   +0.05%     
==========================================
  Files          78       78              
  Lines       51540    51916     +376     
==========================================
+ Hits        46964    47334     +370     
- Misses       4576     4582       +6     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

nh13 added a commit that referenced this pull request Jul 9, 2026
…t-order (R2-CLIP-02/03/04)

Three round-2 clip parity fixes, stacked on #492 (CLIP-01 / setMateInfo):

- R2-CLIP-02: the clip loop was `if len==1 {frag} else if len==2 {pair}` with no
  else, so any template with a secondary/supplementary read (len > 2) was left
  COMPLETELY unclipped with no mate fixing. Now locate the primary R1/R2 by SAM
  flags (fgbio `Template.r1`/`r2`), clip them, and repair mate info on every
  supplementary alignment via a port of htsjdk 5.0.0
  `SamPairUtil.setMateInformationOnSupplementalAlignment` (mate ref/pos/strand,
  mate-unmapped flag, TLEN = -matePrimary.TLEN, MC when mate mapped, and MQ
  unconditionally). Both the single-threaded and threaded paths are refactored.

- R2-CLIP-03: `--clip-bases-past-mate` bounded the mate window with unclipped
  start/end (soft + HARD clips); fgbio uses `mate.unSoftClipped{Start,End}` (soft
  only, since hard-clipped bases are physically absent). Added soft-only
  `unsoftclipped_{start,end}[_raw]` helpers and used them in both clippers, so a
  hard-clipped mate no longer widens the window and under-clips.

- R2-CLIP-04: `--sort-order` on clip only relabeled the SO header field (a no-op);
  removed the flag entirely (rely on a downstream `fgumi sort`).

fgbio-oracle parity (fgbio 4.1.0 ClipBam, default Hard):
- CHIM (len-3 chimeric template, --clip-overlapping-reads): before DIFFER
  (fgumi left 100M/100M unclipped, supp mate-info stale) -> after MATCH
  (75M25H/25H75M, supp gets MC/MQ/mate-pos/TLEN like fgbio).
- HARDMATE (FR dovetail, reverse mate 50M50H, --clip-bases-past-mate): before
  DIFFER (fgumi 110M10H) -> after MATCH (60M60H, past mate's soft end).
- SORTFLAG: --sort-order now rejected ("unexpected argument").

Fixture: reports/fgbio-parity-fixtures/r2-clip-02-03-04.sh (+ before/after evidence).
Tests: soft-only helper unit tests (typed + raw), find_primary_pair_indices
partition test; full suite 2213 passed; ci-fmt/ci-lint clean.
@nh13
nh13 force-pushed the nh/fix-clip-unmap-when-fully-clipped branch from c7535af to 389e375 Compare July 9, 2026 05:33
@nh13
nh13 temporarily deployed to github-actions July 9, 2026 05:33 — with GitHub Actions Inactive
@nh13

nh13 commented Jul 9, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 9, 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

nh13 commented Jul 9, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 9, 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

nh13 commented Jul 9, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 9, 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: 1

🤖 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 `@crates/fgumi-sam/src/clipper.rs`:
- Around line 5969-6057: The new clipper tests are parameterized with runtime
`for` loops, which should be converted to `rstest` cases for per-case reporting.
Refactor the `crosscheck_unmap_*` tests in `clipper.rs` to use `#[rstest]` with
`#[case(...)]` for `ClippingMode` (and any clip/bases variations) instead of
looping inside the test body, while keeping the existing assertions and coverage
intact.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: d27c0889-7069-4598-8416-07f25da78ae2

📥 Commits

Reviewing files that changed from the base of the PR and between f638abc and 389e375.

📒 Files selected for processing (2)
  • crates/fgumi-sam/src/clipper.rs
  • src/lib/commands/clip.rs

Comment thread crates/fgumi-sam/src/clipper.rs Outdated
nh13 added a commit that referenced this pull request Jul 9, 2026
…t-order (R2-CLIP-02/03/04)

Three round-2 clip parity fixes, stacked on #492 (CLIP-01 / setMateInfo):

- R2-CLIP-02: the clip loop was `if len==1 {frag} else if len==2 {pair}` with no
  else, so any template with a secondary/supplementary read (len > 2) was left
  COMPLETELY unclipped with no mate fixing. Now locate the primary R1/R2 by SAM
  flags (fgbio `Template.r1`/`r2`), clip them, and repair mate info on every
  supplementary alignment via a port of htsjdk 5.0.0
  `SamPairUtil.setMateInformationOnSupplementalAlignment` (mate ref/pos/strand,
  mate-unmapped flag, TLEN = -matePrimary.TLEN, MC when mate mapped, and MQ
  unconditionally). Both the single-threaded and threaded paths are refactored.

- R2-CLIP-03: `--clip-bases-past-mate` bounded the mate window with unclipped
  start/end (soft + HARD clips); fgbio uses `mate.unSoftClipped{Start,End}` (soft
  only, since hard-clipped bases are physically absent). Added soft-only
  `unsoftclipped_{start,end}[_raw]` helpers and used them in both clippers, so a
  hard-clipped mate no longer widens the window and under-clips.

- R2-CLIP-04: `--sort-order` on clip only relabeled the SO header field (a no-op);
  removed the flag entirely (rely on a downstream `fgumi sort`).

fgbio-oracle parity (fgbio 4.1.0 ClipBam, default Hard):
- CHIM (len-3 chimeric template, --clip-overlapping-reads): before DIFFER
  (fgumi left 100M/100M unclipped, supp mate-info stale) -> after MATCH
  (75M25H/25H75M, supp gets MC/MQ/mate-pos/TLEN like fgbio).
- HARDMATE (FR dovetail, reverse mate 50M50H, --clip-bases-past-mate): before
  DIFFER (fgumi 110M10H) -> after MATCH (60M60H, past mate's soft end).
- SORTFLAG: --sort-order now rejected ("unexpected argument").

Fixture: reports/fgbio-parity-fixtures/r2-clip-02-03-04.sh (+ before/after evidence).
Tests: soft-only helper unit tests (typed + raw), find_primary_pair_indices
partition test; full suite 2213 passed; ci-fmt/ci-lint clean.
@nh13
nh13 force-pushed the nh/fix-clip-unmap-when-fully-clipped branch from 389e375 to fb85484 Compare July 9, 2026 17:13
@nh13
nh13 temporarily deployed to github-actions July 9, 2026 17:13 — with GitHub Actions Inactive
@nh13

nh13 commented Jul 9, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 9, 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 that referenced this pull request Jul 9, 2026
…t-order (R2-CLIP-02/03/04)

Three round-2 clip parity fixes, stacked on #492 (CLIP-01 / setMateInfo):

- R2-CLIP-02: the clip loop was `if len==1 {frag} else if len==2 {pair}` with no
  else, so any template with a secondary/supplementary read (len > 2) was left
  COMPLETELY unclipped with no mate fixing. Now locate the primary R1/R2 by SAM
  flags (fgbio `Template.r1`/`r2`), clip them, and repair mate info on every
  supplementary alignment via a port of htsjdk 5.0.0
  `SamPairUtil.setMateInformationOnSupplementalAlignment` (mate ref/pos/strand,
  mate-unmapped flag, TLEN = -matePrimary.TLEN, MC when mate mapped, and MQ
  unconditionally). Both the single-threaded and threaded paths are refactored.

- R2-CLIP-03: `--clip-bases-past-mate` bounded the mate window with unclipped
  start/end (soft + HARD clips); fgbio uses `mate.unSoftClipped{Start,End}` (soft
  only, since hard-clipped bases are physically absent). Added soft-only
  `unsoftclipped_{start,end}[_raw]` helpers and used them in both clippers, so a
  hard-clipped mate no longer widens the window and under-clips.

- R2-CLIP-04: `--sort-order` on clip only relabeled the SO header field (a no-op);
  removed the flag entirely (rely on a downstream `fgumi sort`).

fgbio-oracle parity (fgbio 4.1.0 ClipBam, default Hard):
- CHIM (len-3 chimeric template, --clip-overlapping-reads): before DIFFER
  (fgumi left 100M/100M unclipped, supp mate-info stale) -> after MATCH
  (75M25H/25H75M, supp gets MC/MQ/mate-pos/TLEN like fgbio).
- HARDMATE (FR dovetail, reverse mate 50M50H, --clip-bases-past-mate): before
  DIFFER (fgumi 110M10H) -> after MATCH (60M60H, past mate's soft end).
- SORTFLAG: --sort-order now rejected ("unexpected argument").

Fixture: reports/fgbio-parity-fixtures/r2-clip-02-03-04.sh (+ before/after evidence).
Tests: soft-only helper unit tests (typed + raw), find_primary_pair_indices
partition test; full suite 2213 passed; ci-fmt/ci-lint clean.
@nh13

nh13 commented Jul 9, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 9, 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

nh13 commented Jul 10, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 10, 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

nh13 commented Jul 10, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 10, 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.

fgbio SamRecordClipper.clip{Start,End}OfAlignment unmaps a read (via
htsjdk SAMUtils.makeReadUnmapped) when the requested clip is >= the
number of clippable (alignment + insertion) bases, keeping the bases and
qualities intact and emptying the CIGAR. fgumi instead clipped every
aligned base, leaving the read mapped at a stale POS with an all-clip
CIGAR (e.g. 50H) and an empty sequence — silent corruption (CLIP-01).

Add the numberOfClippableBases guard to both SamRecordClipper and
RawRecordClipper (clip_start_of_alignment / clip_end_of_alignment): when
clippable <= requested, unmap the read (clear reference/pos/mapq/CIGAR/
TLEN and the duplicate/secondary/supplementary/proper-pair flags, set
UNMAPPED, reverse-complement bases + reverse quals for reverse-strand
reads) and return the clippable count. Two fgumi tests that pinned the
old all-soft-clip behavior are updated to assert the unmap.

make_read_unmapped[_raw] also reorients the strand-sensitive aux tags of
reverse-strand reads, matching htsjdk reverseComplement(true): the
base-oriented tags E2/SQ are reverse-complemented and the
quality-oriented tags OQ/U2 are reversed (SAMRecord.TAGS_TO_REVERSE_-
COMPLEMENT / TAGS_TO_REVERSE). Without this, a reverse-strand read
carrying e.g. OQ that clipping unmaps kept a forward-oriented OQ,
diverging from fgbio. Only the Z-string encoding is handled (the sole
form these tags take in practice); the typed and raw paths use the same
tag sets so they stay in lockstep. Adds E2/U2/SQ to the SamTag catalog.

Because fgbio ClipBam then calls SamPairUtil.setMateInfo on every pair,
replace clip.rs's partial MC/MQ-only mate update with set_mate_info_raw,
a faithful port of htsjdk setMateInfo (all three branches: both mapped,
both unmapped, one of each) covering mate ref/pos/strand, the
mate-unmapped flag, MQ/MC tags, and TLEN. This makes paired output match
fgbio when a mate unmaps, and also fixes stale mate-pos/TLEN in the
ordinary both-mapped case.

In the one-mapped setMateInfo branch, the mapped read's mate-reverse
flag now reflects the unmapped read's actual strand (SamPairUtil.java:267)
rather than an assumed false. The clipper's own unmap clears REVERSE, but
a read that arrived unmapped-on-input may still carry it, and
set_mate_info_raw runs on every pair, so such a read reaches this branch.

Parity verified end-to-end against fgbio 4.1.0 ClipBam
(reports/fgbio-parity-fixtures/clip-01-unmap-fully-clipped.sh):
before -> DIFFER, after -> IDENTICAL (fgumi compare bams --mode content)
for a fully-clipped fragment, a pair where both mates unmap, and normal
overlapping / fixed / past-mate paired clipping. New unit tests cross-check
the tag reorientation (typed vs raw, negative- and positive-strand) and
the one-mapped mate-reverse flag.

Refs: CLIP-01.
@nh13
nh13 force-pushed the nh/fix-clip-unmap-when-fully-clipped branch from fb85484 to 6794f10 Compare July 10, 2026 03:41
@nh13
nh13 temporarily deployed to github-actions July 10, 2026 03:41 — with GitHub Actions Inactive
@nh13 nh13 changed the title fix(clip): unmap reads when clipping consumes the whole alignment (CLIP-01) fix(clip): unmap fully-clipped reads and match fgbio unmap/mate fidelity (CLIP-01) Jul 10, 2026
nh13 added a commit that referenced this pull request Jul 10, 2026
…t-order (R2-CLIP-02/03/04)

Three round-2 clip parity fixes, stacked on #492 (CLIP-01 / setMateInfo):

- R2-CLIP-02: the clip loop was `if len==1 {frag} else if len==2 {pair}` with no
  else, so any template with a secondary/supplementary read (len > 2) was left
  COMPLETELY unclipped with no mate fixing. Now locate the primary R1/R2 by SAM
  flags (fgbio `Template.r1`/`r2`), clip them, and repair mate info on every
  supplementary alignment via a port of htsjdk 5.0.0
  `SamPairUtil.setMateInformationOnSupplementalAlignment` (mate ref/pos/strand,
  mate-unmapped flag, TLEN = -matePrimary.TLEN, MC when mate mapped, and MQ
  unconditionally). Both the single-threaded and threaded paths are refactored.

- R2-CLIP-03: `--clip-bases-past-mate` bounded the mate window with unclipped
  start/end (soft + HARD clips); fgbio uses `mate.unSoftClipped{Start,End}` (soft
  only, since hard-clipped bases are physically absent). Added soft-only
  `unsoftclipped_{start,end}[_raw]` helpers and used them in both clippers, so a
  hard-clipped mate no longer widens the window and under-clips.

- R2-CLIP-04: `--sort-order` on clip only relabeled the SO header field (a no-op);
  removed the flag entirely (rely on a downstream `fgumi sort`).

fgbio-oracle parity (fgbio 4.1.0 ClipBam, default Hard):
- CHIM (len-3 chimeric template, --clip-overlapping-reads): before DIFFER
  (fgumi left 100M/100M unclipped, supp mate-info stale) -> after MATCH
  (75M25H/25H75M, supp gets MC/MQ/mate-pos/TLEN like fgbio).
- HARDMATE (FR dovetail, reverse mate 50M50H, --clip-bases-past-mate): before
  DIFFER (fgumi 110M10H) -> after MATCH (60M60H, past mate's soft end).
- SORTFLAG: --sort-order now rejected ("unexpected argument").

Fixture: reports/fgbio-parity-fixtures/r2-clip-02-03-04.sh (+ before/after evidence).
Tests: soft-only helper unit tests (typed + raw), find_primary_pair_indices
partition test; full suite 2213 passed; ci-fmt/ci-lint clean.
@nh13
nh13 merged commit cbc7d72 into main Jul 10, 2026
10 checks passed
@nh13
nh13 deleted the nh/fix-clip-unmap-when-fully-clipped branch July 10, 2026 04:08
@nh13 nh13 mentioned this pull request Jul 10, 2026
nh13 added a commit that referenced this pull request Jul 10, 2026
…t-order (R2-CLIP-02/03/04)

Three round-2 clip parity fixes, stacked on #492 (CLIP-01 / setMateInfo):

- R2-CLIP-02: the clip loop was `if len==1 {frag} else if len==2 {pair}` with no
  else, so any template with a secondary/supplementary read (len > 2) was left
  COMPLETELY unclipped with no mate fixing. Now locate the primary R1/R2 by SAM
  flags (fgbio `Template.r1`/`r2`), clip them, and repair mate info on every
  supplementary alignment via a port of htsjdk 5.0.0
  `SamPairUtil.setMateInformationOnSupplementalAlignment` (mate ref/pos/strand,
  mate-unmapped flag, TLEN = -matePrimary.TLEN, MC when mate mapped, and MQ
  unconditionally). Both the single-threaded and threaded paths are refactored.

- R2-CLIP-03: `--clip-bases-past-mate` bounded the mate window with unclipped
  start/end (soft + HARD clips); fgbio uses `mate.unSoftClipped{Start,End}` (soft
  only, since hard-clipped bases are physically absent). Added soft-only
  `unsoftclipped_{start,end}[_raw]` helpers and used them in both clippers, so a
  hard-clipped mate no longer widens the window and under-clips.

- R2-CLIP-04: `--sort-order` on clip only relabeled the SO header field (a no-op);
  removed the flag entirely (rely on a downstream `fgumi sort`).

fgbio-oracle parity (fgbio 4.1.0 ClipBam, default Hard):
- CHIM (len-3 chimeric template, --clip-overlapping-reads): before DIFFER
  (fgumi left 100M/100M unclipped, supp mate-info stale) -> after MATCH
  (75M25H/25H75M, supp gets MC/MQ/mate-pos/TLEN like fgbio).
- HARDMATE (FR dovetail, reverse mate 50M50H, --clip-bases-past-mate): before
  DIFFER (fgumi 110M10H) -> after MATCH (60M60H, past mate's soft end).
- SORTFLAG: --sort-order now rejected ("unexpected argument").

Fixture: reports/fgbio-parity-fixtures/r2-clip-02-03-04.sh (+ before/after evidence).
Tests: soft-only helper unit tests (typed + raw), find_primary_pair_indices
partition test; full suite 2213 passed; ci-fmt/ci-lint clean.
nh13 added a commit that referenced this pull request Jul 10, 2026
…t-order (R2-CLIP-02/03/04)

Three round-2 clip parity fixes, stacked on #492 (CLIP-01 / setMateInfo):

- R2-CLIP-02: the clip loop was `if len==1 {frag} else if len==2 {pair}` with no
  else, so any template with a secondary/supplementary read (len > 2) was left
  COMPLETELY unclipped with no mate fixing. Now locate the primary R1/R2 by SAM
  flags (fgbio `Template.r1`/`r2`), clip them, and repair mate info on every
  supplementary alignment via a port of htsjdk 5.0.0
  `SamPairUtil.setMateInformationOnSupplementalAlignment` (mate ref/pos/strand,
  mate-unmapped flag, TLEN = -matePrimary.TLEN, MC when mate mapped, and MQ
  unconditionally). Both the single-threaded and threaded paths are refactored.

- R2-CLIP-03: `--clip-bases-past-mate` bounded the mate window with unclipped
  start/end (soft + HARD clips); fgbio uses `mate.unSoftClipped{Start,End}` (soft
  only, since hard-clipped bases are physically absent). Added soft-only
  `unsoftclipped_{start,end}[_raw]` helpers and used them in both clippers, so a
  hard-clipped mate no longer widens the window and under-clips.

- R2-CLIP-04: `--sort-order` on clip only relabeled the SO header field (a no-op);
  removed the flag entirely (rely on a downstream `fgumi sort`).

fgbio-oracle parity (fgbio 4.1.0 ClipBam, default Hard):
- CHIM (len-3 chimeric template, --clip-overlapping-reads): before DIFFER
  (fgumi left 100M/100M unclipped, supp mate-info stale) -> after MATCH
  (75M25H/25H75M, supp gets MC/MQ/mate-pos/TLEN like fgbio).
- HARDMATE (FR dovetail, reverse mate 50M50H, --clip-bases-past-mate): before
  DIFFER (fgumi 110M10H) -> after MATCH (60M60H, past mate's soft end).
- SORTFLAG: --sort-order now rejected ("unexpected argument").

Fixture: reports/fgbio-parity-fixtures/r2-clip-02-03-04.sh (+ before/after evidence).
Tests: soft-only helper unit tests (typed + raw), find_primary_pair_indices
partition test; full suite 2213 passed; ci-fmt/ci-lint clean.
nh13 added a commit that referenced this pull request Jul 11, 2026
…t-order (R2-CLIP-02/03/04)

Three round-2 clip parity fixes, stacked on #492 (CLIP-01 / setMateInfo):

- R2-CLIP-02: the clip loop was `if len==1 {frag} else if len==2 {pair}` with no
  else, so any template with a secondary/supplementary read (len > 2) was left
  COMPLETELY unclipped with no mate fixing. Now locate the primary R1/R2 by SAM
  flags (fgbio `Template.r1`/`r2`), clip them, and repair mate info on every
  supplementary alignment via a port of htsjdk 5.0.0
  `SamPairUtil.setMateInformationOnSupplementalAlignment` (mate ref/pos/strand,
  mate-unmapped flag, TLEN = -matePrimary.TLEN, MC when mate mapped, and MQ
  unconditionally). Both the single-threaded and threaded paths are refactored.

- R2-CLIP-03: `--clip-bases-past-mate` bounded the mate window with unclipped
  start/end (soft + HARD clips); fgbio uses `mate.unSoftClipped{Start,End}` (soft
  only, since hard-clipped bases are physically absent). Added soft-only
  `unsoftclipped_{start,end}[_raw]` helpers and used them in both clippers, so a
  hard-clipped mate no longer widens the window and under-clips.

- R2-CLIP-04: `--sort-order` on clip only relabeled the SO header field (a no-op);
  removed the flag entirely (rely on a downstream `fgumi sort`).

fgbio-oracle parity (fgbio 4.1.0 ClipBam, default Hard):
- CHIM (len-3 chimeric template, --clip-overlapping-reads): before DIFFER
  (fgumi left 100M/100M unclipped, supp mate-info stale) -> after MATCH
  (75M25H/25H75M, supp gets MC/MQ/mate-pos/TLEN like fgbio).
- HARDMATE (FR dovetail, reverse mate 50M50H, --clip-bases-past-mate): before
  DIFFER (fgumi 110M10H) -> after MATCH (60M60H, past mate's soft end).
- SORTFLAG: --sort-order now rejected ("unexpected argument").

Fixture: reports/fgbio-parity-fixtures/r2-clip-02-03-04.sh (+ before/after evidence).
Tests: soft-only helper unit tests (typed + raw), find_primary_pair_indices
partition test; full suite 2213 passed; ci-fmt/ci-lint clean.
nh13 added a commit that referenced this pull request Jul 11, 2026
…t-order (R2-CLIP-02/03/04)

Three round-2 clip parity fixes, stacked on #492 (CLIP-01 / setMateInfo):

- R2-CLIP-02: the clip loop was `if len==1 {frag} else if len==2 {pair}` with no
  else, so any template with a secondary/supplementary read (len > 2) was left
  COMPLETELY unclipped with no mate fixing. Now locate the primary R1/R2 by SAM
  flags (fgbio `Template.r1`/`r2`), clip them, and repair mate info on every
  supplementary alignment via a port of htsjdk 5.0.0
  `SamPairUtil.setMateInformationOnSupplementalAlignment` (mate ref/pos/strand,
  mate-unmapped flag, TLEN = -matePrimary.TLEN, MC when mate mapped, and MQ
  unconditionally). Both the single-threaded and threaded paths are refactored.

- R2-CLIP-03: `--clip-bases-past-mate` bounded the mate window with unclipped
  start/end (soft + HARD clips); fgbio uses `mate.unSoftClipped{Start,End}` (soft
  only, since hard-clipped bases are physically absent). Added soft-only
  `unsoftclipped_{start,end}[_raw]` helpers and used them in both clippers, so a
  hard-clipped mate no longer widens the window and under-clips.

- R2-CLIP-04: `--sort-order` on clip only relabeled the SO header field (a no-op);
  removed the flag entirely (rely on a downstream `fgumi sort`).

fgbio-oracle parity (fgbio 4.1.0 ClipBam, default Hard):
- CHIM (len-3 chimeric template, --clip-overlapping-reads): before DIFFER
  (fgumi left 100M/100M unclipped, supp mate-info stale) -> after MATCH
  (75M25H/25H75M, supp gets MC/MQ/mate-pos/TLEN like fgbio).
- HARDMATE (FR dovetail, reverse mate 50M50H, --clip-bases-past-mate): before
  DIFFER (fgumi 110M10H) -> after MATCH (60M60H, past mate's soft end).
- SORTFLAG: --sort-order now rejected ("unexpected argument").

Fixture: reports/fgbio-parity-fixtures/r2-clip-02-03-04.sh (+ before/after evidence).
Tests: soft-only helper unit tests (typed + raw), find_primary_pair_indices
partition test; full suite 2213 passed; ci-fmt/ci-lint clean.
nh13 added a commit that referenced this pull request Jul 11, 2026
…t-order (R2-CLIP-02/03/04) (#502)

Three round-2 clip parity fixes, stacked on #492 (CLIP-01 / setMateInfo):

- R2-CLIP-02: the clip loop was `if len==1 {frag} else if len==2 {pair}` with no
  else, so any template with a secondary/supplementary read (len > 2) was left
  COMPLETELY unclipped with no mate fixing. Now locate the primary R1/R2 by SAM
  flags (fgbio `Template.r1`/`r2`), clip them, and repair mate info on every
  supplementary alignment via a port of htsjdk 5.0.0
  `SamPairUtil.setMateInformationOnSupplementalAlignment` (mate ref/pos/strand,
  mate-unmapped flag, TLEN = -matePrimary.TLEN, MC when mate mapped, and MQ
  unconditionally). Both the single-threaded and threaded paths are refactored.

- R2-CLIP-03: `--clip-bases-past-mate` bounded the mate window with unclipped
  start/end (soft + HARD clips); fgbio uses `mate.unSoftClipped{Start,End}` (soft
  only, since hard-clipped bases are physically absent). Added soft-only
  `unsoftclipped_{start,end}[_raw]` helpers and used them in both clippers, so a
  hard-clipped mate no longer widens the window and under-clips.

- R2-CLIP-04: `--sort-order` on clip only relabeled the SO header field (a no-op);
  removed the flag entirely (rely on a downstream `fgumi sort`).

fgbio-oracle parity (fgbio 4.1.0 ClipBam, default Hard):
- CHIM (len-3 chimeric template, --clip-overlapping-reads): before DIFFER
  (fgumi left 100M/100M unclipped, supp mate-info stale) -> after MATCH
  (75M25H/25H75M, supp gets MC/MQ/mate-pos/TLEN like fgbio).
- HARDMATE (FR dovetail, reverse mate 50M50H, --clip-bases-past-mate): before
  DIFFER (fgumi 110M10H) -> after MATCH (60M60H, past mate's soft end).
- SORTFLAG: --sort-order now rejected ("unexpected argument").

Fixture: reports/fgbio-parity-fixtures/r2-clip-02-03-04.sh (+ before/after evidence).
Tests: soft-only helper unit tests (typed + raw), find_primary_pair_indices
partition test; full suite 2213 passed; ci-fmt/ci-lint clean.

This branch was previously deployed

1 inactive deployment
github-actions — 6794f109 Deployed Jul 10, 2026 by nh13 via coverage #2129
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.

1 participant