Skip to content

Add IsovarReadPhasing adapter for RNA-read variant phasing - #183

Merged
iskandr merged 8 commits into
masterfrom
read-phasing-adapter
May 14, 2026
Merged

Add IsovarReadPhasing adapter for RNA-read variant phasing#183
iskandr merged 8 commits into
masterfrom
read-phasing-adapter

Conversation

@iskandr

@iskandr iskandr commented May 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Addresses #182. Full closure depends on the companion isovar follow-up (#184), which moves the mutant-transcript half of the existing VarcodeAdapter into a separate IsovarMutantTranscript class implementing varcode's MutantTranscriptSource Protocol. Both halves then plug into varcode.ReadPhaseResolver directly; the issue #182 acceptance test will run end-to-end after that.

Design notes

  • Backed entirely by data already on IsovarResult -- no new variant-identification logic.
  • has_evidence is true iff the variant is in the input set and has num_alt_fragments > 0.
  • partners_in_cis returns a deterministically-ordered tuple (sorted by contig, start, ref, alt) so callers can rely on ordering in snapshot tests.
  • Adapter is duck-typed on three attributes (variant, num_alt_fragments, phased_variants_in_supporting_reads), documented in the module's "Contract" section so non-Isovar producers (long-read RNA tools, hand-rolled stubs) can target the same shape.

Related upstream work

Test plan

  • pytest tests/test_read_phasing.py -- 16 unit + integration tests:
    • 8 unit tests on hand-rolled IsovarResults: alt-evidence true/false, unknown variants, partners returned/empty, deterministic ordering, __repr__, and a doctest.testmod runner that executes the module's documented example.
    • 4 pipeline smoke tests against the b16 RNA fixture via run_isovar (the b16 fixture has 4 variants on 4 different chromosomes, so no actual co-phasing — these verify the adapter consumes real Isovar output without errors).
    • 2 integration tests driving isovar.phasing.annotate_phased_variants directly with hand-rolled inputs to exercise the non-empty-partner path: one for a 3-node v1-v2-v3 topology where v1↔v3 isn't directly bridged, one for the upstream threshold propagating through.
    • 1 synthetic-BAM end-to-end test that builds 7 pysam-shaped reads via MockAlignmentFile/make_pysam_read covering a somatic SNV at chr1:4 and a nearby germline SNP at chr1:8, walks them through the full ReadCollectorannotate_phased_variantsIsovarReadPhasing stack, and asserts the somatic and germline calls are reported as in-cis partners. Canonical motivating scenario for Germline-aware effect prediction (umbrella) varcode#268.
    • 1 doctest-runner (counted above).
  • pytest tests/test_phasing.py tests/test_varcode_adapter.py -- no regressions in adjacent phasing code.

Exposes IsovarResult.phased_variants_in_supporting_reads through a
narrow has_evidence/partners_in_cis interface, suitable as a read-
phasing source for varcode's germline-aware effect prediction.

Distinct from VarcodeAdapter (which implements the full
IsovarAssemblyProvider protocol): IsovarReadPhasing answers only the
phasing question and so does not require assembled contigs or mutant
transcripts.

Closes #182.
@coveralls

coveralls commented May 14, 2026

Copy link
Copy Markdown

Coverage Status

coverage: 93.592% (+0.08%) from 93.517% — read-phasing-adapter into master

…epr__

- Replace the broken docstring example (referenced undefined names) with
  a doctest-runnable one built on SimpleNamespace, executed via a
  dedicated `test_docstring_examples_run` test.
- Add a module-level "Contract" section spelling out the three
  attributes the adapter reads from each input result. This is intended
  as the basis for the matching `ReadPhasingSource` protocol on the
  varcode side.
- Note the symmetry guarantee inherited from
  `compute_phasing_counts` / `threshold_phased_variant_counts`.
- Add `__repr__` for friendlier debugging.
iskandr added 4 commits May 14, 2026 11:42
- Add four integration tests that exercise IsovarReadPhasing through a
  full run_isovar invocation on the b16 RNA fixture (same data the
  varcode_adapter tests use). Cover: has_evidence matches
  num_alt_fragments; partners are a subset of the input set; partners
  exactly match phased_variants_in_supporting_reads; partners are
  symmetric. The b16 fixture's partner sets may be empty -- the
  assertions are structural and pass either way while still catching
  drop/fabricate/asymmetry regressions in the adapter.
- Remove the "last entry wins" sentence from `__init__`'s docstring.
  It pinned an accidental dict-comprehension detail as a contract; not
  worth promising.
The b16 fixture has four variants on four different chromosomes, so
`run_isovar` against it never produces non-empty
`phased_variants_in_supporting_reads` -- the b16 tests verify wiring
but not phased-partner behavior end-to-end.

