Skip to content

chimeric: --chimMultimapNmax enumeration, and --chimFilter banGenomicN - #149

Open
BenjaminDEMAILLE wants to merge 2 commits into
scverse:mainfrom
BenjaminDEMAILLE:bd/chimeric-multimap
Open

chimeric: --chimMultimapNmax enumeration, and --chimFilter banGenomicN#149
BenjaminDEMAILLE wants to merge 2 commits into
scverse:mainfrom
BenjaminDEMAILLE:bd/chimeric-multimap

Conversation

@BenjaminDEMAILLE

@BenjaminDEMAILLE BenjaminDEMAILLE commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Chimeric detection: --chimMultimapNmax enumeration, and --chimFilter banGenomicN.

What changed

--chimMultimapNmax implements STAR's ChimericDetection::chimericDetectionMult. The existing path pins the best transcript as one segment and searches for a partner, so it can emit at most one chimera per read. The new path walks every transcript pair, keeps those within --chimMultimapScoreRange of the best chimeric score, and reports them all.

Two STAR behaviours the loop depends on:

  • The score floor ratchets. It starts at --chimScoreMin, rises above the best linear alignment score, again to readLength - chimScoreDropMax, then to best - chimMultimapScoreRange each time a better chimera appears. Pairs admitted early can be dropped later, so the final retain is not redundant with the in-loop test.
  • Over the cap reports nothing. More survivors than --chimMultimapNmax and STAR emits no chimera rather than an arbitrary subset.

--chimNonchimScoreDropMin gates the search: a read that already aligns linearly across its length is not a fusion candidate however well its halves score apart.

The tail shared by both paths — segment ordering, junction overhang, geometry, motif classification, non-GTAG penalty, score re-check — moved into finalize_chimera, so the two routes cannot drift apart.

--chimFilter is a post-detection filter, as in STAR: both detection routes funnel through apply_chim_filter, so neither can skip it. banGenomicN (the default) drops a junction whose flanking genomic bases are not real bases, since sequence around an assembly gap produces junctions that look clean by score and mean nothing. None disables filtering.

The ban tests the base value, as STAR does (G[pos] == 4), not whether the position is in bounds. Treating an unanswerable position as a ban would reject alignments STAR keeps.

Why

Three of the four flags were parsed but inert. --chimMultimapNmax 0 remains the default and selects the single-best path, so behaviour without the flag is unchanged.

Verification

New tests:

  • chim_multimap_reports_every_locus_within_the_score_range — a read whose 3' end maps equally well to two places: the old path reports 1 junction, --chimMultimapNmax 10 reports 2
  • chim_multimap_beyond_the_cap_reports_nothing
  • chim_multimap_score_range_drops_the_weaker_locus — and admits it again when the range widens
  • chim_multimap_requires_the_linear_alignment_to_leave_the_read_unexplained
  • chim_multimap_is_off_by_default
  • chim_filter_bans_a_genomic_n_at_the_junction_and_none_keeps_it

Gate green: 566 lib + integration tests, clippy -D warnings, cargo fmt --check, MSRV 1.89.

Output-neutral on non-chimeric runs. On chimeric runs at default flags, output is unchanged: banGenomicN can only remove a junction, and multimap detection is off.

🤖 Generated with Claude Code

BenjaminDEMAILLE and others added 2 commits July 28, 2026 23:49
Adds `--chimFilter`, `--chimMultimapNmax`, `--chimMultimapScoreRange` and
`--chimNonchimScoreDropMin`.

`--chimFilter` is implemented. STAR treats it as a post-detection filter rather
than a condition inside the search, and so does this: every detection route —
the intra-mate path and the inter-mate path — funnels through
`apply_chim_filter`, so a route cannot quietly skip it. `banGenomicN` (STAR's
default) drops a junction whose flanking genomic bases are not real bases:
sequence around an assembly gap yields junctions that look clean by score and
mean nothing. `None` keeps everything.

One detail worth stating because it changes what the code does: the ban tests
the base *value*, as STAR does (`G[pos] == 4`), not whether the position is in
bounds. Treating an unanswerable position as a ban would reject alignments STAR
keeps, and it broke an existing test whose fixture deliberately uses
out-of-range coordinates.

The three multimap knobs are declared and validated but not yet acted on:
`--chimMultimapNmax 0` is STAR's default and means the existing single-best
detection, which is what this branch still does. Reporting several chimeras per
read is `chimericDetectionMult`, a separate search with its own output columns,
and it deserves its own change rather than being smuggled in behind a flag
declaration. Declaring them now keeps the parameter surface honest and gives
that work somewhere to land.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
STAR's `ChimericDetection::chimericDetectionMult`. The old path pins the best
transcript as one segment and searches for a partner, so it can only ever emit
one chimera; a read whose fusion partner maps equally well to two places has one
of them silently discarded. This walks every transcript pair instead and reports
all that survive.

Two behaviours the loop depends on, both STAR's:

The score floor ratchets. It starts at `--chimScoreMin`, rises above the best
linear alignment score, again to `readLength - chimScoreDropMax`, and then every
new best chimera raises it to `best - chimMultimapScoreRange`. Pairs admitted
early can therefore be dropped later, which is why the final `retain` is not
redundant with the in-loop test.

More survivors than `--chimMultimapNmax` means no output at all, not a truncated
list. The read is chimerically multimapping beyond the cap and STAR reports
nothing rather than an arbitrary subset.

`--chimNonchimScoreDropMin` gates the whole search: a read that already aligns
linearly across its length is not a fusion candidate, however well its halves
score apart. All three knobs were parsed but inert before this; they are real
now, and the default `--chimMultimapNmax 0` keeps the old path unchanged.

The tail shared by both paths — order the segments by read position, check the
junction overhang and the geometry, classify the motif, apply the non-GTAG
penalty, re-check the score — moves into `finalize_chimera`, so the two routes
cannot drift apart. It returns the post-penalty score alongside the alignment
because the ratchet needs that number, not `ChimericAlignment::total_score`,
which is the plain sum of the segment scores and knows nothing about the read
overlap or the penalty.

Tests cover the discriminator (two equal loci: old path 1, multimap 2), the cap
reporting nothing, the score range admitting and excluding the weaker locus, the
nonchim gate, and the knob being inert at its default.
@BenjaminDEMAILLE BenjaminDEMAILLE changed the title chimeric: --chimFilter banGenomicN, and the multimap knobs chimeric: --chimMultimapNmax enumeration, and --chimFilter banGenomicN Jul 29, 2026
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