Skip to content

Quickstart

A first prediction

Give TopiaryPredictor one or more mhctools models, the alleles to predict against, and a source of sequence:

from topiary import TopiaryPredictor, Affinity
from mhctools import NetMHCpan

predictor = TopiaryPredictor(
    models=NetMHCpan,
    alleles=["A0201"],
    filter_by=Affinity <= 500,
)
df = predictor.predict_from_sequences(["MASIINFEKLGGG"])

The returned DataFrame has one row per (peptide, allele, kind):

Column Meaning
peptide Predicted peptide sequence
allele HLA allele
peptide_length Length of the peptide
kind Prediction type (pMHC_affinity, pMHC_presentation, etc.)
affinity Predicted IC50 binding affinity (nM)
percentile_rank Percentile rank (lower = stronger binder)
score Model score (not necessarily a probability or bounded by 1)
prediction_method_name Which predictor produced this row

Variant-derived predictions add gene, variant, effect, contains_mutant_residues, wt_peptide and more — the API reference has the full column list.

Sequences can be named, scored as given rather than scanned, or read from a file:

# Sliding-window scan over named proteins
df = predictor.predict_from_named_sequences({"PRAME": PRAME_SEQUENCE})

# Score these peptides as-is, no sliding window
df = predictor.predict_from_named_peptides({"flu_np": "GILGFVFTL"})

The same runs from the command line

# From a FASTA of protein sequences
topiary --fasta proteins.fasta \
  --mhc-predictor netmhcpan --mhc-alleles A0201,B0702 \
  --filter-by "affinity <= 500 | presentation.rank <= 2" \
  --output-csv results.csv

# Score specific peptides (no sliding window)
topiary --peptide-csv peptides.csv \
  --mhc-predictor netmhcpan --mhc-alleles A0201 \
  --ic50-cutoff 500 \
  --output-csv results.csv

# From specific genes
topiary --gene-names BRAF TP53 EGFR \
  --mhc-predictor netmhcpan --mhc-alleles A0201 \
  --filter-by "affinity <= 500" \
  --output-csv results.csv

# From genomic variants (VCF)
topiary --vcf somatic.vcf \
  --mhc-predictor netmhcpan --mhc-alleles A0201 \
  --filter-by "affinity <= 500" \
  --only-novel-epitopes \
  --output-csv results.csv

# CTA proteins, excluding peptides also found in vital organs
topiary --cta \
  --exclude-tissues heart_muscle lung liver \
  --mhc-predictor netmhcpan --mhc-alleles A0201 \
  --filter-by "affinity <= 500" \
  --output-csv results.csv

The command-line interface page covers predictor names, the remaining input modes, output options and exit status.

Multiple models

from topiary import Affinity, Presentation
from mhctools import NetMHCpan, MHCflurry

predictor = TopiaryPredictor(
    models=[NetMHCpan, MHCflurry],
    alleles=["A0201", "A0301", "B0702"],
    filter_by=Affinity <= 500,
    sort_by=Presentation.score,
)

When several models produce the same prediction kind, say which one an expression means with brackets:

Affinity["netmhcpan"] <= 500           # NetMHCpan only
Affinity["mhcflurry"].score            # MHCflurry only
Presentation["mhcflurry"].rank <= 2    # MHCflurry presentation

The string form is netmhcpan:affinity <= 500. See the ranking DSL for the whole grammar.

Input formats

from topiary.inputs import (
    read_fasta, read_peptide_csv, read_peptide_fasta,
    read_sequence_csv, slice_regions,
)

# Protein FASTA (scanned with sliding window)
seqs = read_fasta("proteins.fasta")

# Peptide FASTA (each entry predicted as-is, no sliding window)
seqs = read_peptide_fasta("peptides.fasta")

# CSV with 'peptide' column (as-is)
seqs = read_peptide_csv("peptides.csv")

# CSV with 'sequence' column (sliding window)
seqs = read_sequence_csv("proteins.csv")

# Region slicing (half-open intervals)
sliced = slice_regions(seqs, {
    "spike": [(319, 541)],           # RBD only
    "nucleocapsid": [(0, 50), (350, 419)],
})

Ensembl and tissue lookups

from topiary.sources import (
    sequences_from_gene_names,
    sequences_from_gene_ids,
    sequences_from_transcript_ids,
    tissue_expressed_sequences,
    cta_sequences,
    ensembl_proteome,
    available_tissues,
)

# Specific genes (uses longest protein-coding transcript)
seqs = sequences_from_gene_names(["BRAF", "TP53", "EGFR"])

# By Ensembl ID
seqs = sequences_from_gene_ids(["ENSG00000157764"])
seqs = sequences_from_transcript_ids(["ENST00000288602"])

# Tissue-expressed genes (requires pirlygenes)
seqs = tissue_expressed_sequences(["testis"], min_ntpm=10.0)

# CTA genes (requires pirlygenes)
seqs = cta_sequences()

# Full Ensembl proteome
seqs = ensembl_proteome()