Add two integration tests that drive
`isovar.phasing.annotate_phased_variants` directly with hand-rolled
results sharing alt-read names, then exercise `IsovarReadPhasing`
against the annotated output:

  * test_adapter_consistent_with_annotate_phased_variants verifies
    the adapter exposes exactly what the upstream pipeline populates,
    on a v1<-(r12a,r12b)->v2<-(r23)->v3 topology that v1/v3 don't
    directly bridge.
  * test_adapter_respects_min_shared_fragments_threshold verifies a
    single shared read below threshold yields no partners.

Also relabel the b16 block as a pipeline smoke test and point readers
at the new integration tests for phased-partner coverage.
Builds a synthetic mini-BAM (via MockAlignmentFile + make_pysam_read)
with reads spanning an imagined somatic SNV at chr1:4 (T>G) and a
nearby germline SNP at chr1:8 (T>A): three reads carry both alts,
two carry the somatic alt only, two carry the germline alt only.

Walks the full ReadCollector -> annotate_phased_variants ->
IsovarReadPhasing stack and verifies the somatic and germline calls
are reported as in-cis partners.

This is the canonical motivating scenario for openvax/varcode#268's
germline-aware effect prediction: a somatic mutation whose codon
overlaps a germline polymorphism, where the protein outcome depends
on whether the two are in cis -- and only RNA-read co-occurrence
disambiguates.
…nterface

* Rename test_adapter_respects_min_shared_fragments_threshold to
  test_threshold_filter_propagates_through_adapter and explain that
  the adapter is passive here -- the test asserts the threshold
  propagates rather than being silently undone.
* Document in the module Contract that partners_in_cis and
  has_evidence answer independent questions and are not
  cross-checked. Note that outputs from run_isovar /
  annotate_phased_variants never exhibit a has_evidence=False with a
  non-empty partners_in_cis, since the upstream pipeline only
  populates partners for variants with shared supporting reads.
iskandr added 2 commits May 14, 2026 14:31
Varcode is dropping Isovar-named identifiers from its public surface
(openvax/varcode#378, openvax/varcode#379). Replace the docstring
reference to the old `IsovarAssemblyProvider` Protocol with the new
`ReadPhasingSource` + `MutantTranscriptSource` split, name the matching
consumer (`ReadPhaseResolver`), and point at #184 for the
companion heavy-adapter rework that will split `VarcodeAdapter` into
two narrower classes.

No code change; the adapter's method names (`has_evidence`,
`partners_in_cis`) already match the new `ReadPhasingSource` Protocol.
Phrase the relationship as "implements the shape that varcode adopts in
#378/#379" rather than "satisfies varcode's ReadPhasingSource Protocol",
so a reader who lands on master between isovar#183 merging and
varcode#379 merging doesn't see a docstring claiming a varcode symbol
that doesn't yet resolve. No behavior change.
@iskandr
iskandr merged commit c3e1ef2 into master May 14, 2026
6 checks passed
iskandr added a commit that referenced this pull request May 14, 2026
Bump version to 1.6.0 for IsovarReadPhasing (#183)
iskandr added a commit to openvax/varcode that referenced this pull request May 14, 2026
* Generalize phasing API; remove Isovar-named identifiers (bump 4.24.0)

Closes #378. The phasing Protocol bundled two unrelated concerns
(read-phasing + mutant-transcript construction) and named them after
Isovar, even though varcode imports nothing isovar-shaped. That
made the dependency direction look circular (isovar already declares
varcode>=4.15) and forced any non-Isovar source to either lie about
its identity or build a wrapper.

Split into two narrow Protocols, both fully generic:

- ReadPhasingSource: has_evidence(variant) + partners_in_cis(variant).
- MutantTranscriptSource: mutant_transcript(variant, transcript).

IsovarPhaseResolver renamed to ReadPhaseResolver. Accepts any
ReadPhasingSource; routes mutant_transcript() to the wrapped source
when it also satisfies MutantTranscriptSource (otherwise returns None
silently rather than raising). Source tag flipped from "isovar" to
"read_phasing".

The actual implementation classes (Isovar's IsovarReadPhasing in
openvax/isovar#183; the heavier mutant-transcript adapter tracked in
openvax/isovar#184) stay in isovar, where the dependency direction
already supports them. Varcode no longer mentions Isovar by name on
any public symbol.

Hard rename, no deprecation alias.

Test suite: tests/test_isovar_phase_resolver.py renamed to
tests/test_read_phase_resolver.py with the stub updated to satisfy
both new Protocols. test_haplotype_effect.py + test_vcf_phase_resolver.py
updated to match. 1188/1190 pass; 2 pre-existing skips.

* Rename resolver .source -> .phase_source to disambiguate from Outcome.source; trim Isovar-named examples from docstrings
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.

2 participants