Problem
varcode.phasing currently defines one Protocol — IsovarAssemblyProvider — that bundles two unrelated concerns:
- Read-phasing — "are these two variants observed on the same supporting reads?" Implemented today as
has_contig(variant, transcript) + variants_in_contig(variant, transcript).
- 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 PhaseAmbiguousEffect → PhaseCandidateSet 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
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
Problem
varcode.phasingcurrently defines one Protocol —IsovarAssemblyProvider— that bundles two unrelated concerns:has_contig(variant, transcript)+variants_in_contig(variant, transcript).MutantTranscriptfrom the assembled contig for this (variant, transcript)." Implemented asmutant_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:isovar.IsovarReadPhasingshipped in openvax/isovar#183).AssemblyProvidermis-describes the phasing case — Isovar isn't doing transcript assembly, just local cDNA reconstruction around a variant.has_contig,variants_in_contig) leak an implementation detail.openvax/isovar#183 ships
IsovarReadPhasingwith the new method names (has_evidence+partners_in_cis). It cannot plug intovarcode.IsovarPhaseResolvertoday 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
IsovarAssemblyProviderProtocol with two narrower ones:Why these names:
ReadPhasingSource— generic across data sources (Isovar, long-read assemblers, hand-rolled). The currentIsovarAssemblyProvidername conflates "what Isovar happens to ship" with "what phasing requires."has_evidence— neutral phrasing.has_contigwas 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 thetranscriptparameter 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
PhaseAmbiguousEffect→PhaseCandidateSetrename's approach).IsovarAssemblyProviderReadPhasingSource+MutantTranscriptSource.has_contig(variant, transcript)has_evidence(variant)variants_in_contig(variant, transcript)partners_in_cis(variant)mutant_transcript(variant, transcript)MutantTranscriptSource.IsovarPhaseResolveris updated to require onlyReadPhasingSource. Themutant_transcriptmethod on the resolver becomes optional — present iff the wrapped provider also satisfiesMutantTranscriptSource.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
ReadPhasingSource+MutantTranscriptSourceProtocols invarcode/phasing.py.IsovarAssemblyProvider. UpdateIsovarPhaseResolver.__init__to accept anyReadPhasingSource.IsovarPhaseResolver.in_cisto callhas_evidence+partners_in_cisinstead ofhas_contig+variants_in_contig.IsovarPhaseResolver.mutant_transcriptto short-circuitNonewhen the wrapped provider doesn't implementMutantTranscriptSource.tests/test_isovar_phase_resolver.py— the in-repoStubAssemblyProviderbecomes aStubReadPhasingSourcewith the new methods.VarcodeAdapterto implement both narrower protocols (separate isovar PR).vaxrank/tests/data/b16.f10/b16.combined.bam+b16.expressed.vcfonce the new isovar wiring lands.Out of scope
IsovarPhaseResolveritself (e.g. toReadPhaseResolver) to drop the Isovar-specific name. Worth doing eventually — the class isn't Isovar-bound anymore — but a separate change.Acceptance criterion
Same as openvax/isovar#182's acceptance test, but with this issue's varcode change actually making it work:
That snippet running — even on a tiny BAM — closes both this issue and isovar#182.
References
IsovarReadPhasingPR (matches the new protocol shape).varcode/phasing.py:34-146— currentIsovarAssemblyProvider+IsovarPhaseResolver.isovar/varcode_adapter.py:29— existingVarcodeAdapter, needs companion update.PhaseAmbiguousEffectrename precedent.