Skip to content

fix(group,dedup): pair duplex strands sharing an unclipped 5' coordinate - #485

Merged
nh13 merged 1 commit into
mainfrom
nh/fix-duplex-strand-pairing
Jul 8, 2026
Merged

nh13 merged 1 commit into
mainfrom
nh/fix-duplex-strand-pairing

Conversation

@nh13

@nh13 nh13 commented Jul 8, 2026 •

Copy link
Copy Markdown
Member

Summary

fgumi's paired grouping strategy can split a single duplex molecule into two when a template's two mates share an unclipped 5' coordinate — i.e. fully-overlapping / short-insert fragments. This diverges from fgbio GroupReadsByUmi, which pairs the two strands into one molecule.

Root cause

Duplex strand pairing prefixes each half of the paired UMI with a lower- vs higher-coordinate-read marker (lowerReadUmiPrefix / higherReadUmiPrefix) so the top strand's prefixed UMI is the exact reverse of the bottom strand's. That prefix is chosen by is_r1_genomically_earlier_raw, which broke the position tie with r1_pos <= r2_pos. On a tie (r1_pos == r2_pos) that returns true for both strands (top = R1 forward, bottom = R1 reverse), so both get the lower prefix on R1's half, their prefixed UMIs are no longer reverses of each other, they fail to reverse-match in the assigner, and the molecule is split in two.

fgbio breaks the same tie on strand (GroupReadsByUmi.umiForRead): pos1 < pos2 || (pos1 == pos2 && r1.positiveStrand). This PR mirrors that — on a tie, R1 is "earlier" iff it is on the forward strand. The buggy helper was shared by group and dedup; both are fixed.

compare bams was blind to it

The grouping-mode comparison keyed only on the full MI (base + /A|/B), treating the two strands as independent groups. A strand-pairing split keeps the per-MiKey mapping a consistent bijection (X/A↔X/A, X/B↔Y/A), so it reported the split as EQUIVALENT with zero mismatches — which is why the benchmark's own group.paired equivalence check never flagged this. This PR adds a base-level (strand-suffix-stripped) molecule-membership check: reads that share a molecule in one BAM must share a molecule in the other. A genuine strand-pairing split now makes the groupings DIFFER; a molecule relabel or an /A↔/B swap stays EQUIVALENT.

Tests

  • New regression test test_paired_assigner_pairs_overlapping_duplex_strands: builds the two strands of one duplex molecule at equal unclipped 5' positions and asserts they group into one molecule. Verified RED on the pre-fix code (["0/A","0/A","1/A","1/A"], two bases), GREEN after.
  • Repaired test_paired_assigner_explicit_ab_ba_symmetry: the port of fgbio's "correctly group reads with the paired assigner when the two UMIs are the same" used an FR-only builder (so it never built the reverse-strand bottom read) and asserted two separate groups, inverting fgbio's contract. It now builds a real reverse-strand bottom read and asserts one molecule with /A + /B, matching fgbio.
  • Unit tests for the compare base-level check (split flagged; molecule relabel, /A↔/B swap, and single-strand relabel not flagged).

Full suite green (2215 tests); fmt and clippy (pedantic) clean.

Validation on real data

Reproduced end-to-end on the agilent-hs2 vendor sample (SRR30485713): fgumi's own group → duplex → filter produced 1,025,610 filtered records vs fgbio's 1,025,612 — a single degenerate duplex molecule (chr11:119116712; 8 top-strand + 3 bottom-strand templates of a fully-overlapping fragment) that fgumi split and fgbio kept. After the fix fgumi produces 1,025,612, byte-identical to fgbio. The behavior is present on main (0.4.0), so it is not a recent regression. The fixed compare bams flags exactly that one molecule out of 10,220,662 (Grouping mismatches: 1, DIFFER) where the old tool reported EQUIVALENT.

Summary by CodeRabbit

  • Bug Fixes
    • Improved BAM grouping consistency checks with an additional molecule-level duplex strand-pairing validation to catch split/merge discrepancies.
    • Fixed strand-aware tie-breaking for paired-read processing when mates share identical unclipped 5’ coordinates.
    • Updated pairing/UMI grouping behavior so duplex strands from the same molecule remain together and aren’t split.
  • Tests
    • Added unit tests for duplex pairing mismatch detection, and expanded regression coverage for label swapping, relabeling, single-strand cases, and fully overlapping short-insert templates.

@nh13
nh13 temporarily deployed to github-actions July 8, 2026 09:19 — with GitHub Actions Inactive
@coderabbitai

coderabbitai Bot commented Jul 8, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: df51a58e-0f50-4fee-ae4c-43dac5923f74

📥 Commits

Reviewing files that changed from the base of the PR and between 5db2e1a and d9e623f.

📒 Files selected for processing (4)
  • src/lib/commands/common.rs
  • src/lib/commands/compare/bams.rs
  • src/lib/commands/dedup.rs
  • src/lib/commands/group.rs

