Phasing and germline API¶
See Phasing and Germline-aware annotation for input requirements and examples.
Phasing¶
varcode.ReadPhasingSource
¶
Bases: 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. Implementations
decide how strong the evidence is; consumers only see a boolean
membership question.
has_evidence(variant) -> bool
¶
partners_in_cis(variant) -> Sequence
¶
Variants observed in cis with variant — i.e. on the
same supporting reads / fragment / contig. May include
germline SNPs, nearby somatic variants, or variant itself
(implementations pick their convention). Empty sequence when
no evidence covers variant.
Source code in varcode/phasing.py
varcode.MutantTranscriptSource
¶
Bases: Protocol
Reports an observed mutant transcript for (variant, transcript).
Independent of phasing. A source can implement just
:class:ReadPhasingSource, just :class:MutantTranscriptSource,
or both. Consumers iterating effects use this channel to substitute
the observed mutant protein (from RNA assembly, long-read calling,
etc.) for the reference-inferred one.
mutant_transcript(variant, transcript)
¶
The :class:~varcode.MutantTranscript for
(variant, transcript), or None when this source has
no observed transcript for that pair.
varcode.MolecularPhaseResolver(phasing_source: ReadPhasingSource)
¶
Phase resolver backed by a :class:ReadPhasingSource (#269, #259).
Two variants are cis if the source reports them as co-observed. That's direct molecular evidence — not a probabilistic call.
Usage::
# Any object satisfying ReadPhasingSource works. Common
# implementation: an Isovar adapter shipped by openvax/isovar.
resolver = MolecularPhaseResolver(source)
effects = variants.effects(phase_resolver=resolver)
If source also satisfies :class:MutantTranscriptSource, the
resolver routes :meth:mutant_transcript calls to it — so any
effect whose (variant, transcript) is covered by the source
gets its :attr:~MutationEffect.mutant_transcript populated with
the observed mutant transcript.
Sources may also expose their own in_cis(v1, v2, transcript=None)
method. When present, the resolver delegates to that method instead
of reducing through :meth:ReadPhasingSource.partners_in_cis. This
lets direct-read sources return None for pairs without enough
co-covering evidence.
Source code in varcode/phasing.py
has_evidence(variant) -> bool
¶
mutant_transcript(variant, transcript)
¶
Return the observed :class:MutantTranscript for
(variant, transcript), or None when the wrapped source
doesn't implement :class:MutantTranscriptSource or has no
transcript for that pair.
Source code in varcode/phasing.py
in_cis(v1, v2, transcript=None) -> Optional[bool]
¶
Return True if v1 and v2 are co-observed by the
wrapped source, False if exactly one has evidence (so they
are on distinct physical molecules — trans), None when
neither has evidence.
transcript is accepted for interface symmetry with
:class:VCFPhaseResolver.in_cis but isn't consulted at the
Protocol layer — isoform-specific sources are not yet a
first-class concern. Reintroducible later as an optional
Protocol extension.
Source code in varcode/phasing.py
phased_partners(variant, transcript=None) -> Sequence
¶
Variants co-observed with variant — i.e. the cis set.
Empty when the source has no evidence for variant.
Source code in varcode/phasing.py
varcode.ReadPhaseResolver(phasing_source: ReadPhasingSource)
¶
Bases: MolecularPhaseResolver
Compatibility name for :class:MolecularPhaseResolver.
The old name is kept because it shipped in varcode 5.0.0. New code
should prefer :class:MolecularPhaseResolver, which better describes
sources backed by reads, paired fragments, assembled contigs, or other
direct molecular evidence.
Source code in varcode/phasing.py
varcode.RNAReadPhasingSource(bam_path: str, *, variants=None, min_mapping_quality: int = 20, min_base_quality: int = 20, min_alt_reads: int = 2, max_distance_from_read_edge: Optional[int] = 5, require_proper_pair: bool = True, skip_duplicates: bool = True, skip_secondary: bool = True, skip_supplementary: bool = True)
¶
BAM-backed phasing source for RNA read co-occurrence.
This is the lightweight alternative to an Isovar-style assembly
source. It reads quality-filtered alignments from an RNA-seq BAM and
answers whether variants are observed on the same read or paired-end
fragment. It does not assemble contigs and does not provide
mutant_transcript; callers that need observed mutant proteins
should use an assembly-backed source.
Usage::
source = RNAReadPhasingSource("tumor.rna.bam")
resolver = MolecularPhaseResolver(source)
effects = variants.effects(phase_resolver=resolver)
| PARAMETER | DESCRIPTION |
|---|---|
bam_path
|
Coordinate-sorted, indexed BAM path.
TYPE:
|
variants
|
Optional universe used by :meth:
TYPE:
|
min_mapping_quality
|
Minimum MAPQ for reads contributing evidence.
TYPE:
|
min_base_quality
|
Minimum base quality for SNV/MNV and insertion allele calls.
TYPE:
|
min_alt_reads
|
Minimum alt-supporting reads/fragments required for
TYPE:
|
max_distance_from_read_edge
|
Discard allele calls whose queried bases are closer than this
many bases to either read edge. Set to
TYPE:
|
require_proper_pair
|
For paired reads, discard fragments not marked proper pair. Unpaired reads are still accepted.
TYPE:
|
skip_duplicates
|
Standard SAM flag filters.
TYPE:
|
skip_secondary
|
Standard SAM flag filters.
TYPE:
|
skip_supplementary
|
Standard SAM flag filters.
TYPE:
|
Source code in varcode/rna_read_phasing.py
close()
¶
register_variants(variants)
¶
Register variants used by :meth:partners_in_cis.
This is optional for ordinary MolecularPhaseResolver.in_cis use,
where both queried variants are registered automatically.
Source code in varcode/rna_read_phasing.py
register_haplotype(variants, *, flanking_bases=5)
¶
Test a known local allele combination by its anchored RNA sequence.
| PARAMETER | DESCRIPTION |
|---|---|
variants
|
Nonoverlapping substitutions/deletions on one genome and contig. All alternate alleles form one hypothesis, not an assumed phase. Register competing hypotheses separately when necessary.
TYPE:
|
flanking_bases
|
Unchanged reference bases required on each side (default five).
TYPE:
|
Notes
A match requires the entire sequence, including both flanks, on ONE alignment, with the configured base-quality and read-edge filters. Equivalent D/N/split-gap encodings can then support the same known sequence. This does not establish a genomic deletion from an RNA skip. Reference is fetched from the variants' genome; unavailable sequence, overlapping edits and reference mismatches raise ValueError. No BAM bases are corrected, no missing bases filled, and no mates assembled. Registered variants use these full-context matches instead of individual CIGAR calls. Nonmatches remain unknown, not reference/trans evidence.
Source code in varcode/rna_read_phasing.py
supports_variant(variant) -> Optional[int]
¶
Count quality-filtered reads/fragments supporting variant.alt.
Returns None when the variant's contig is absent from the BAM.
Source code in varcode/rna_read_phasing.py
has_evidence(variant) -> bool
¶
True if the BAM has enough alt-supporting reads/fragments.
in_cis(v1, v2, transcript=None) -> Optional[bool]
¶
Return cis/trans from RNA read or fragment co-occurrence.
True means enough fragments support both alts. False
means enough fragments support one alt with the other's reference
allele. None means the BAM does not contain enough
co-covering evidence to decide.
Source code in varcode/rna_read_phasing.py
partners_in_cis(variant) -> Sequence
¶
Known registered variants observed in cis with variant.
Source code in varcode/rna_read_phasing.py
varcode.VCFPhaseResolver(variant_collection, sample)
¶
Phase resolver backed by VCF GT + PS FORMAT fields.
Reads the phase data that varcode's VCF loader already parses
into :class:~varcode.Genotype (via #267): whether the
GT delimiter was | (phased) or / (unphased), the
PS phase-set identifier, and the per-haplotype allele indices
in :attr:Genotype.alleles.
Two variants are cis when they sit in the same phase set on
the same haplotype slot, trans when they sit in the same
phase set on different slots, and the resolver returns None
("no evidence") for variants that aren't both phased, don't share
a phase set, or lack called alleles.
Compatible with any tool that writes standard-shaped VCF:
WhatsHap, HapCUT2, DeepVariant, GATK HaplotypeCaller, long-read
callers (PEPPER-DeepVariant, Clair3), population phasers
(SHAPEIT5, Eagle2). varcode doesn't care which one wrote the
file — it only reads GT and PS.
Multi-allelic sites are handled: varcode splits those rows into
one :class:~varcode.Variant per ALT, each with an
alt_allele_index preserved on the
:class:~varcode.VariantCollection metadata. The resolver maps
each variant to its GT-encoded index and asks "which haplotype
slot carries this specific alt?".
Single-sample by construction. Phase is per-sample; multi-sample VCFs need one resolver per sample.
Currently supplies the cis/trans query but does not attach a
:class:~varcode.MutantTranscript — DNA phasing alone doesn't
produce an assembled contig. The natural next step is a
HaplotypeEffect / multi-variant apply_variants_to_transcript
helper that, when two or more cis variants overlap the same
transcript, builds a single joint :class:MutantTranscript
applying all edits at once. That's a separate PR — this
resolver already has the inputs it needs (in_cis) to drive
the grouping.
Source code in varcode/phasing.py
in_cis(v1, v2, transcript=None) -> Optional[bool]
¶
Return True if v1 and v2 are on the same
haplotype in the same phase set, False if they're on
different haplotypes in the same phase set, None when
the phase relationship can't be determined (unphased GT,
different phase sets, uncalled alleles).
transcript is accepted for interface symmetry with
:class:MolecularPhaseResolver.in_cis but isn't consulted —
DNA-level phase is isoform-agnostic.
Source code in varcode/phasing.py
phased_partners(variant, transcript=None)
¶
Variants in the collection that are cis with variant
under this resolver — i.e. sit in the same phase set on the
same haplotype slot. Empty when variant isn't phased or
has no called alt in the sample.
Source code in varcode/phasing.py
varcode.apply_phase_resolver_to_effects(effects, phase_resolver)
¶
Post-process an :class:EffectCollection (or any iterable of
:class:MutationEffect) to attach observed
:class:MutantTranscript objects when the resolver has evidence.
Mutates each effect in place by setting
effect.mutant_transcript. Effects whose transcript isn't
resolvable or whose (variant, transcript) has no observed
transcript are left untouched — so this is safe to call on a mixed
collection where only some variants have RNA evidence.
Source code in varcode/phasing.py
Germline-aware annotation¶
varcode.GermlineContext(variants: 'VariantCollection', completeness: Completeness = Completeness.COMPLETE, reference_name: Optional[str] = None, metadata: Mapping[str, Any] = dict())
dataclass
¶
The patient's germline, packaged with completeness metadata and reference-build info for cross-VCF validation.
Construct via the from_* classmethods rather than instantiating
directly; the constructors apply the input-shape-specific
validation each route needs.
| ATTRIBUTE | DESCRIPTION |
|---|---|
variants |
The germline variants as a :class:
TYPE:
|
completeness |
How to interpret absence-of-a-call (see :class:
TYPE:
|
reference_name |
The genome reference these variants were called against —
TYPE:
|
metadata |
Open-ended dict for caller-supplied annotations (source caller name, sample identifier, normalization tool, etc.). Not interpreted by varcode; rides along for downstream consumers and serialization.
TYPE:
|
Examples:
Route 1 — full germline call set::
ctx = GermlineContext.from_germline_vcf("normal.vcf")
Route 2 — multi-sample VCF, extract a column. The user must declare completeness explicitly because absence-from-a-multi- sample column rarely means ref/ref::
ctx = GermlineContext.from_multi_sample_vcf(
"merged.vcf", sample="NORMAL", completeness=Completeness.SPARSE)
Direct construction (tests, custom pipelines)::
ctx = GermlineContext.from_variants(
germline_variants, completeness=Completeness.COMPLETE,
reference_name="GRCh38")
Explicit empty context — opt-in to reference-relative fallback::
ctx = GermlineContext.empty()
from_germline_vcf(path: str, *, completeness: Completeness = Completeness.COMPLETE, metadata: Optional[Mapping[str, Any]] = None, **load_vcf_kwargs) -> 'GermlineContext'
classmethod
¶
Load a full germline VCF into a context.
load_vcf_kwargs are passed through to
:func:varcode.load_vcf — for example genome= or
only_passing=False. The returned context defaults to
Completeness.COMPLETE; pass completeness= only if the
VCF is something other than a real germline call set.
Source code in varcode/germline.py
from_multi_sample_vcf(path: str, sample: str, *, completeness: Completeness, metadata: Optional[Mapping[str, Any]] = None, **load_vcf_kwargs) -> 'GermlineContext'
classmethod
¶
Load a multi-sample VCF and extract one sample's calls as the germline.
completeness is required (no default) — multi-sample VCFs
from somatic callers (Mutect2's NORMAL column, e.g.) are
almost always sparse, but pure-germline multi-sample VCFs
(1000G, gnomAD batch genotyping) are complete. Forcing the
caller to declare prevents subtle correctness bugs from
treating a sparse column as if absence implied ref/ref.
The sample is filtered post-load. If you need per-sample
zygosity information, pass include_info=True (the default)
and consult vc.metadata[variant]["sample_info"][sample]
downstream.
Source code in varcode/germline.py
from_variants(variants, *, completeness: Completeness = Completeness.COMPLETE, reference_name: Optional[str] = None, metadata: Optional[Mapping[str, Any]] = None) -> 'GermlineContext'
classmethod
¶
Construct from an already-built :class:VariantCollection
or any iterable of :class:Variant objects.
Useful for tests, hand-built pipelines, and downstream tools
that already have variants in memory and don't need to re-parse
a VCF. reference_name should be passed explicitly when not
carried by the variants themselves; otherwise cross-VCF
validation will be a no-op.
Source code in varcode/germline.py
empty() -> 'GermlineContext'
classmethod
¶
Explicit no-germline context. Use this in pipelines where
germline= is structurally required but the caller has no
germline data — it documents intent better than passing
None, and downstream code can rely on the kwarg always
being a :class:GermlineContext.
Effect prediction with an empty context falls through to reference-relative annotation (no patient transcript construction), with no warnings — the caller has explicitly opted in to the fallback.
Source code in varcode/germline.py
__bool__() -> bool
¶
Truthy when there's something to apply. EMPTY contexts
are falsy so if germline_context: reads idiomatically.
validate_against(somatic, *, validate_reference: bool = True) -> None
¶
Cross-validate this context with a somatic
:class:VariantCollection. Hard error on reference-build
mismatch unless validate_reference=False; warn on
suspicious shapes (empty germline, sparse coverage with no
overlap with somatic, etc.).
Called automatically by :meth:Variant.effects /
:meth:VariantCollection.effects when a context is supplied;
callers running validation manually can do so up front to fail
fast before annotation.
Source code in varcode/germline.py
variants_in_window(contig: str, start: int, end: int) -> Tuple
¶
Germline variants overlapping [start, end] on
contig (inclusive on both ends).
Used by the window-based lookup machinery (slice 2 of #268). Lazy interval index is built on first call and cached on the instance — subsequent calls are O(log N) per contig.
Returns a tuple (immutable) so callers can safely cache the result without worrying about the underlying index mutating.
Source code in varcode/germline.py
varcode.Completeness
¶
Bases: Enum
How exhaustive the germline call set is — the load-bearing flag that pins what absence of a call at a position means.
The same data structure ("a list of germline variants") can come from very different pipelines, and downstream effect prediction cannot make the right call without knowing which:
- If a position is absent from a real germline VCF emitted by a germline caller that examined the entire normal BAM, the patient is ref/ref there. Effect prediction proceeds reference-relative at that codon.
- If a position is absent from the
NORMALcolumn of a somatic-caller VCF, it likely means the somatic caller didn't emit a row — not that the position is ref/ref. The patient's germline state at that codon is unknown. The honest output is a possibility set including "unknown germline." - If a position is absent from a panel-of-normals filter list, it definitely doesn't imply ref/ref — the file only lists curated hotspots.
Mis-treating "absent" as "ref/ref" silently produces wrong germline-aware effects on somatic variants in long stretches of the genome the somatic caller never touched. The flag exists so that mistake fails loud (or at least produces an honest possibility set) instead of silently corrupting clinical annotation.
Values
+-------------------+-----------------------------------+--------------------------+
| Value | Typical pipeline of origin | Absence at a position |
+===================+===================================+==========================+
| :attr:COMPLETE | Germline caller (DeepVariant, | ⇒ ref/ref |
| | HaplotypeCaller, Strelka2 | |
| | germline) on the normal BAM | |
+-------------------+-----------------------------------+--------------------------+
| :attr:SPARSE | NORMAL column of a somatic | ⇒ unknown (probably |
| | tumor-vs-normal VCF (Mutect2, | ref/ref but not |
| | Strelka2 somatic, VarScan2 | queried). Honest output|
| | somatic) | is a possibility set. |
+-------------------+-----------------------------------+--------------------------+
| :attr:HOTSPOTS_ | Panel-of-normals filter list, | ⇒ definitely unknown. |
| ONLY | ClinVar pathogenic list, single- | Strictly weaker |
| | hotspot allowlists | evidence than SPARSE. |
+-------------------+-----------------------------------+--------------------------+
| :attr:EMPTY | "I have no germline data" — | n/a (no germline-aware |
| | explicit fallback, used so users | logic runs; equivalent to|
| | opt into reference-relative | not passing germline= at |
| | annotation rather than getting it | all) |
| | by accident from a missing kwarg | |
+-------------------+-----------------------------------+--------------------------+
What downstream slices do with this
Slice 3 of #268 wires germline= through annotator dispatch.
When a somatic variant lands in a transcript window that has no
germline calls, the annotator reads this flag to decide between:
COMPLETE→ patient is ref/ref in this window; emit a single reference-relative effect.SPARSE/HOTSPOTS_ONLY→ patient's germline is unknown in this window; emit a possibility set including the reference-relative effect plus "germline-unknown" outcomes so the user sees the uncertainty.EMPTY→ no germline-aware logic; reference-relative.
Constructors and defaults
:meth:GermlineContext.from_germline_vcf defaults to
COMPLETE because that's almost always what
a real germline VCF is.
:meth:GermlineContext.from_multi_sample_vcf requires the
caller to declare completeness explicitly (no default) — a
multi-sample VCF could be either, and silently defaulting
either direction is a correctness bug waiting to happen.
:meth:GermlineContext.empty always sets EMPTY.
varcode.predict_germline_aware_effect(somatic_variant, transcript, germline_ctx: GermlineContext, annotator, phase_resolver=None, window_fn=default_germline_window, max_hypotheses: int = 8)
¶
Predict the effect of somatic_variant on transcript
against the patient's germline-applied transcript.
Single entry point for germline-aware effect prediction.
:func:varcode.effects.predict_variant_effects calls this whenever
a non-empty :class:GermlineContext is supplied; otherwise it
bypasses the germline path entirely and the existing annotator
dispatch produces today's reference-relative output unchanged.
Behaviour by case:
- No germline in the somatic's window — patient transcript ≡
reference transcript at this locus; delegate to
annotatordirectly. SPARSE / HOTSPOTS_ONLY contexts mark the result witheffect.germline_unknown = Trueso consumers can see the uncertainty. - Germline in window, phase known (resolver answers, or
hemizygous, or all-cis-by-zygosity) — single patient haplotype;
classify against it via :func:
_classify_against_patient_baseline. - Germline in window, phase unknown — enumerate hypotheses
(capped via
max_hypotheses), classify each, wrap in :class:~varcode.effects.effect_classes.PhaseCandidateSet.
LOH (somatic matches germline at position+alt with het zygosity)
sets effect.is_loh = True regardless of which branch ran.
window_fn is the pluggable window selector — defaults to
:func:default_germline_window (codon-level, with splice-signal
expansion when the somatic is splice-adjacent). Callers that
need different windows pass their own.
Source code in varcode/germline.py
948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 | |
varcode.apply_germline_to_transcript(transcript, germline_ctx, somatic_variant=None)
¶
Apply germline variants from germline_ctx to transcript,
returning the patient's baseline :class:MutantTranscript.
Lower-level entry point for callers that want the patient
transcript directly without going through full effect prediction.
Used internally by :func:predict_germline_aware_effect; exposed
publicly for downstream tools (Isovar, Exacto) that want to
compute a custom analysis on the patient haplotype.
Behaviour:
- If
germline_ctxis empty, returnsNone. - If
somatic_variantis provided, restricts germline to the somatic's window (per :func:default_germline_window); else applies all germline variants overlapping any exon of the transcript. - If germline edits conflict (overlapping cDNA ranges) or land
outside the CDS, returns
Noneand the caller falls back.
The returned object is the same shape that
:func:varcode.mutant_transcript.apply_variants_to_transcript
produces: a :class:MutantTranscript carrying the germline
edits with mutant_protein_sequence populated when the edits
land after the CDS start.
Source code in varcode/germline.py
varcode.enumerate_phase_hypotheses(somatic_variant, germline_in_window, phase_resolver=None, max_hypotheses: int = 8) -> Tuple[PhaseHypothesis, ...]
¶
Enumerate plausible phase configurations of somatic_variant
relative to germline_in_window.
Three regimes:
- Hemizygous chromosome (chrX/Y/M, male X) — single haplotype; all germline-in-window is implicitly cis. One hypothesis.
- Resolver answers for every pair (
phase_resolver.in_cisreturns True/False for each(somatic, germline_v)) — a single deterministic hypothesis with cis/trans assigned per the resolver.phase_state="phased". - Phase unknown — enumerate all 2^n cis/trans assignments
across n germline variants. Cap at
max_hypotheses; emit a single"unknown"placeholder when the cap is exceeded (consumers see aTooManyHypothesesevidence flag).
The cap is configurable so downstream pipelines that tolerate more hypotheses (long-read with rich phasing, manual analyses) can raise it. Default 8 = up to 3 germline variants in a window fully unphased.
Source code in varcode/germline.py
743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 | |
varcode.detect_loh(somatic_variant, germline_in_window) -> bool
¶
True when somatic_variant is identical at (position, alt)
to a germline variant in the window.
LOH is the most common "looks somatic but isn't really" case —
the patient was germline het at this position, and the tumor lost
the reference allele, so the variant call says "alt" in tumor and
"het" in normal but the alt itself is the germline allele. We
flag the resulting effect with is_loh=True so consumers can
distinguish a true somatic mutation from a zygosity change.
Only same-position-and-alt counts. A position where germline and somatic disagree on alt is a different mutation, not LOH.
Source code in varcode/germline.py
varcode.default_germline_window(somatic_variant, transcript) -> Tuple[str, int, int]
¶
Default window for looking up germline variants relevant to a somatic variant on a transcript.
Returns (contig, start, end) covering the codon containing the
somatic variant — three reference bases on each side of
somatic_variant.start. This is the window from #268's table
for in-exon coding variants.
Larger windows (splice signal region for splice-adjacent variants,
same exon for frameshift candidates) are useful refinements but
don't change the API. Callers that need them pass a custom
window_fn to :func:predict_germline_aware_effect.
Splice-adjacent: when the somatic is within 6bp of an exon-intron boundary, expand to a 12bp window centered on the boundary so germline edits to the donor / acceptor signal show up in the lookup. This catches the "germline broke the splice site" case without forcing the caller to wire up a separate window function.