Skip to content

fix(align): a soft-clipped variant of a pair is not a second locus - #254

Open
BenjaminDEMAILLE wants to merge 1 commit into
mainfrom
fix/pair-subset-loci
Open

fix(align): a soft-clipped variant of a pair is not a second locus#254
BenjaminDEMAILLE wants to merge 1 commit into
mainfrom
fix/pair-subset-loci

Conversation

@BenjaminDEMAILLE

Copy link
Copy Markdown
Contributor

Closes #31.

What the extra loci were

Not different places in the genome. The reported NH=20 read carries 24 records at AS:i:174 and 16 at AS:i:173, and the 173s are the same alignments with the leading 9-base exon soft-clipped off:

174   pos 1497     9M31373N9M114635N9M12705N74M
173   pos 32879    9S9M114635N9M12705N74M
173   pos 160237   27S74M

STAR produces only the 174s.

The rule, and why it was not firing

stitchWindowAligns.cpp drops a transcript whose blocks are a subset of another's and which scores lower:

uint uNew = trA.mappedLength - blocksOverlap(trA, *wTr[iTr]);
if (uNew==0 && Score < wTr[iTr]->maxScore) break;

The equivalent check here (stitch.rs:2385) is gated on wt.exons.len() == existing.exons.len(). These variants have one exon fewer, so they slip past the comparison meant to catch them.

This PR applies the rule to whole pairs, after the score-range filter: a pair whose mate1 and mate2 are both covered by a better-scoring pair is dropped. Equal scores keep both, matching STAR's strict <.

Measured

nf-core/rnaseq test data, 50 000 pairs, --outFilterMultimapNmax 20:

STAR before after
deepest NH 14 20 14
uniquely mapped 41691 41665 41684
multi-mapped 934 961 942

The whole NH histogram now matches STAR's shape at the tail (…7:4 9:2 12:4 14:2 on both sides).

Project's yeast tier, 10 000 pairs of ERR12389696 (via test/yeast_tier.sh in #253):

STAR before after
uniquely mapped 7861 7860 7860
same chr/pos/CIGAR 98.4825% 98.4825%
same NH 99.9644% 99.9763%

So: the fixture that showed the bug moves toward STAR on three figures, and the faithfulness benchmark does not move except for a small NH improvement.

Three attempts that are not in this diff

Worth recording, because each looked right and each cost about 39 uniquely mapped reads on the yeast tier:

  1. Removing the same_structure gate in the stitcher. Faithfulness 98.48% → 97.98%, 92 mates lost.
  2. Applying the rule to single transcripts per window after finalisation. Same loss.
  3. Applying it to single transcripts after the quality filters. Same loss.

The reason is the same in all three: a subset alignment is sometimes the only one that survives the score and overhang gates, and align_read is reused by the paired-end mate-overlap merge with those gates disabled, so pruning there removes candidates the pairing stage would have used. Operating on assembled pairs avoids both.

I also mis-measured once along the way: an earlier edit to stitch.rs followed me across a branch switch and sat in three consecutive "clean" runs, which is why they returned suspiciously identical numbers. The figures above are from a tree whose only change is this one, checked with git diff --stat.

Tests

Four unit tests on the new rule: the reported case, the same case with the subset arriving first (order must not decide), two genuinely distinct loci at different scores (both kept), and an equal-scoring subset (kept, as STAR keeps it).

cargo test, cargo clippy --all-targets -- -D warnings on a cold cache, and cargo fmt --check are green.

🤖 Generated with Claude Code

#31 reported NH up to 20 where STAR caps at 14 on the same data. The extra
loci are not different places in the genome: they are the same alignments with
the leading exon soft-clipped off, scoring one point lower, kept as separate
multimapper entries.

STAR removes them in stitchWindowAligns.cpp, where a transcript whose blocks
are a subset of another's and which scores lower is dropped. The equivalent
check here is gated on the two transcripts having the same number of exons,
and these variants have one fewer, so they slip past the very comparison meant
to catch them.

The rule is applied to whole pairs, after the score-range filter: a pair whose
mate1 and mate2 are both covered by a better-scoring pair is dropped. Equal
scores keep both, matching STAR's strict comparison.

Measured, nf-core/rnaseq test data (50 000 pairs, --outFilterMultimapNmax 20):

  deepest NH        STAR 14   before 20   after 14
  uniquely mapped   STAR 41691   before 41665   after 41684
  multi-mapped      STAR 934     before 961     after 942

and on the project's yeast tier (10 000 pairs of ERR12389696), no regression:

  uniquely mapped   STAR 7861   before 7860   after 7860
  same chr/pos/CIGAR            before 98.4825%   after 98.4825%
  same NH                       before 99.9644%   after 99.9763%

Three earlier attempts at this regressed the yeast tier and are not in this
diff: applying the rule inside the stitcher, and applying it to single
transcripts either before or after the quality filters. All three cost ~39
uniquely mapped reads, because a subset alignment is sometimes the only one
that survives the score and overhang gates, and because align_read is reused
for the paired mate-overlap merge with those gates disabled.

Closes #31.
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.

Multi-mapper NH cap differs from STAR (NH up to 20 vs STAR's 7); possibly missing --outFilterMultimapScoreRange

1 participant