Skip to content

Self-sequence and presentation evidence

SelfProteome searches an explicit reference corpus. Sequence similarity, predicted binding and observed presentation are separate facts; none establishes TCR recognition. Peptide-loaded or minigene recognition can also differ from recognition of native protein processing and presentation (primary study).

Use all-match evidence for complete vaccine-window searches or multiple similar candidates. The existing nearest() and self_nearest_* predictor columns remain a single-nearest-sequence view.

Use binding_aware_self_evidence to compare supplied query and self measurements from one explicitly selected model, version, endpoint and measurement context. This produces the self_mimic_* and self_strongest_nearby_* axes while preserving every candidate and origin. It does not run predictors or infer TCR recognition.

Binding-conditioned evidence

from topiary import binding_aware_self_evidence

evidence = binding_aware_self_evidence(
    reference, query_peptides, alleles=query_alleles,
    predictions=query_and_self_predictions,
    prediction_method_name="netmhcpan", predictor_version="4.2c",
    kind="pMHC_affinity", binding_metric="percentile_rank",
    max_mismatches=2, excluded_gene_ids=cta_gene_ids)

predictions uses the long-form schema of match_self_peptides. Supply both query and nearby self measurements; this API never fills a missing measurement by borrowing another allele, model, version, endpoint or measurement context. measurement_context=None selects unstated contexts only. Raw value comparisons additionally require value_unit and lower_is_better; score requires an explicit direction. Percentile ranks default to lower-is-better, but still must come from the same selected model/version/endpoint. Ranks are not pooled across tools.

Each query occurrence and allele receives:

  • self_nearest_candidates: all bounded matches and all origins, including excluded origins and the supplied observation/prediction records.
  • self_mimic_candidates: every nearest in-scope match with a selected metric at least as strong as the query, retaining all ties.
  • self_strongest_nearby_candidates: every strongest in-scope match within the radius, regardless of whether the query's own score is available.
  • Separate axis *_status and *_complete fields. A match can be known while coverage is incomplete. no_match_within_scope requires complete bounded search and selected prediction coverage; otherwise the status is unassessed.

With incomplete coverage, the nearest/strongest match is only the best among available selected measurements. An unmeasured candidate could change the answer.

Scalar fields such as self_mimic_peptide and self_mimic_percentile_rank are present only when all tied candidates agree. Read them through the DSL as self_mimic.affinity.rank or self_strongest_nearby.affinity.rank. Unknown scalars remain unknown; inspect the structured candidate lists to resolve ties. Typed CSV/TSV preserves these lists and the comparison policy in result.extra['self_binding']. For DSL evaluation on this standalone table, pass group_keys=['query_index', 'peptide', 'allele']; query_index preserves repeated queries and is not a fabricated source-protein offset.

No universal anchor-versus-TCR-facing position weights are imposed. An anchor change can alter the bound structure and TCR interaction (primary structural study). Even sequence and binding concordance do not establish native tissue presentation or recognition (molecular mimicry study). These outputs organize evidence for review; they are not a calibrated cross-reactivity probability or a vaccine-safety declaration.

All-match evidence

from topiary import SelfProteome, match_self_peptides, self_matches_in_windows

# Synthetic reference. In production, retain an unfiltered corpus so a shared
# sequence keeps both CTA and non-CTA origins. oncoref owns CTA membership.
reference = SelfProteome.from_peptides(
    {"CTA": "GILGFVFTL", "healthy": "GILGFVFTL", "near": "SIINFEKM"},
    peptide_lengths=[8, 9],
)
windows = {"full": "GILGFVFTLGGGSIINFEKLELAGIGILT", "trimmed": "SIINFEKLELAGIGILT"}
exact = self_matches_in_windows(
    windows, reference, peptide_lengths=[8, 9],
    alleles={name: ["A0201", "B0702"] for name in windows},
    excluded_gene_ids={"CTA"},  # illustrative caller-resolved exclusion
)
similar = match_self_peptides(reference, ["SIINFEKL"], max_mismatches=1, alleles=["A0201"])
exact.to_tsv("window-self-evidence.tsv")

reference.match_candidates(...) delegates to match_self_peptides. Queries retain order and duplicates through query_index; every matched sequence and gene/transcript/reference-offset origin has a row. self_match_id identifies the reference occurrence across queries. Window output also carries window_id, window_sequence and every zero-based peptide_offset. The exact adapter checks all requested lengths and offsets, not only mutation-overlapping peptides.