# List available tissues
print(available_tissues())

Exclusion

from topiary.inputs import exclude_by
from topiary.sources import tissue_expressed_sequences

vital = tissue_expressed_sequences(["heart_muscle", "lung", "liver"])

# Substring mode: heart 8-mer inside CTA 9-mer -> excluded
df = exclude_by(df, vital, mode="substring")

# Exact mode: only full peptide matches
df = exclude_by(df, vital, mode="exact")

Peptide properties

from topiary.properties import add_peptide_properties

df = add_peptide_properties(df, groups=["manufacturability"])
# Adds: charge, hydrophobicity, aromaticity, molecular_weight,
#   cysteine_count, instability_index, max_7mer_hydrophobicity,
#   cterm_7mer_hydrophobicity, difficult_nterm, difficult_cterm, asp_pro_bonds

See Peptide Properties for all available properties and groups.

From somatic variants

from varcode import load_vcf

variants = load_vcf("somatic.vcf")
df = predictor.predict_from_variants(variants)
# DataFrame includes:
#   fragment_id, source_type, variant, gene, gene_id, transcript_id,
#   transcript_name, effect, effect_type, contains_mutant_residues,
#   overlaps_target, mutation_start_in_peptide, mutation_end_in_peptide,
#   wt_peptide, wt_peptide_length, ...

RNA quantification can annotate and filter these rows; see RNA expression.

From protein fragments (non-variant sources)

from topiary import ProteinFragment

fragments = [
    ProteinFragment(
        fragment_id="HPV16_E6__abc12345",
        source_type="viral:hpv16",
        sequence="MHQKRTAMFQDPQERPRKLPQLCTELQTTIHDIILECVYCKQQLLRREVYDFAFRDLCIVYRDGNPYAVCDKCLKFYSKISEYRHYCYSLYGTTLEQQYNKPLCDLLIRCINCQKPLCPEEKQRHLDKKQRFHNIRGRWTGRCMSCCRSSRTRRETQL",
    ),
]
df = predictor.predict_from_fragments(fragments)
# Any origin — variants, SVs, ERVs, CTAs, viral, allergen, autoantigen,
# synthetic. fragment_id threads through so downstream tools (vaxrank,
# vaccine-window selection) can group peptides back to their fragment.

From cached predictions (skip the predictor call)

Use a pre-computed prediction table instead of running the predictor live — for reproducibility, iterating on filters/ranking without paying the predictor cost, or ingesting output from another tool.

From a prior topiary run:

from topiary import CachedPredictor, TopiaryPredictor

cache = CachedPredictor.from_topiary_output("prior_run.parquet")
predictor = TopiaryPredictor(models=cache)
df = predictor.predict_from_variants(variants)

From mhcflurry or NetMHCpan output:

cache = CachedPredictor.from_mhcflurry("mhcflurry_predictions.csv")
cache = CachedPredictor.from_netmhcpan_stdout("netmhcpan_run.out")

From the CLI (format auto-detected):

topiary --peptide-csv peptides.csv \
    --mhc-cache-file netmhcpan_run.out \
    --output-csv results.csv

Full details — all loaders, sharding, version invariants, and the CLI flag reference — in Cached Predictions.

Whole-peptide half-life models

mhctools' PeptiVerse and PlifePred2 score a complete peptide rather than a peptide-MHC pair, so they need no allele. Configure the external model installation through mhctools, then pass the instance, class or public class name (Topiary 5.53.0 and newer):

from mhctools import PeptiVerse
from topiary import TopiaryPredictor, evaluate_scores, parse

model = PeptiVerse(peptiverse_home="/path/to/PeptiVerse")
df = TopiaryPredictor(models=model).predict_from_named_peptides({
    "candidate": "SIINFEKLGGALQ",
})
scores = evaluate_scores(df, parse("peptide_view(serum_half_life.value)"))

Protein and fragment scanning reject these peptide-only models and point to predict_from_named_peptides: Topiary does not invent scanning lengths or assign a whole peptide's half-life to the ligands inside it.

Interpreting the output

serum_half_life, blood_half_life and pMHC_stability are separate kinds. The first two describe the peptide without MHC; the third describes a peptide-MHC complex. Known physical half-lives use value in hours, and an unknown unit stays missing rather than being replaced by the model's native score. Do not assume a half-life score is a probability, or infer a percentile the model does not supply. Use an explicit peptide_view(...) when combining peptide-level evidence with per-allele predictions.

Topiary's tests carry synthetic sidecar output through prediction, cache replay, TSV, wide/long conversion and ranking. That verifies data transport, not biological accuracy or suitability for long vaccines. The external models and their runtime environments are not bundled.

PlifePred2 had upstream workspace-safety and unit/assay validation issues, addressed in mhctools 3.39.0 (included in Topiary's current minimum). Its native output is not a verified half-life in hours by default.

Neither model supplies per-bond extracellular peptidase maps. Site-level cleavage, full assay/chemistry provenance and configuration-aware cache identity remain tracked in Topiary #288.