Skip to content

Annotators API

Ordinary annotation uses the default without selecting an implementation. See annotation choices for alternatives and Writing an annotator for extensions.

Annotators

varcode.EffectAnnotator

Bases: Protocol

Protocol for an object that annotates variant effects on transcripts.

Conforming objects expose:

  • name — short identifier (e.g. "fast") used in the registry and in serialized provenance.
  • :meth:annotate_on_transcript — the per-transcript entry point, returning a MutationEffect or NotImplemented.

Return Python's NotImplemented singleton when this particular input is unsupported. Public prediction APIs expose it as an Unresolved effect with a reason, retaining this annotator's provenance. They never silently replace a selected annotator's result with another's prediction. None is invalid; exceptions retain normal error-handling semantics.

Optionally exposes version (string) — used in CSV provenance headers so readers can detect when a serialized collection came from a different annotator version. Built-in annotators track varcode's version; third-party annotators expose their own.

Optionally implement annotate_with_context(variant, transcript, germline_ctx, phase_resolver=None) with the same return contract. Without it, nonempty germline context is unsupported. Empty context calls annotate_on_transcript as usual.

Optionally implement annotate_haplotype(variants, transcript, germline_ctx=None, phase_resolver=None) with the same return contract. Collections delegate each known-cis group to this method. A missing hook or refusal leaves an explicit unresolved group alongside individual effects.

The contract is duck-typed (@runtime_checkable) so third-party annotators don't need to inherit from varcode just to register.

annotate_on_transcript(variant: Variant, transcript: Transcript) -> MutationEffect | NotImplementedType

Predict a transcript consequence or explicitly decline the input.

PARAMETER DESCRIPTION
variant

Genomic variant, including StructuralVariant subclasses.

TYPE: Variant

transcript

Reference transcript selected for annotation.

TYPE: Transcript

RETURNS DESCRIPTION
MutationEffect or NotImplementedType

A consequence (which may retain multiple outcomes), or NotImplemented for an unsupported input. Never return None.

Source code in varcode/annotators/__init__.py
def annotate_on_transcript(
        self, variant: Variant, transcript: Transcript,
) -> MutationEffect | NotImplementedType:
    """Predict a transcript consequence or explicitly decline the input.

    Parameters
    ----------
    variant : Variant
        Genomic variant, including StructuralVariant subclasses.
    transcript : pyensembl.Transcript
        Reference transcript selected for annotation.

    Returns
    -------
    MutationEffect or types.NotImplementedType
        A consequence (which may retain multiple outcomes), or
        ``NotImplemented`` for an unsupported input. Never return None.
    """
    ...

varcode.FastEffectAnnotator

Annotate point edits and structural variants through one interface.

version = _varcode_version class-attribute instance-attribute

Built-in annotators track varcode's own version; third-party annotators expose their own. CSV provenance headers and round-trip warnings read from this field.

annotate_on_transcript(variant, transcript)

Delegate to the existing per-transcript prediction.