Exclusions flag self_in_scope=False; they never delete origins. A reference already filtered with include="non_cta" cannot recover removed CTA origins. For an Ensembl corpus use include="all" and resolve the exclusion set with cta_gene_ids(source="oncoref") for the appropriate species/tier. A sequence match alone is not evidence that the peptide was presented.

Both functions accept observations= and predictions= as tables or record lists. They preserve multiple records in typed list/dict columns:

Input Required fields Interpretation
Observations evidence_id, peptide, source, evidence_kind, allele_assignment evidence_kind is observed or predicted; allele assignment is confirmed, predicted or unknown. Confirmed/predicted assignments require an explicit allele.
Predictions peptide, allele, kind, value, prediction_method_name, predictor_version Preserve supplied model facts, including unknown versions, missing values and conflicts; no averaging or model choice.

Observation records may carry tissue, assay, source version and other JSON-compatible provenance. An optional gene_id restricts attribution to that origin; its absence does not identify the producing gene. An unresolved allele_set remains a genotype, not confirmed restriction. Alleles are parsed with mhcgnomes, including non-human alleles.

self_observations and self_predictions retain the records. self_same_allele_predictions contains only finite numeric value measurements at the query allele with per-allele MHC scope. Haplotype presenter labels do not become per-allele evidence. This reports any supplied measurement, not complete coverage of a desired model panel. Other-allele records remain in self_predictions. Observation flags distinguish confirmed and inferred same-allele assignments; unreported observations remain unknown.

Coverage is explicit:

Field Meaning
self_search_status matched, no_match_in_scope, or unassessed.
self_search_complete, self_search_reason Whether the requested canonical, same-length search is complete; unavailable lengths and unsupported query/reference sequences have explicit reasons. A match can coexist with incomplete coverage.
self_observation_status Observation input absent, a record reported, or no record reported for this match/origin.
self_prediction_status Prediction input absent, query allele unknown, same-allele measurement reported, or not reported.

no_match_in_scope is negative evidence only within the declared reference, lengths and Hamming radius. It does not mean no presentable peptide or no risk. Indels, substitutions outside the radius and unsearched reference data are not assessed. Comparisons use chunks of 65,536 reference rows; exact queries reuse the reference index. Metadata under extra['self_search'] records the reference identity, parameters, coverage counts and hashes of normalized supplied evidence. extra['self_window_coverage'] also records requests too short for a peptide.

CSV/TSV preserves these fields and nested records. This is an evidence table, not a fabricated prediction table: join it to candidate measurements at the explicit query/allele identity before using a prediction policy. Preserve all matches and their IDs when there are several per candidate. The composed test in tests/test_consumer_workflows.py checks named-criterion unknown handling and exact replay after joining and saving the evidence.

Vaxrank owns window selection. scripts/check_vaxrank_candidates.py consumes its released builtin:openvax-v1 bundle and shows that self evidence changes a window choice while retaining both intended epitopes and their alleles. That synthetic fixture uses 100% target-score retention and checks every required target; it does not establish that a general aggregate-score threshold protects each target.

Basic usage

from topiary import SelfProteome, TopiaryPredictor
from mhctools import NetMHCpan

ref = SelfProteome.from_ensembl(species="human", release=115)
# Default include="non_cta" strips CTAs using oncoref membership via pirlygenes.
# release= names the installed Ensembl annotation release. Omitting it reads
# the release pyensembl currently exposes for human, which moves when
# pyensembl is upgraded; name one for a reproducible analysis.

predictor = TopiaryPredictor(
    models=[NetMHCpan(alleles=["HLA-A*02:01"])],
    alleles=["HLA-A*02:01"],
    self_proteome=ref,
)
df = predictor.predict_from_variants(variants)

# Output gains:
#   self_nearest_peptide
#   self_nearest_peptide_length
#   self_nearest_edit_distance
#   self_nearest_gene_id
#   self_nearest_transcript_id
#   self_nearest_reference_offset
#   self_nearest_reference_version

The columns join onto the predictor output on peptide. They're attached before filter_by and sort_by evaluate, so you can reference them in DSL expressions:

predictor = TopiaryPredictor(
    models=[NetMHCpan(alleles=["HLA-A*02:01"])],
    alleles=["HLA-A*02:01"],
    self_proteome=ref,
    filter_by=(Affinity <= 500) & (Column("self_nearest_edit_distance") >= 3),
)