Walkthrough

This PR makes raw-record R1 ordering strand-aware across dedup and group, and adds duplex molecule-base mismatch counting to grouping comparisons in compare/bams.rs.

Changes

Strand-aware tie-break and duplex validation

Layer / File(s) Summary
Shared raw-record ordering helper
src/lib/commands/common.rs
Adds is_r1_genomically_earlier_raw for reference-, position-, and strand-aware ordering of paired raw BAM records.
Dedup caller and test update
src/lib/commands/dedup.rs
Uses the shared helper, removes the local implementation, and updates the equal-position test to check strand-dependent tie-breaking.
Group caller and paired-duplex tests
src/lib/commands/group.rs
Uses the shared helper and revises paired-strand tests to cover duplex molecule-base preservation and overlapping short-insert fragments.
Duplex grouping mismatch validation
src/lib/commands/compare/bams.rs
Adds MiKey::base(), counts base-level pairing mismatches in ordered and unordered grouping checks, updates grouping mismatch stats, and adds unit tests.

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

Possibly related issues

Possibly related PRs

Suggested labels: bug, fgumi group, fgumi dedup, fgumi compare

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main fix in group and dedup for duplex strands sharing an unclipped 5' coordinate.
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.
✨ 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-duplex-strand-pairing

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

@codecov

codecov Bot commented Jul 8, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.82297% with 15 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.12%. Comparing base (5b036fe) to head (d9e623f).

Files with missing lines Patch % Lines
src/lib/commands/compare/bams.rs 82.55% 15 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #485      +/-   ##
==========================================
+ Coverage   91.06%   91.12%   +0.06%     
==========================================
  Files          78       78              
  Lines       51376    51540     +164     
==========================================
+ Hits        46783    46964     +181     
+ Misses       4593     4576      -17     

☔ 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

nh13 commented Jul 8, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

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

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

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

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

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

Copy link
Copy Markdown

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
putComment timed out

@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 `@src/lib/commands/dedup.rs`:
- Around line 469-489: The R1 tie-break ordering logic is duplicated in both
is_r1_genomically_earlier_raw and the matching helper in group, so it should be
centralized to avoid behavior drift. Extract the shared strand-aware raw-BAM
comparison into a common helper that both dedup and group call, preserving the
existing ref-id, unclipped 5' position, and reverse-strand tie-break behavior.
Update the callers to use the shared helper and keep the symbols
is_r1_genomically_earlier_raw and the corresponding group-side function aligned
through that shared implementation.
🪄 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: f3fbd800-bd12-4dae-b47e-383b50e1a1b8

📥 Commits

Reviewing files that changed from the base of the PR and between 5b036fe and 5db2e1a.

📒 Files selected for processing (3)
  • src/lib/commands/compare/bams.rs
  • src/lib/commands/dedup.rs
  • src/lib/commands/group.rs

Comment thread src/lib/commands/dedup.rs Outdated
fgumi's `paired` grouping strategy could split a single duplex molecule into
two when a template's two mates share an unclipped 5' coordinate (fully
overlapping / short-insert fragments). `is_r1_genomically_earlier_raw` broke
the position tie with `r1_pos <= r2_pos`, which returns true for both the top
(R1 forward) and bottom (R1 reverse) strand. That assigns the lower/higher
paired-UMI prefix inconsistently between the two strands, so their prefixed
UMIs are no longer reverses of each other and fail to pair.

Mirror fgbio `GroupReadsByUmi.umiForRead`'s tie-break
(`pos1 == pos2 && r1.positiveStrand`): on a tie, R1 is earlier iff it is on
the forward strand. Applied to both `group` and `dedup`, which shared the
buggy helper.

Also fix `compare bams` grouping mode: it keyed only on the full MI
(base + `/A`|`/B`) and so reported such a split as EQUIVALENT (the per-MiKey
mapping stays a consistent bijection). Add a base-level, strand-suffix-
stripped molecule-membership check so a strand-pairing difference makes the
groupings DIFFER; a molecule relabel or an `/A`<->`/B` swap stays EQUIVALENT.

Tests: add a regression test for the overlapping-fragment tie case; repair
`test_paired_assigner_explicit_ab_ba_symmetry` (the port used an FR-only
builder and asserted two groups, inverting fgbio's contract) to build a real
reverse-strand bottom read and assert one molecule with `/A` + `/B`; add unit
tests for the compare base-level check.
@nh13
nh13 force-pushed the nh/fix-duplex-strand-pairing branch from 5db2e1a to d9e623f Compare July 8, 2026 16:31
@nh13
nh13 temporarily deployed to github-actions July 8, 2026 16:31 — with GitHub Actions Inactive
@nh13

nh13 commented Jul 8, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

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

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

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

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

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

This branch was previously deployed

1 inactive deployment
github-actions — d9e623f6 Deployed Jul 8, 2026 by nh13 via coverage #1995
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