Returns the raw effect class (ExonicSpliceSite / SpliceDonor / etc. for splice disruptions), not wrapped in SpliceOutcomeSet. The wrap is applied at the collection boundary in :func:predict_variant_effects so internal consumers (notably the protein_diff annotator's dual dispatch) can still pattern-match on the raw class.

Source code in varcode/annotators/fast.py
def annotate_on_transcript(self, variant, transcript):
    """Delegate to the existing per-transcript prediction.

    Returns the raw effect class (``ExonicSpliceSite`` /
    ``SpliceDonor`` / etc. for splice disruptions), **not** wrapped
    in ``SpliceOutcomeSet``. The wrap is applied at the collection
    boundary in :func:`predict_variant_effects` so internal
    consumers (notably the ``protein_diff`` annotator's dual
    dispatch) can still pattern-match on the raw class.
    """
    if getattr(variant, "is_structural", False):
        from ..effects.structural import predict_structural_variant_effect
        return predict_structural_variant_effect(variant, transcript)
    # Lazy import avoids a circular dep at package import time.
    from ..effects.effect_prediction import (
        _predict_variant_effect_on_transcript_raw,
    )
    return _predict_variant_effect_on_transcript_raw(variant, transcript)

annotate_with_context(variant, transcript, germline_ctx, phase_resolver=None)

Use the established patient-baseline path for point edits.

Structural haplotype composition uses transcript_model. Do not send an SV's placeholder alleles to the point-edit builder.

Source code in varcode/annotators/fast.py
def annotate_with_context(
        self, variant, transcript, germline_ctx, phase_resolver=None):
    """Use the established patient-baseline path for point edits.

    Structural haplotype composition uses ``transcript_model``.
    Do not send an SV's placeholder alleles to the point-edit builder.
    """
    if getattr(variant, "is_structural", False):
        return NotImplemented
    from ..germline import predict_germline_aware_effect
    return predict_germline_aware_effect(
        variant, transcript, germline_ctx, annotator=self,
        phase_resolver=phase_resolver)

annotate_haplotype(variants, transcript, germline_ctx=None, phase_resolver=None)

Build the established point-edit or observed joint transcript.

Patient-baseline and structural composition require the shared transcript model. Decline them rather than silently ignoring context.

Source code in varcode/annotators/fast.py
def annotate_haplotype(
        self, variants, transcript, germline_ctx=None, phase_resolver=None):
    """Build the established point-edit or observed joint transcript.

    Patient-baseline and structural composition require the shared
    transcript model. Decline them rather than silently ignoring context.
    """
    from ..effects.effect_classes import HaplotypeEffect
    from ..mutant_transcript import apply_variants_to_transcript

    if germline_ctx:
        return NotImplemented
    mt = None
    if hasattr(phase_resolver, "mutant_transcript"):
        for variant in variants:
            mt = phase_resolver.mutant_transcript(variant, transcript)
            if mt is not None:
                break
    if mt is None:
        if any(getattr(v, "is_structural", False) for v in variants):
            return NotImplemented
        mt = apply_variants_to_transcript(variants, transcript)
    if mt is None:
        return NotImplemented
    return HaplotypeEffect(
        variants=variants, transcript=transcript, mutant_transcript=mt,
        phase_source=getattr(phase_resolver, "phase_source", None))

varcode.ProteinDiffEffectAnnotator

Classify effects by diffing translated mutant protein against the reference protein.

Experimental alternative for point edits. The parity harness compares it with the default on their shared domain and records intentional divergences with issue links. Structural variants return NotImplemented. Shared splice/germline classification helpers do not depend on this annotator.

annotate_with_context(variant, transcript, germline_ctx, phase_resolver=None)

Share the default's patient-baseline path for point edits.

Source code in varcode/annotators/protein_diff.py
def annotate_with_context(
        self, variant, transcript, germline_ctx, phase_resolver=None):
    """Share the default's patient-baseline path for point edits."""
    return FastEffectAnnotator.annotate_with_context(
        self, variant, transcript, germline_ctx,
        phase_resolver=phase_resolver)

annotate_haplotype(variants, transcript, germline_ctx=None, phase_resolver=None)

Use the shared point-edit builder for joint predictions.

Source code in varcode/annotators/protein_diff.py
def annotate_haplotype(
        self, variants, transcript, germline_ctx=None, phase_resolver=None):
    """Use the shared point-edit builder for joint predictions."""
    return FastEffectAnnotator.annotate_haplotype(
        self, variants, transcript, germline_ctx=germline_ctx,
        phase_resolver=phase_resolver)

annotate_on_transcript(variant, transcript)

Classify the effect of variant on transcript.

Runs fast first to detect splice-adjacent variants (which stay fast-classified); for everything else, builds a :class:MutantTranscript and diffs the translated protein.

Source code in varcode/annotators/protein_diff.py
def annotate_on_transcript(self, variant, transcript):
    """Classify the effect of ``variant`` on ``transcript``.

    Runs fast first to detect splice-adjacent variants (which
    stay fast-classified); for everything else, builds a
    :class:`MutantTranscript` and diffs the translated protein.
    """
    from pyensembl import Transcript
    if not isinstance(transcript, Transcript):
        raise TypeError(
            "Expected %s : %s to have type Transcript" % (
                transcript, type(transcript)))

    if getattr(variant, "is_structural", False):
        return NotImplemented

    if not transcript.is_protein_coding:
        return NoncodingTranscript(variant, transcript)

    if not transcript.complete:
        return IncompleteTranscript(variant, transcript)

    # Run fast to get the splice classification.
    fast_effect = FastEffectAnnotator().annotate_on_transcript(
        variant, transcript)

    # Pure-intronic splice effects: fast only — no protein-
    # level diff to compute. SpliceDonor/SpliceAcceptor are
    # IntronicSpliceSite subclasses, so one check covers all.
    if isinstance(fast_effect, IntronicSpliceSite):
        return fast_effect

    # Non-splice location-based classes (UTR, deep intronic): fast's
    # position-based classification is authoritative. The protein may
    # be unchanged but "outside the CDS" carries location semantics a
    # whole-protein diff doesn't. Closes #318.
    if isinstance(fast_effect, (ThreePrimeUTR, FivePrimeUTR, Intronic)):
        return fast_effect

    # ExonicSpliceSite: dual-dispatch. Fast provides the
    # splice class; protein-diff provides the alternate_effect
    # via protein diff.
    if isinstance(fast_effect, ExonicSpliceSite):
        mt = apply_variant_to_transcript(variant, transcript)
        if mt is not None and mt.mutant_protein_sequence is not None:
            alt = classify_from_protein_diff(
                variant=variant,
                transcript=transcript,
                ref_protein=str(transcript.protein_sequence),
                mut_protein=mt.mutant_protein_sequence,
                length_delta=mt.total_length_delta,
                mutant_transcript=mt)
            return ExonicSpliceSite(
                variant=variant,
                transcript=transcript,
                exon=fast_effect.exon,
                alternate_effect=alt)
        return fast_effect

    # Non-splice: protein-diff slow path.
    mt = apply_variant_to_transcript(variant, transcript)
    if mt is None or mt.mutant_protein_sequence is None:
        # UTR, ref-mismatch, splice-junction-spanning, etc.
        return fast_effect

    # A recognised start codon at the CDS start (e.g. CTG/TTG, or MT
    # ATA/GTG under table 2) initiates as Met, whatever it decodes to
    # internally. Ensembl writes such initiators literally in some
    # proteins and as Met in others. The mutant builder already uses
    # initiation semantics; normalize the reference too (#320, #537, #634).
    codon_table = codon_table_for_transcript(transcript)
    cds_start = min(transcript.start_codon_spliced_offsets)
    ref_first_codon = str(
        transcript.sequence)[cds_start:cds_start + 3].upper()
    mutant_cds_start = _mutant_cds_start(transcript, mt.edits)
    mut_first_codon = mt.cdna_sequence[
        mutant_cds_start:mutant_cds_start + 3].upper()
    ref_protein = initiator_as_met(
        str(transcript.protein_sequence), ref_first_codon, codon_table)
    mut_protein = mt.mutant_protein_sequence

    # Proteins match → Silent or AlternateStartCodon. Handle
    # both here because the shared classifier doesn't have
    # access to the cDNA edit offset for the correct aa_pos.
    if ref_protein == mut_protein:
        # An insertion immediately before the retained start can have a
        # CDS anchor on the minus strand, but only changes the 5' UTR.
        if mt.edits and all(e.cdna_end <= cds_start for e in mt.edits):
            return FivePrimeUTR(variant, transcript)
        if ref_first_codon != mut_first_codon:
            if mut_first_codon in codon_table.start_codons:
                return AlternateStartCodon(
                    variant=variant,
                    transcript=transcript,
                    ref_codon=ref_first_codon,
                    alt_codon=mut_first_codon)
        from ..effects.effect_classes import Silent
        edit = mt.edits[0] if mt.edits else None
        aa_pos = (edit.cdna_start - cds_start) // 3 if edit else 0
        aa_ref = (
            ref_protein[aa_pos]
            if 0 <= aa_pos < len(ref_protein)
            else "")
        return Silent(
            variant=variant,
            transcript=transcript,
            aa_pos=aa_pos,
            aa_ref=aa_ref)

    return classify_from_protein_diff(
        variant=variant,
        transcript=transcript,
        ref_protein=ref_protein,
        mut_protein=mut_protein,
        length_delta=mt.total_length_delta,
        mutant_transcript=mt)

Registry

varcode.register_annotator(annotator: _AnnotatorT) -> _AnnotatorT

Add an annotator to the process-global registry, keyed by its .name. Re-registering under the same name overrides the previous entry — this is deliberate so callers can swap implementations in tests.

PARAMETER DESCRIPTION
annotator

Implementation with a nonempty name and an annotation method.

TYPE: EffectAnnotator

RETURNS DESCRIPTION
EffectAnnotator

The same instance, preserving its concrete type.

RAISES DESCRIPTION
ValueError

The instance has no nonempty name.

Source code in varcode/annotators/registry.py
def register_annotator(annotator: _AnnotatorT) -> _AnnotatorT:
    """Add an annotator to the process-global registry, keyed by its
    ``.name``. Re-registering under the same name overrides the
    previous entry — this is deliberate so callers can swap
    implementations in tests.

    Parameters
    ----------
    annotator : EffectAnnotator
        Implementation with a nonempty name and an annotation method.

    Returns
    -------
    EffectAnnotator
        The same instance, preserving its concrete type.

    Raises
    ------
    ValueError
        The instance has no nonempty name.
    """
    name = getattr(annotator, "name", None)
    if not name:
        raise ValueError(
            "Annotator %r has no .name attribute; cannot register." % annotator)
    _REGISTRY[name] = annotator
    return annotator

varcode.get_annotator(name: str) -> EffectAnnotator

Look up a registered annotator by name. Raises KeyError if no annotator is registered under that name.

Source code in varcode/annotators/registry.py
def get_annotator(name: str) -> EffectAnnotator:
    """Look up a registered annotator by name. Raises ``KeyError``
    if no annotator is registered under that name.
    """
    return _REGISTRY[name]

varcode.get_default_annotator() -> EffectAnnotator

Return the annotator currently configured as the default ("transcript_model" unless changed via :func:set_default_annotator or :func:use_annotator).

Source code in varcode/annotators/registry.py
def get_default_annotator() -> EffectAnnotator:
    """Return the annotator currently configured as the default
    (``"transcript_model"`` unless changed via :func:`set_default_annotator` or
    :func:`use_annotator`).
    """
    return _REGISTRY[_DEFAULT_NAME]

varcode.resolve_annotator(annotator_or_name: str | EffectAnnotator | None) -> EffectAnnotator

Normalize a per-call annotator= argument to an instance.

Accepts None (use the current default), a string (look up in the registry), or an object that already implements the :class:EffectAnnotator protocol (used directly without validation — we trust duck-typing).

Raises :class:KeyError for unknown names so callers that mistype a string get an early, legible error rather than an obscure attribute failure downstream.

Source code in varcode/annotators/registry.py
def resolve_annotator(
        annotator_or_name: str | EffectAnnotator | None,
) -> EffectAnnotator:
    """Normalize a per-call ``annotator=`` argument to an instance.

    Accepts ``None`` (use the current default), a string (look up in
    the registry), or an object that already implements the
    :class:`EffectAnnotator` protocol (used directly without
    validation — we trust duck-typing).

    Raises :class:`KeyError` for unknown names so callers that
    mistype a string get an early, legible error rather than an
    obscure attribute failure downstream.
    """
    if annotator_or_name is None:
        return get_default_annotator()
    if isinstance(annotator_or_name, str):
        if annotator_or_name not in _REGISTRY:
            raise KeyError(
                "No annotator registered under %r — known annotators: %r. "
                "Register one with varcode.register_annotator() or pick "
                "from the list." % (
                    annotator_or_name, sorted(_REGISTRY)))
        return _REGISTRY[annotator_or_name]
    return annotator_or_name

varcode.set_default_annotator(name: str) -> None

Swap the process-wide default annotator. name must refer to a registered annotator.

Source code in varcode/annotators/registry.py
def set_default_annotator(name: str) -> None:
    """Swap the process-wide default annotator. ``name`` must refer
    to a registered annotator.
    """
    global _DEFAULT_NAME
    if name not in _REGISTRY:
        raise KeyError(
            "No annotator registered under %r — call register_annotator() "
            "first or pick from %r." % (name, sorted(_REGISTRY)))
    _DEFAULT_NAME = name

varcode.use_annotator(name_or_instance: str | EffectAnnotator) -> Iterator[None]

Context manager that temporarily swaps the default annotator.

Useful for sequential comparisons and scoped overrides::

with varcode.use_annotator("protein_diff"):
    effects = variant_collection.effects()

Accepts the same argument shape as the annotator= kwarg: a registered-name string, or an annotator instance. Passing an instance registers it temporarily under its .name so that name-based lookups inside the block find it; on exit the previous default and any previously-registered annotator under that name are restored.

PARAMETER DESCRIPTION
name_or_instance

Registered name or implementation to select for the block.

TYPE: str or EffectAnnotator

YIELDS DESCRIPTION
None

The context manager does not bind an annotator with as.

Notes

The override is process-global, not thread- or task-local. Concurrent callers should pass annotator= explicitly. Restoration also occurs when the block raises an exception.

Source code in varcode/annotators/registry.py
@contextmanager
def use_annotator(name_or_instance: str | EffectAnnotator) -> Iterator[None]:
    """Context manager that temporarily swaps the default annotator.

    Useful for sequential comparisons and scoped overrides::

        with varcode.use_annotator("protein_diff"):
            effects = variant_collection.effects()

    Accepts the same argument shape as the ``annotator=`` kwarg:
    a registered-name string, or an annotator instance. Passing an
    instance registers it temporarily under its ``.name`` so that
    name-based lookups inside the block find it; on exit the
    previous default and any previously-registered annotator under
    that name are restored.

    Parameters
    ----------
    name_or_instance : str or EffectAnnotator
        Registered name or implementation to select for the block.

    Yields
    ------
    None
        The context manager does not bind an annotator with ``as``.

    Notes
    -----
    The override is process-global, not thread- or task-local. Concurrent
    callers should pass ``annotator=`` explicitly. Restoration also occurs
    when the block raises an exception.
    """
    global _DEFAULT_NAME
    prior_default = _DEFAULT_NAME

    if isinstance(name_or_instance, str):
        if name_or_instance not in _REGISTRY:
            raise KeyError(
                "No annotator registered under %r — register one first or "
                "pass an instance." % name_or_instance)
        _DEFAULT_NAME = name_or_instance
        prior_registration = None
    else:
        # Keep the runtime check for callers without static checking.
        name = cast(str, getattr(name_or_instance, "name", None))
        if not name:
            raise ValueError(
                "Annotator instance has no .name attribute; cannot scope.")
        prior_registration = _REGISTRY.get(name)
        _REGISTRY[name] = name_or_instance
        _DEFAULT_NAME = name

    try:
        yield
    finally:
        _DEFAULT_NAME = prior_default
        if not isinstance(name_or_instance, str):
            if prior_registration is None:
                _REGISTRY.pop(name, None)
            else:
                _REGISTRY[name] = prior_registration

Shared transcript model

This is the default annotator; annotator="fast" selects the compatibility implementation. The former realized name and public imports remain aliases. See supported inputs and results before using it.

varcode.TranscriptModelEffectAnnotator

Default annotator backed by the shared transcript model.

annotate_with_context(variant, transcript, germline_ctx, phase_resolver=None)

Annotate through the same pipeline with patient germline edits.

Source code in varcode/transcript_model.py
def annotate_with_context(
        self, variant, transcript, germline_ctx, phase_resolver=None):
    """Annotate through the same pipeline with patient germline edits."""
    return self._annotate_with_context(
        (variant,), transcript, germline_ctx, phase_resolver)

annotate_haplotype(variants, transcript, germline_ctx=None, phase_resolver=None)

Realize the whole cis group with phase/splice alternatives.

Reconcile supplied RNA models with the same phase/splice products; a per-variant observation need not cover every edit in the group.

Source code in varcode/transcript_model.py
def annotate_haplotype(
        self, variants, transcript, germline_ctx=None, phase_resolver=None):
    """Realize the whole cis group with phase/splice alternatives.

    Reconcile supplied RNA models with the same phase/splice products;
    a per-variant observation need not cover every edit in the group.
    """
    if germline_ctx:
        result = self._annotate_with_context(
            variants, transcript, germline_ctx, phase_resolver)
    else:
        result = self._predict_variants(variants, transcript)
    if result is not NotImplemented:
        from .phasing import apply_phase_resolver_to_effects
        result = apply_phase_resolver_to_effects([result], phase_resolver)[0]
    return result

varcode.predict_transcript_model_effect(variants, transcript, germline_variants=(), phase_resolver=None, sequence_provider=None, max_hypotheses=64, max_phase_hypotheses=None, homozygous_germline=())

Predict one ordinary top effect with alternatives in .candidates.

Canonical and exon-skip paths resolve from transcript annotation alone. Intron retention and cryptic sites are also realized when genomic sequence is available, and remain explicit :class:Unresolved candidates at sequence-free tier 0. Rule order is never mislabeled as probability.

More than max_phase_hypotheses phase hypotheses (default max_hypotheses), or more than max_hypotheses combined phase/splice outcomes, gives a :class:~varcode.HypothesisLimit instead of a partial candidate set. homozygous_germline lists germline variants on both haplotypes, which are always cis.

variants is a known-cis group. Members also present in the germline anchor the patient baseline; only novel alleles are applied to that baseline to obtain the mutant. Repeated normalized alleles are edited once. Other germline alleles retain their phase uncertainty.

Source code in varcode/transcript_model.py
def predict_transcript_model_effect(
        variants, transcript, germline_variants=(), phase_resolver=None,
        sequence_provider=None, max_hypotheses=64, max_phase_hypotheses=None,
        homozygous_germline=()):
    """Predict one ordinary top effect with alternatives in ``.candidates``.

    Canonical and exon-skip paths resolve from transcript annotation alone.
    Intron retention and cryptic sites are also realized when genomic
    sequence is available, and remain explicit :class:`Unresolved`
    candidates at sequence-free tier 0. Rule order is never mislabeled as
    probability.

    More than ``max_phase_hypotheses`` phase hypotheses (default
    ``max_hypotheses``), or more than ``max_hypotheses`` combined
    phase/splice outcomes, gives a :class:`~varcode.HypothesisLimit`
    instead of a partial candidate set. ``homozygous_germline`` lists
    germline variants on both haplotypes, which are always cis.

    ``variants`` is a known-cis group. Members also present in the germline
    anchor the patient baseline; only novel alleles are applied to that
    baseline to obtain the mutant. Repeated normalized alleles are edited
    once. Other germline alleles retain their phase uncertainty.
    """
    max_hypotheses = _check_max_hypotheses(max_hypotheses)
    max_phase_hypotheses = max_hypotheses if max_phase_hypotheses is None else (
        _check_max_hypotheses(max_phase_hypotheses, "max_phase_hypotheses"))
    variants = tuple(variants)
    if not variants:
        raise ValueError("predict_transcript_model_effect requires a somatic variant")
    germline_variants = _unique_alleles(germline_variants)
    somatic_variants = tuple(
        v for v in _unique_alleles(variants)
        if not detect_germline_overlap(v, germline_variants))
    if not somatic_variants:
        result = GermlineAlleleOverlap(variants[0], transcript)
        result.variants = variants
        return result
    primary = somatic_variants[0]
    sv_type = getattr(primary, "sv_type", None)
    assembled = sv_type in ("DEL", "DUP", "INV") and primary.alt_assembly
    nonlocal_span = sv_type in ("DEL", "DUP", "INV") and (
        primary.affected_start < transcript.start
        or primary.affected_end > transcript.end)
    junction_record = sv_type is not None and any(
        bracket in getattr(primary, "symbolic_alt", "") for bracket in "[]")
    if sv_type in ("BND", "CNV", "INV") or assembled or nonlocal_span or junction_record:
        if len(variants) != 1 or germline_variants:
            return Unresolved(
                primary, transcript, mechanism="structural_haplotype",
                reason=("Composition across a supplied assembly or nonlocal junction "
                        "requires mapped patient-baseline coordinates"))
        # One established builder owns fusion partners, junction inserts and
        # supplied transcript assemblies. A finite single-transcript layout
        # must not replace those products with a clipped local span.
        from .effects.structural import predict_structural_variant_effect
        return predict_structural_variant_effect(primary, transcript)
    if not transcript.is_protein_coding:
        return NoncodingTranscript(primary, transcript)
    if not transcript.complete:
        return IncompleteTranscript(primary, transcript)
    if sequence_provider is None:
        sequence_provider = _provider_for_genome(primary.genome, transcript)

    phase = partition_germline_by_phase(
        primary, germline_variants, phase_resolver,
        homozygous=homozygous_germline, cis_variants=variants)
    try:
        outcomes = _realize_outcomes(
            variants, somatic_variants, transcript,
            phase.hypotheses(max_phase_hypotheses),
            sequence_provider, max_hypotheses,
            phase_source=getattr(phase_resolver, "phase_source", None))
    except ReferenceMismatchError as error:
        expected, observed = error.genomic_expected_ref, error.genomic_observed_ref
        if transcript.on_backward_strand:
            expected, observed = reverse_complement(expected), reverse_complement(observed)
        try:
            offset = transcript.spliced_offset(error.genome_start)
        except (KeyError, ValueError):
            offset = None
        raise ReferenceMismatchError(
            error.variant, transcript, expected, observed,
            transcript_offset=offset, genome_start=error.genome_start,
            genome_end=error.genome_end) from error
    except NonlocalStructuralEdit as error:
        return Unresolved(
            primary, transcript, mechanism="nonlocal_structural_variant",
            reason=str(error))
    except HypothesisLimitError as error:
        result = HypothesisLimit(
            primary, transcript, error.max_hypotheses, phase,
            _reference_effect(variants, transcript, germline_variants,
                              sequence_provider, max_hypotheses))
        result.variants = variants
        return result
    candidates = merge_classified_outcomes(outcomes)
    ordered = order_realized_candidates(candidates, _classification_priority)
    return _attach_candidate_set(ordered)

Compatibility names

RealizedEffectAnnotator and predict_realized_effect are retained aliases for the transcript-model implementation. Prefer the canonical names above in new code; selecting the old "realized" registry name still works.

varcode.RealizedEffectAnnotator

Default annotator backed by the shared transcript model.

varcode.predict_realized_effect(variants, transcript, germline_variants=(), phase_resolver=None, sequence_provider=None, max_hypotheses=64, max_phase_hypotheses=None, homozygous_germline=())

Predict one ordinary top effect with alternatives in .candidates.

Canonical and exon-skip paths resolve from transcript annotation alone. Intron retention and cryptic sites are also realized when genomic sequence is available, and remain explicit :class:Unresolved candidates at sequence-free tier 0. Rule order is never mislabeled as probability.

More than max_phase_hypotheses phase hypotheses (default max_hypotheses), or more than max_hypotheses combined phase/splice outcomes, gives a :class:~varcode.HypothesisLimit instead of a partial candidate set. homozygous_germline lists germline variants on both haplotypes, which are always cis.

variants is a known-cis group. Members also present in the germline anchor the patient baseline; only novel alleles are applied to that baseline to obtain the mutant. Repeated normalized alleles are edited once. Other germline alleles retain their phase uncertainty.

Source code in varcode/transcript_model.py
def predict_transcript_model_effect(
        variants, transcript, germline_variants=(), phase_resolver=None,
        sequence_provider=None, max_hypotheses=64, max_phase_hypotheses=None,
        homozygous_germline=()):
    """Predict one ordinary top effect with alternatives in ``.candidates``.

    Canonical and exon-skip paths resolve from transcript annotation alone.
    Intron retention and cryptic sites are also realized when genomic
    sequence is available, and remain explicit :class:`Unresolved`
    candidates at sequence-free tier 0. Rule order is never mislabeled as
    probability.

    More than ``max_phase_hypotheses`` phase hypotheses (default
    ``max_hypotheses``), or more than ``max_hypotheses`` combined
    phase/splice outcomes, gives a :class:`~varcode.HypothesisLimit`
    instead of a partial candidate set. ``homozygous_germline`` lists
    germline variants on both haplotypes, which are always cis.

    ``variants`` is a known-cis group. Members also present in the germline
    anchor the patient baseline; only novel alleles are applied to that
    baseline to obtain the mutant. Repeated normalized alleles are edited
    once. Other germline alleles retain their phase uncertainty.
    """
    max_hypotheses = _check_max_hypotheses(max_hypotheses)
    max_phase_hypotheses = max_hypotheses if max_phase_hypotheses is None else (
        _check_max_hypotheses(max_phase_hypotheses, "max_phase_hypotheses"))
    variants = tuple(variants)
    if not variants:
        raise ValueError("predict_transcript_model_effect requires a somatic variant")
    germline_variants = _unique_alleles(germline_variants)
    somatic_variants = tuple(
        v for v in _unique_alleles(variants)
        if not detect_germline_overlap(v, germline_variants))
    if not somatic_variants:
        result = GermlineAlleleOverlap(variants[0], transcript)
        result.variants = variants
        return result
    primary = somatic_variants[0]
    sv_type = getattr(primary, "sv_type", None)
    assembled = sv_type in ("DEL", "DUP", "INV") and primary.alt_assembly
    nonlocal_span = sv_type in ("DEL", "DUP", "INV") and (
        primary.affected_start < transcript.start
        or primary.affected_end > transcript.end)
    junction_record = sv_type is not None and any(
        bracket in getattr(primary, "symbolic_alt", "") for bracket in "[]")
    if sv_type in ("BND", "CNV", "INV") or assembled or nonlocal_span or junction_record:
        if len(variants) != 1 or germline_variants:
            return Unresolved(
                primary, transcript, mechanism="structural_haplotype",
                reason=("Composition across a supplied assembly or nonlocal junction "
                        "requires mapped patient-baseline coordinates"))
        # One established builder owns fusion partners, junction inserts and
        # supplied transcript assemblies. A finite single-transcript layout
        # must not replace those products with a clipped local span.
        from .effects.structural import predict_structural_variant_effect
        return predict_structural_variant_effect(primary, transcript)
    if not transcript.is_protein_coding:
        return NoncodingTranscript(primary, transcript)
    if not transcript.complete:
        return IncompleteTranscript(primary, transcript)
    if sequence_provider is None:
        sequence_provider = _provider_for_genome(primary.genome, transcript)

    phase = partition_germline_by_phase(
        primary, germline_variants, phase_resolver,
        homozygous=homozygous_germline, cis_variants=variants)
    try:
        outcomes = _realize_outcomes(
            variants, somatic_variants, transcript,
            phase.hypotheses(max_phase_hypotheses),
            sequence_provider, max_hypotheses,
            phase_source=getattr(phase_resolver, "phase_source", None))
    except ReferenceMismatchError as error:
        expected, observed = error.genomic_expected_ref, error.genomic_observed_ref
        if transcript.on_backward_strand:
            expected, observed = reverse_complement(expected), reverse_complement(observed)
        try:
            offset = transcript.spliced_offset(error.genome_start)
        except (KeyError, ValueError):
            offset = None
        raise ReferenceMismatchError(
            error.variant, transcript, expected, observed,
            transcript_offset=offset, genome_start=error.genome_start,
            genome_end=error.genome_end) from error
    except NonlocalStructuralEdit as error:
        return Unresolved(
            primary, transcript, mechanism="nonlocal_structural_variant",
            reason=str(error))
    except HypothesisLimitError as error:
        result = HypothesisLimit(
            primary, transcript, error.max_hypotheses, phase,
            _reference_effect(variants, transcript, germline_variants,
                              sequence_provider, max_hypotheses))
        result.variants = variants
        return result
    candidates = merge_classified_outcomes(outcomes)
    ordered = order_realized_candidates(candidates, _classification_priority)
    return _attach_candidate_set(ordered)