Scope

Three construction modes:

include= Behavior Configuration
"all" Whole proteome, no filter —
"non_cta" (default for human Ensembl) Remove CTA genes cta_source="pirlygenes" default; set / callable accepted
"protected_tissues" Keep only genes expressed in named tissues tissues=[…], min_tissue_ntpm=… for human HPA data; or explicit tissue_gene_ids={…} for any species
callable Arbitrary gene_id → bool filter —

Tissue expression selects the reference genes; it is not evidence of peptide presentation. Supply presentation observations separately when available.

Human users get zero-config include="non_cta" via pirlygenes:

ref = SelfProteome.from_ensembl(species="human", release=115)

Non-human users must name a release= — only human has a default — and either use include="all" or supply their own CTA source, because oncoref's CTA membership is human-only today:

ref = SelfProteome.from_ensembl(species="mouse", release=102, include="all")

# Or with a custom CTA list:
ref = SelfProteome.from_ensembl(
    species="mouse",
    release=102,
    include="non_cta",
    cta_source={"ENSMUSG00000031691", "ENSMUSG00000079641"},
)

A non-human include="non_cta" call without cta_source= raises at construction — silent unfiltered results would misstate the reference scope.

Non-Ensembl sources

For users whose reference proteome isn't in Ensembl, from_fasta takes a protein-FASTA file directly:

ref = SelfProteome.from_fasta("my_reference.fa")
# include="all" by default; callable scope also works.
# include="non_cta" isn't available here — FASTA has no gene metadata.

For test or programmatic use:

ref = SelfProteome.from_peptides(
    {"geneA": "MASIINFEKLGGG", "geneB": "QPRSTVWYACDEF"},
    peptide_lengths=[8, 9, 10, 11],
)

Reference version

Every row of the output carries a self_nearest_reference_version string. Matching strings identify the same indexed reference; comparing results also requires the same queries, metric and search settings. Its form is {source}-{species}[-{release}]+include-{scope}+sha256:{digest}:

ensembl-human-115+include-non_cta+cta-oncoref-VERSION+sha256:3f2a9c1e7b40
ensembl-mouse-102+include-all+sha256:91d0c4e2a8b7
fasta-fasta+include-callable-keep_named+sha256:0be51d2c9f63
peptides-synthetic+include-all+sha256:bd8a4e854d1c

source is the constructor: ensembl, fasta or peptides. When from_ensembl is called without a release, the string records the release pyensembl selected. The leading parts describe the proteome for a reader; the digest identifies it. It covers every record the index was built from (gene id, transcript id and sequence, in order) and the lengths indexed, so two proteomes share a string only when they hold the same sequences under the same identifiers. A callable filter is labelled by its qualified name, which is the same on every run; the digest tells two filters apart.

Algorithm

Reference and query peptides use the public encode_amino_acids() integer encoding. The default metric uses NCBI's BLOSUM62 matrix. Canonical pairs retain Topiary's established distance behavior. NCBI's B/J/Z ambiguity scores use a symmetric conservative transformation, while O/U/X/* and unrecognized characters receive the symmetric worst-case canonical distance (15). This prevents missing substitution evidence from looking like an exact match. metric="hamming" counts encoded residue mismatches instead. For a query of length L, the same-length search is vectorized against that reference bucket, with 1aa insertion/deletion neighbors checked separately when enabled.

Performance notes

  • Construction: one pass over the reference proteome extracts every L-mer for each configured length, dedupes per length, and encodes into a (M, L) int8 array. Corpus size depends on the release, scope and lengths.
  • Lookup: vectorized comparisons are chunked to bound temporary memory. Runtime depends on the corpus and query count; exact matching reuses a hash index.

The existing nearest() path checks one-residue indel neighbors. The all-match and binding-conditioned APIs default to same-length Hamming candidates. Use max_edits=2 (instead of max_mismatches) to enumerate every candidate within a Levenshtein radius, including substitutions, insertions and deletions. Index all positive lengths from query-length minus radius through query-length plus radius; absent lengths and unsupported residues make coverage incomplete. self_edit_distance records the chosen search distance; self_mismatches is defined only for equal-length sequences. The dynamic-programming search is chunked, but broader radii and more query/reference sequences increase runtime.