Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,23 @@
## [Unreleased]

**Breaking**
- Phasing API generalized; Isovar-named identifiers removed from the
varcode public surface ([#378](https://github.com/openvax/varcode/issues/378)).
Varcode no longer imports or names any upstream tool — implementations
of the new generic Protocols live in their respective packages
(e.g. `isovar.IsovarReadPhasing`, openvax/isovar#183).
- `IsovarAssemblyProvider` (Protocol) **removed**, split into:
- `ReadPhasingSource` with `has_evidence(variant) -> bool` and
`partners_in_cis(variant) -> Sequence[Variant]`.
- `MutantTranscriptSource` with
`mutant_transcript(variant, transcript) -> Optional[MutantTranscript]`.
- `IsovarPhaseResolver` renamed to `ReadPhaseResolver`. Constructor
accepts any `ReadPhasingSource`; routes `mutant_transcript(...)`
to the wrapped source when it also satisfies `MutantTranscriptSource`.
Returns `None` otherwise instead of raising.
- Resolver `source` tag changed from `"isovar"` to `"read_phasing"`
on `ReadPhaseResolver`. Consumers filtering effects by phase
source need to update their filter values.
- `varcode.effects.effect_classes.PhaseAmbiguousEffect` renamed to
`PhaseCandidateSet`. No deprecation alias — update imports.
Public surface (`.candidates`, `.outcomes`, `.most_likely`,
Expand Down
12 changes: 10 additions & 2 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,17 @@ Auto-generated from in-source docstrings via

## Phasing

### `varcode.IsovarPhaseResolver`
### `varcode.ReadPhasingSource`

::: varcode.IsovarPhaseResolver
::: varcode.ReadPhasingSource

### `varcode.MutantTranscriptSource`

::: varcode.MutantTranscriptSource

### `varcode.ReadPhaseResolver`

::: varcode.ReadPhaseResolver

### `varcode.VCFPhaseResolver`

Expand Down
7 changes: 4 additions & 3 deletions docs/germline.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ possibility set** — one classified effect per haplotype hypothesis

- `VCFPhaseResolver(merged_phased.vcf)` — reads `PS` tags from a
WhatsHap- or HapCUT2-phased merged VCF.
- `IsovarPhaseResolver(provider)` — checks which haplotype the
somatic was observed on in RNA reads.
- `ReadPhaseResolver(source)` — wraps any RNA-phasing source
(typically an Isovar adapter shipped by `openvax/isovar`) to check
which haplotype the somatic was observed on in RNA reads.
- Anything implementing `in_cis(v1, v2, transcript) -> bool | None`.

Phase known → single `MutationEffect`. Phase unknown →
Expand Down Expand Up @@ -109,7 +110,7 @@ print("trans ->", type(eff_trans).__name__, eff_trans.short_description)
```

In real code use `VCFPhaseResolver(merged_phased.vcf)` or
`IsovarPhaseResolver(provider)` — same `phase_resolver=` slot.
`ReadPhaseResolver(source)` — same `phase_resolver=` slot.

## When does this matter?

Expand Down
102 changes: 54 additions & 48 deletions tests/test_haplotype_effect.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
variants are in cis according to a phase resolver, varcode builds a
single joint :class:`MutantTranscript` rather than computing
per-variant effects independently. Tests exercise both the
:class:`IsovarPhaseResolver` and :class:`VCFPhaseResolver` backends.
:class:`ReadPhaseResolver` and :class:`VCFPhaseResolver` backends.
"""

import os
Expand All @@ -25,8 +25,8 @@
import pytest

from varcode import (
IsovarPhaseResolver,
MutantTranscript,
ReadPhaseResolver,
VCFPhaseResolver,
Variant,
VariantCollection,
Expand Down Expand Up @@ -86,29 +86,28 @@ def test_apply_variants_order_independent():


# -----------------------------------------------------------------
# Isovar-resolver-driven joint effect
# Read-phasing-resolver-driven joint effect
# -----------------------------------------------------------------


class _StubAssemblyProvider:
def __init__(self, contigs):
self._contigs = contigs
class _StubReadPhasingSource:
def __init__(self, phasing=None, mutant_transcripts=None):
self._phasing = dict(phasing) if phasing else {}
self._mts = dict(mutant_transcripts) if mutant_transcripts else {}

def has_contig(self, variant, transcript):
return (variant, transcript.id) in self._contigs
def has_evidence(self, variant):
return variant in self._phasing

def variants_in_contig(self, variant, transcript):
entry = self._contigs.get((variant, transcript.id))
return entry[0] if entry is not None else ()
def partners_in_cis(self, variant):
return self._phasing.get(variant, ())

def mutant_transcript(self, variant, transcript):
entry = self._contigs.get((variant, transcript.id))
return entry[1] if entry is not None else None
return self._mts.get((variant, transcript.id))


def test_isovar_resolver_produces_haplotype_effect():
"""Two cis variants via Isovar stub → collection contains a
HaplotypeEffect with both variants and the contig-derived
def test_read_resolver_produces_haplotype_effect():
"""Two cis variants via read-phasing stub → collection contains a
HaplotypeEffect with both variants and the resolver-provided
MutantTranscript (not DNA inference)."""
from pyensembl import cached_release
g = cached_release(81)
Expand All @@ -118,29 +117,31 @@ def test_isovar_resolver_produces_haplotype_effect():
stub_mt = MutantTranscript(
reference_transcript=transcript,
cdna_sequence="ACGT" * 50,
mutant_protein_sequence="MISOVAR" + "A" * 100,
annotator_name="isovar",
mutant_protein_sequence="MOBSERVED" + "A" * 100,
annotator_name="upstream_rna_tool",
)
contigs = {
(v1, transcript.id): ((v1, v2), stub_mt),
(v2, transcript.id): ((v1, v2), stub_mt),
}
resolver = IsovarPhaseResolver(_StubAssemblyProvider(contigs))
source = _StubReadPhasingSource(
phasing={v1: (v1, v2), v2: (v1, v2)},
mutant_transcripts={
(v1, transcript.id): stub_mt,
(v2, transcript.id): stub_mt,
},
)
resolver = ReadPhaseResolver(source)
collection = VariantCollection([v1, v2])
effects = collection.effects(phase_resolver=resolver)
haplotype_effects = [e for e in effects if isinstance(e, HaplotypeEffect)]
assert len(haplotype_effects) == 1
he = haplotype_effects[0]
assert set(he.variants) == {v1, v2}
assert he.transcript is transcript
assert he.phase_source == "isovar"
# Prefers the Isovar contig over DNA inference.
assert he.phase_source == "read_phasing"
# Prefers the resolver-provided contig over DNA inference.
assert he.mutant_transcript is stub_mt
# Protein passes through.
assert he.mutant_protein_sequence.startswith("MISOVAR")
assert he.mutant_protein_sequence.startswith("MOBSERVED")


def test_isovar_resolver_haplotype_and_per_variant_coexist():
def test_read_resolver_haplotype_and_per_variant_coexist():
"""Per-variant effects stay on the collection alongside the
HaplotypeEffect — additive, not replacement."""
from pyensembl import cached_release
Expand All @@ -152,13 +153,15 @@ def test_isovar_resolver_haplotype_and_per_variant_coexist():
reference_transcript=transcript,
cdna_sequence="ACGT" * 50,
mutant_protein_sequence="M" + "A" * 99,
annotator_name="isovar",
annotator_name="upstream_rna_tool",
)
source = _StubReadPhasingSource(
phasing={v1: (v1, v2)},
mutant_transcripts={(v1, transcript.id): stub_mt},
)
contigs = {(v1, transcript.id): ((v1, v2), stub_mt)}
resolver = IsovarPhaseResolver(_StubAssemblyProvider(contigs))
resolver = ReadPhaseResolver(source)
collection = VariantCollection([v1, v2])
effects = collection.effects(phase_resolver=resolver)
# Per-variant Substitution for v1 AND v2 are still present.
per_variant_for_v1 = [
e for e in effects
if e.variant == v1 and not isinstance(e, HaplotypeEffect)]
Expand All @@ -169,9 +172,7 @@ def test_isovar_resolver_haplotype_and_per_variant_coexist():
assert per_variant_for_v2


def test_isovar_resolver_no_haplotype_when_only_one_variant():
"""Single variant with a contig → no HaplotypeEffect (minimum
group size is 2)."""
def test_read_resolver_no_haplotype_when_only_one_variant():
from pyensembl import cached_release
g = cached_release(81)
transcript = g.transcript_by_id("ENST00000003084")
Expand All @@ -180,16 +181,18 @@ def test_isovar_resolver_no_haplotype_when_only_one_variant():
reference_transcript=transcript,
cdna_sequence="ACGT" * 50,
mutant_protein_sequence="M" + "A" * 99,
annotator_name="isovar",
annotator_name="upstream_rna_tool",
)
contigs = {(v1, transcript.id): ((v1,), stub_mt)}
resolver = IsovarPhaseResolver(_StubAssemblyProvider(contigs))
source = _StubReadPhasingSource(
phasing={v1: (v1,)},
mutant_transcripts={(v1, transcript.id): stub_mt},
)
resolver = ReadPhaseResolver(source)
effects = VariantCollection([v1]).effects(phase_resolver=resolver)
assert not any(isinstance(e, HaplotypeEffect) for e in effects)


def test_isovar_resolver_no_haplotype_when_variants_trans():
"""Two variants with separate contigs → no HaplotypeEffect."""
def test_read_resolver_no_haplotype_when_variants_trans():
from pyensembl import cached_release
g = cached_release(81)
transcript = g.transcript_by_id("ENST00000003084")
Expand All @@ -198,17 +201,20 @@ def test_isovar_resolver_no_haplotype_when_variants_trans():
mt1 = MutantTranscript(
reference_transcript=transcript,
cdna_sequence="ACGT" * 50,
annotator_name="isovar")
annotator_name="upstream_rna_tool")
mt2 = MutantTranscript(
reference_transcript=transcript,
cdna_sequence="TGCA" * 50,
annotator_name="isovar")
# Each variant is on its own contig — they're trans.
contigs = {
(v1, transcript.id): ((v1,), mt1),
(v2, transcript.id): ((v2,), mt2),
}
resolver = IsovarPhaseResolver(_StubAssemblyProvider(contigs))
annotator_name="upstream_rna_tool")
# Each variant has evidence only of itself — they're trans.
source = _StubReadPhasingSource(
phasing={v1: (v1,), v2: (v2,)},
mutant_transcripts={
(v1, transcript.id): mt1,
(v2, transcript.id): mt2,
},
)
resolver = ReadPhaseResolver(source)
effects = VariantCollection([v1, v2]).effects(phase_resolver=resolver)
assert not any(isinstance(e, HaplotypeEffect) for e in effects)

Expand Down
Loading