Skip to content

Split IsovarAssemblyProvider into ReadPhasingSource + MutantTranscriptSource #377

Description

@iskandr

Problem

varcode.phasing currently defines one Protocol — IsovarAssemblyProvider — that bundles two unrelated concerns:

  1. Read-phasing — "are these two variants observed on the same supporting reads?" Implemented today as has_contig(variant, transcript) + variants_in_contig(variant, transcript).
  2. Mutant-transcript reconstruction — "build me a MutantTranscript from the assembled contig for this (variant, transcript)." Implemented as mutant_transcript(variant, transcript).

IsovarPhaseResolver.in_cis(...) only consults concern (1). Concern (2) is consumed elsewhere (RNA-evidence outcome collapse, mutant protein sequence). Bundling them in a single Protocol means:

  • Every read-phasing source must also implement transcript assembly, even when it has no assembly to give (long-read RNA tools that only emit per-read variant co-occurrence; lightweight phasers like the new isovar.IsovarReadPhasing shipped in openvax/isovar#183).
  • The name AssemblyProvider mis-describes the phasing case — Isovar isn't doing transcript assembly, just local cDNA reconstruction around a variant.
  • Method names tied to assembly vocabulary (has_contig, variants_in_contig) leak an implementation detail.

openvax/isovar#183 ships IsovarReadPhasing with the new method names (has_evidence + partners_in_cis). It cannot plug into varcode.IsovarPhaseResolver today because the resolver still consumes the old protocol. This issue is the varcode follow-up that makes the end-to-end demo from openvax/isovar#182 actually run.

Proposed split

Replace the single IsovarAssemblyProvider Protocol with two narrower ones:

@runtime_checkable
class ReadPhasingSource(Protocol):
    """Reports per-variant read-level evidence and co-occurring partners.

    The minimum interface needed to answer ``in_cis(v1, v2)`` from
    read-level data — co-observation on the same supporting reads,
    same long-read fragment, same assembled contig, etc.
    """
    def has_evidence(self, variant) -> bool: ...
    def partners_in_cis(self, variant) -> Sequence[Variant]: ...


@runtime_checkable
class MutantTranscriptSource(Protocol):
    """Reports an observed mutant transcript for ``(variant, transcript)``.

    Used by RNA-evidence outcome collapse to substitute the observed
    protein for the reference-inferred one. Independent of phasing —
    a source can provide one, the other, or both.
    """
    def mutant_transcript(self, variant, transcript) -> Optional[MutantTranscript]: ...

Why these names:

  • ReadPhasingSource — generic across data sources (Isovar, long-read assemblers, hand-rolled). The current IsovarAssemblyProvider name conflates "what Isovar happens to ship" with "what phasing requires."
  • has_evidence — neutral phrasing. has_contig was Isovar-vocabulary; long-read tools don't have "contigs," they have fragments. The question is just "do you know anything about this variant?"
  • partners_in_cis — direct: returns the cis-set, not implementation detail like "variants on the contig."

IsovarPhaseResolver.in_cis(v1, v2, transcript) keeps the transcript parameter at the resolver level (it's needed for transcript-isoform-specific phasing in long-read pipelines that emit per-isoform contigs) but the protocol drops it for now — none of the shipped implementations are isoform-specific, and adding it back later is additive.

Migration

Hard rename, no alias (matches the recent PhaseAmbiguousEffectPhaseCandidateSet rename's approach).

Old New
IsovarAssemblyProvider Removed. Split into ReadPhasingSource + MutantTranscriptSource.
has_contig(variant, transcript) has_evidence(variant)
variants_in_contig(variant, transcript) partners_in_cis(variant)
mutant_transcript(variant, transcript) Unchanged, but lives on MutantTranscriptSource.

IsovarPhaseResolver is updated to require only ReadPhasingSource. The mutant_transcript method on the resolver becomes optional — present iff the wrapped provider also satisfies MutantTranscriptSource.

isovar.VarcodeAdapter (which implements the full old protocol — isovar/varcode_adapter.py:29) needs a small update to expose both narrower protocols. Filed as a follow-up on isovar side, blocking on this issue.

Scope

  • Define ReadPhasingSource + MutantTranscriptSource Protocols in varcode/phasing.py.
  • Remove IsovarAssemblyProvider. Update IsovarPhaseResolver.__init__ to accept any ReadPhasingSource.
  • Update IsovarPhaseResolver.in_cis to call has_evidence + partners_in_cis instead of has_contig + variants_in_contig.
  • Update IsovarPhaseResolver.mutant_transcript to short-circuit None when the wrapped provider doesn't implement MutantTranscriptSource.
  • Update tests/test_isovar_phase_resolver.py — the in-repo StubAssemblyProvider becomes a StubReadPhasingSource with the new methods.
  • CHANGELOG entry under Breaking.
  • Coordinate with isovar: update VarcodeAdapter to implement both narrower protocols (separate isovar PR).
  • End-to-end test against vaxrank/tests/data/b16.f10/b16.combined.bam + b16.expressed.vcf once the new isovar wiring lands.

Out of scope

  • Renaming IsovarPhaseResolver itself (e.g. to ReadPhaseResolver) to drop the Isovar-specific name. Worth doing eventually — the class isn't Isovar-bound anymore — but a separate change.
  • Async / lazy phasing sources (only compute partners on demand). Defer until a use case demands it.

Acceptance criterion

Same as openvax/isovar#182's acceptance test, but with this issue's varcode change actually making it work:

from varcode import load_vcf, GermlineContext, IsovarPhaseResolver
from isovar import run_isovar, IsovarReadPhasing

tumor = load_vcf("tumor.vcf", genome="GRCh38")
germline = GermlineContext.from_germline_vcf("normal.vcf")

isovar_results = run_isovar(variants=tumor, alignment_file="tumor.rna.bam")
phaser = IsovarPhaseResolver(IsovarReadPhasing(isovar_results))

effects = tumor.effects(germline=germline, phase_resolver=phaser)
# PhaseCandidateSet outputs collapse wherever Isovar saw somatic +
# germline on the same RNA reads.

That snippet running — even on a tiny BAM — closes both this issue and isovar#182.

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions