Repository navigation
fix(consensus)!: require mapped reads for consensus unless --allow-unmapped - #737
Conversation
|
Note Reviews pausedUse the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (5)
WalkthroughMixed mapped and unmapped records now retain mapped reads during filtering. Unmapped records receive rejection indices when mapped records exist. Wholly unmapped families remain available when ChangesMapped-read precedence
Estimated code review effort: 3 (Moderate) | ~20 minutes Merge Risk: 🔵 Low · up to The PR prevents unmapped reads from contaminating consensus while preserving wholly unmapped handling when explicitly enabled. It is mergeable with owner awareness that the shared Sequence Diagram(s)sequenceDiagram
participant InputRecords
participant PreGroupFilter
participant AlignmentFilter
participant ConsensusCaller
InputRecords->>PreGroupFilter: provide mapped and unmapped records
PreGroupFilter->>AlignmentFilter: pass records allowed for grouping
AlignmentFilter->>AlignmentFilter: remove unmapped reads from mixed groups
AlignmentFilter->>ConsensusCaller: return retained reads and rejection indices
ConsensusCaller->>InputRecords: produce consensus output
Possibly related PRs
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
Comment |
|
@coderabbitai pause |
✅ Action performedReviews paused. |
Add regression tests to the three consensus commands covering a molecule that contains a half-mapped read pair. The simplex and duplex cases currently fail. `fgumi group --allow-unmapped` deliberately supports wholly unmapped templates, but its gate is on templates with *both* reads unmapped, so a half-mapped pair is kept regardless. The consensus pre-group filter then keeps any record that is mapped *or* has a mapped mate, which admits the unmapped end as well. That end has no cigar, and `is_cigar_prefix` returns true for an empty left hand side, so in `select_most_common_alignment_group` it matches every alignment group rather than forming one of its own. It is retained and contributes bases and depth to the consensus with no alignment supporting it. simplex: a family of three pairs, one half-mapped, reports cD=3 for R2 where only two reads are eligible, and the unmapped read's bases reach the consensus. duplex: the same molecule shape reports aD=3 for the AB single-strand consensus of R2 where only two AB-R2s are mapped. codec: already unaffected, since its alignment grouping is reached only for templates forming a primary FR pair. The test guards that rather than reproducing a defect, and passes as written. Two further simplex cases cover `--allow-unmapped`, which must keep working: within a set being consensus called mapped reads must still win, while a wholly unmapped family must still produce a consensus.
584d151 to
b41ce9e
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #737 +/- ##
==========================================
+ Coverage 94.12% 94.22% +0.09%
==========================================
Files 181 186 +5
Lines 109415 110979 +1564
==========================================
+ Hits 102987 104568 +1581
+ Misses 6428 6411 -17 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
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/common.rs`:
- Line 383: Update the comment at src/lib/commands/common.rs:383-383 to describe
that the default pre-group predicate keeps only mapped primary records, removing
references to the obsolete mapped-mate exception. Also update the test
documentation at src/lib/commands/common.rs:2222-2231 to match this default
behavior; no code changes are needed.
🪄 Autofix
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: 67f53720-5911-4c6a-9cb8-c3549484d212
📒 Files selected for processing (5)
crates/fgumi-consensus/src/vanilla_caller.rssrc/lib/commands/common.rstests/integration/test_codec_command.rstests/integration/test_duplex_command.rstests/integration/test_simplex_command.rs
b41ce9e to
1386d8b
Compare
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
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/common.rs`:
- Around line 362-363: Update the documentation near the `--allow-unmapped`
option in `common.rs` to remove the claim that fully unmapped input can be
consensus-called universally. Clarify that the unmapped-consensus behavior
applies only to callers that support it, while `CodecConsensusCaller` still
requires mapped primary FR pairs.
🪄 Autofix
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: 9ca745ed-b7d3-4fc7-b3af-e158d945a648
📒 Files selected for processing (2)
src/lib/commands/common.rssrc/lib/commands/duplex.rs
…mapped Drop unmapped records in the consensus pre-group filter rather than keeping any record that is mapped *or* has a mapped mate. This is fgumi's copy of fgbio's `ConsensusCallingIterator` filter, shared by the simplex, duplex and codec commands, so all three are covered by construction. An unmapped record has no cigar, and `is_cigar_prefix` returns true for an empty left hand side, so in `select_most_common_alignment_group` an empty cigar matched every alignment group rather than forming one of its own. The read was retained and contributed bases and depth to the consensus with no alignment supporting it. Whether it was retained at all depended on where it landed in the length-descending sort, so the outcome turned on a read-length tiebreak. The mapped end of a half-mapped pair is unaffected: it is mapped in its own right. This is a behaviour change for input containing half-mapped pairs. It restores parity with fgbio, where the equivalent family produces no consensus at all rather than an R2 built from unaligned reads (fulcrumgenomics/fgbio#1168). `--allow-unmapped` continues to relax the rule so wholly unmapped input can be consensus called, as `fgumi group --allow-unmapped` intends. Under that flag, unmapped reads are admitted to grouping but still never displace mapped ones: `drop_unmapped_if_any_mapped` prefers the mapped reads within each set being consensus called, and leaves a wholly unmapped set intact. Reads it drops are attributed to `RejectionReason::Unmapped`, which had no production call site until now, and are added to the rejected-index set so they reach the rejects output. The `unmapped_paired_mate_mapped` case in `test_consensus_pregroup_keep_flags` is inverted to pin the new behaviour, and a case is added for the mapped end of a half-mapped pair, which is still kept. Full suite: 7247 tests, 0 failures. Formatting and clippy clean.
1386d8b to
444e746
Compare
|
@coderabbitai review |
✅ Action performedReview finished.
|
Companion to fulcrumgenomics/fgbio#1168, which fixes the same defect in the Scala implementation.
Problem
Unmapped reads contribute bases and depth to consensus reads, with no alignment supporting them.
fgumi group --allow-unmappeddeliberately supports wholly unmapped templates (ribosome display and similar), but its gate is on templates with both reads unmapped, so a half-mapped pair is kept regardless of that flag. The consensus pre-group filter then kept any record that was mapped or had a mapped mate, which admitted the unmapped end too.That end has no cigar, and
is_cigar_prefixreturnstruefor an empty left hand side —a.len() > b.len()is false,last_index = 0.saturating_sub(1), and the loop body never runs:So in
select_most_common_alignment_groupan empty cigar matches every alignment group instead of forming one of its own, and the read is retained rather than rejected. Whether it is retained at all depends on where it lands in the length-descending sort, so the outcome turns on a read-length tiebreak.Observed for a molecule of three read pairs where one pair has an unmapped R2:
simplexcD=3for R2 where only two reads are eligible, and the unmapped read's bases reach the consensusduplexaD=3for the AB single-strand consensus of R2 where only two AB-R2s are mappedcodecFix
Two layers.
1.
consensus_pregroup_keep_flagsnow requires the record to be mapped (src/lib/commands/common.rs). This is fgumi's copy of fgbio'sConsensusCallingIteratorfilter and is already shared by simplex, duplex and codec, so all three are covered by construction — and it is already parameterised by--allow-unmapped, so the relaxation has a home. The mapped end of a half-mapped pair is unaffected; it is mapped in its own right.2. Under
--allow-unmapped, mapped reads still win.drop_unmapped_if_any_mappedremoves unmapped reads from any set being consensus called that also holds a mapped read, and leaves a wholly unmapped set intact. Dropped reads are attributed toRejectionReason::Unmapped— which existed in the enum with no production call site until now — and reach the rejects output.So: unmapped reads are not consensus input by default; with
--allow-unmappedthey are admitted but never displace mapped reads.Behaviour change
This is breaking for input containing half-mapped pairs. A family of such pairs previously produced an R2 consensus built from unaligned reads. It now produces no consensus for that end, and therefore no read pair.
That is deliberate: it restores parity with fgbio, where the same family produces nothing at all. Gating the fallback behind
--allow-unmappedrather than applying it unconditionally is what keeps the default in step with fgbio while preserving the wholly-unmapped capability fgbio does not have.Tests
Written first, watched failing, then fixed — the two commits are
test:thenfix:in that order, so the first is red by design. Happy to squash if you would rather not carry a red commit.At the test commit, verified: 3 failing, 2 passing guards.
test:committest_simplex_ignores_unmapped_end_when_mapped_reads_presenttest_duplex_ignores_unmapped_end_when_mapped_reads_presenttest_simplex_prefers_mapped_reads_even_when_unmapped_are_allowed--allow-unmappedtest_codec_ignores_half_mapped_pair_when_mapped_reads_presenttest_simplex_allow_unmapped_still_consenses_a_wholly_unmapped_family--allow-unmappedcapability against this changeThe simplex tests assert R2's bases as well as its depth. Depth alone can look correct while the consensus is still contaminated — that is how the
--max-readsinteraction was caught in the fgbio equivalent.test_consensus_pregroup_keep_flags'sunmapped_paired_mate_mappedcase is inverted to pin the new behaviour, and a case is added for the mapped end of a half-mapped pair, which is still kept.After:
cargo ci-test7247 passed, 0 failed;cargo ci-fmtandcargo ci-lintclean.Not addressed here
The root cause in
is_cigar_prefixis untouched. An empty cigar is still a prefix of every cigar. This PR makes that unreachable from the consensus callers by default, and harmless under--allow-unmapped; any future path producing an empty cigar would hit it again.Risk: output changes for consensus grouping and consensus depth/bases, pinned by mapped-read filtering; sort order, corrected UMIs, and metrics: none.
unsafechanges: none, andCLAUDE.mdallowlist updates: none. Memory bounds, queue capacity, and thread/backpressure policy: none.Fix: Require mapped records by default. With
--allow-unmapped, retain wholly unmapped families but prefer mapped records when both types exist. Attribute removed records toRejectionReason::Unmapped.Tests cover simplex, duplex, and CODEC behavior, including half-mapped pairs and mapped-read depth.