mhcmatch — which neoantigens are presented, and which ones a T cell will see
Pure Python, no compiled extension beyond the seqtree
search core. MHC-I and MHC-II, human and mouse. Every reference dataset is fetched from
isalgo/pmhc_data on first use, so a fresh
pip install runs every example here with no manual downloads.
pip install mhcmatch
# rank a donor's neoantigen candidates end to end
mhcmatch rank fasta candidates.fasta --alleles donor.alleles --cls mhc1 --tumor SKCM --out ranked.tsv
import mhcmatch
store = mhcmatch.Store.from_pmhc(tier="shortlist", species="human") # auto-fetched, cached
store.restriction("NLVPMVATV", calibrated=True) # ranked alleles + %rank / P(present) / band
store.binder_score("NLVPMVATV") # the single-number binder index
store.scan_protein(my_protein, cls="mhc1") # which windows are presented
📖 Full documentation · Getting started · Command reference · The EPIC scorer · Notebooks
Pick your entry point
The fifteen things people actually come here to do. The CLI page has every command with every flag.
| your question | command | Python |
|---|---|---|
| Which of these peptides does an allele present? | mhcmatch predict f.fasta --alleles 'HLA-A*02:01' |
predict.predict_fasta |
| Which allele presents this peptide? | mhcmatch restriction PEP --calibrated |
store.restriction |
| Is it a binder at all, one number? | mhcmatch binder PEP |
store.binder_score |
| What is the IC50, and vs its wild type? | mhcmatch affinity PEP --allele A --wt WTPEP |
store.affinity_model |
| Will a T cell respond to it? | mhcmatch complement --peptides p.txt |
complement.score |
| Rank neoantigen candidates for a donor | mhcmatch rank fasta ... |
rank.rank_fasta |
| Why did this candidate rank there? | mhcmatch explain PEP --allele A |
— |
| Which peptides in this protein are presented? | mhcmatch scan p.fasta --correction bh |
store.scan_protein |
| What self / viral peptide does it mimic? | mhcmatch mimics --peptides p.txt |
mimics.neighbours |
| Where in the proteome does it come from? | mhcmatch source --peptides p.txt |
Proteome.find_sources |
| Is this gene on in a normal tissue? | mhcmatch expression GENE --tissue T --safety |
expression.lookup |
| What does this allele's motif look like? | mhcmatch logo 'HLA-A*02:01' |
logo.motif |
| Which k candidates go in the cassette? | mhcmatch cassette select --candidates pool.tsv -k 20 |
cassette.select |
| What is this cassette worth, against another? | mhcmatch cassette score --cassettes c.tsv --pool pool.tsv |
cassette.score / .lam |
| Assemble and order the chosen units | mhcmatch cassette build --candidates units.tsv --screen |
vector.select / vector.order |
Four things that surprise people, and each is a link away from its detail:
predictandrank fastadrop nothing by default.--rank-thresholdtakessb/wb/none/ a percentage, and the tiers are class-aware, because a bare number cannot be:2.0is the weak cut for class I and the strong cut for class II. On class II a flat2.0kept 0 of 56 scored pairs in testing — an empty table, returncode 0.- Two whitelists, because they make two different claims.
--keep-genes 'TP53,KRAS'keeps a candidate for its gene;--keep-epitopes builtinkeeps it for being one of 23,299 assay-validated immunogenic peptides. Matched rows carrykeep_reason, because a row kept for its gene is not evidence about its peptide. → CLI cassetteandvectorare two jobs, not two names.cassettechooses units and scores a finished construct;vectorassembles one already chosen. Onemhcmatch cassetteverb hides the split on the CLI; the Python column above does not. → Designing a cassettepredictandrestrictionare different axes.predictasks is it presented at all (the NetMHCpan%Rank_ELanalogue);restrictionasks which allele. A peptide can top one and not the other —NLVPMVATVis unambiguously A*02:01-restricted yet bands mid-pack against A*02:01's own ligands.
The shipped model
Expression, Presentation, Immunogenic Complementarity — four blocks, listed here in the order that spells the name:
| letter | block | columns |
|---|---|---|
E |
expression | expr_lvl, expr_norm |
P |
presentation | binder, log10a |
I |
immunogenic — physchem | C_phys_buried, C_phys_charge |
C |
complementarity — corpus | C_corpus_thymus, C_corpus_self, C_corpus_viral |
The fit enters them presentation first — presentation, expression, physchem, corpus, which is
the blocks list on the artifact itself — so a later block's coefficient is what that term is
worth after the earlier ones. Read a coefficient against that order, not against the acronym.
One artifact per (cls, species, mode), and no fallback — asking for a cell that was never
fitted raises rather than scoring it with a neighbour's coefficients.
model_id |
model version | release | terms | rows | positives | intercepts | AUROC | how that AUROC is measured |
|---|---|---|---|---|---|---|---|---|
mhc1.human.neoantigen |
12 | 1.15.0 | 9 | 339,595 | 594 | 7 per screen | 0.7094 | leave-one-screen-out, mean |
mhc1.mouse.neoantigen |
6 | 1.15.0 | 9 | 921 | 379 | 61 per reference | 0.6335 | in-sample, within reference |
mhc2.human.neoantigen |
2 | 1.15.0 | 6 | 1,112 | 656 | 157 per reference | 0.6020 | in-sample, within reference |
mhc2.mouse.neoantigen |
4 | 1.15.0 | 6 | 468 | 177 | 30 per reference | 0.5741 | in-sample, within reference |
mhc1.human.pathogen |
2 | 1.15.0 | 2 | 16,790 | 7,002 | 1 per corpus, global | 0.5988 | in-sample, pooled off the logit |
mhc1.mouse.pathogen |
2 | 1.15.0 | 2 | 10,404 | 2,196 | 1 per corpus, global | 0.5561 | in-sample, pooled off the logit |
mhc2.human.pathogen |
2 | 1.15.0 | 3 | 7,946 | 5,148 | 1 per corpus, global | 0.5824 | in-sample, pooled off the logit |
mhc2.mouse.pathogen |
2 | 1.15.0 | 3 | 11,725 | 3,324 | 1 per corpus, global | 0.6446 | in-sample, pooled off the logit |
Human class I is the cell this library is built around. Human class II is fitted on far fewer rows from far fewer assays and its AUROC says so; the mouse cells are thinner again, and mouse class II is the thinnest of all. Read a class-II or mouse number as a transfer that was checked, not as a second headline.
Two things the table does not show.
A ninth cell ships that is not a default. mhc2.human.neoantigen has a second fit carrying the
three corpus terms, reached as rank.aggregate("mhc2", "human", variant="corpus"). It exists
because the corpus block became computable at class II in 1.20.0 — the HLA Ligand Atlas gives
132,818 class-II peptides over 29 benign tissues, against the 27,987 of the class-II thymic
fraction alone. It is not the default because computing it answered in the negative: on the
same 1,112 rows all three coefficients span zero, C_corpus_thymus at +0.0170 (95 % CI −0.4944 to
+0.3995, sign held in 53 % of resamples). At class II the corpus block adds nothing, and the
six-term fit is what a class-II score should use. The same corpus rebuild is what made the mouse
class-I cell fittable, where the block does carry.
mode="pathogen" is a supported surface, not an internal. Four cells are fitted on IEDB T-cell
corpora whose negative class is "assayed and did not respond", which is a different question from
the neoantigen cells' "nominated and did not respond" — so the two are not comparable term by term,
and a pathogen cell drops expression outright, there being no host gene to measure. Reach them with
--epitope pathogen. They also back the physicochemistry work, which is why the pathogen corpora are
maintained alongside the neoantigen ones.
The AUROC column is three different protocols — do not read it down, and do not average it. Coefficients are deliberately not written here: they move with every refit, and this table quoted a superseded set for a full release each time it was maintained by hand. The record is docs/models.rst — every coefficient with its bootstrap interval, what each fit was trained on, how its AUROC was measured, and the caveats that come with each one, all generated from the artifacts on every docs build. Or ask the artifact:
mhcmatch models --all # which cells ship
mhcmatch rank --coefficients # every term, its block, its coefficient
mhcmatch rank --holdout # held-out AUROC, the grouped CVs, the corpus
Python
import mhcmatch
from mhcmatch import complement, expression, mimics, rank
store = mhcmatch.Store.from_pmhc(tier="shortlist", species="human")
store.decompose("NLVPMVATV") # anchor / TCR-facing split
aff = store.affinity_model("mhc1")
aff.predict_ic50("NLVPMVATV", "HLA-A*02:01") # 18.9 nM (shortlist tier)
aff.amplitude("NLVPMVATL", "NLVPMVATV", "HLA-A*02:01") # Kd_WT/Kd_MT (Łuksza eq. 9)
complement.score(peptides) # vectorised: pass the list, not a loop
complement.posterior(peptides, prior=4.2e-4) # the log-odds carries NO prior; supply yours
rank.models() # every shipped fit and its provenance
rank.aggregate("mhc1", "mouse") # the artifact itself
expression.gene_level("Trp53", species="mouse") # FANTOM5 tissues + syngeneic models
mimics.neighbours(peptides, ref_sets, threads=0) # threaded C++ neighbour search
pm = mhcmatch.Proteome.from_hf("human")
pm.find_sources(peptides, max_subs=1, threads=0) # batch; find_source() is the single-query form
pm.wildtype("NLVPMVATV") # the WT counterpart, for agretopicity
Every one of these takes a list and returns one per input. Passing a list beats looping the single-query form by orders of magnitude — the batch call reaches C++ once with the GIL released. → Batch and threads
Advanced entry points — mimicry (signed per-component mimicry risk), portfolio (the
over-dispersion and support machinery under cassette score), luksza (the published R term),
recognition (the ESM head), calibrate, known, ligand, pseudoseq.
→ API reference
Install extras
The base install is seqtree, numpy and huggingface_hub. Every model that ships by default
runs on it — torch is not required to score recognition, and asking for a head without its
extra raises a named error rather than silently degrading.
| extra | pulls | needed for |
|---|---|---|
mhcmatch[notebooks] |
marimo, polars |
the worked examples in notebooks/ |
mhcmatch[logo] |
logomaker, matplotlib, pandas |
drawing a motif logo. logo.motif returns the matrix on the base install |
mhcmatch[stats] |
scipy |
your own over-dispersion (portfolio.betabinom_rho) and the exact LP behind portfolio.linearly_supported |
mhcmatch[esm] |
torch, transformers |
only the esm64_glm recognition head (~2.4 GB checkpoint on first use) |
mhcmatch[structure] |
tcren |
the structure-based ΔΔG head |
mhcmatch[precursor] |
vdjmatch |
precursor-frequency estimates |
mhcmatch[docs] |
sphinx, pydata-sphinx-theme |
building the documentation |
mhcmatch bootstrap optionally pre-fetches the ligand panel (~16 MB) — it only decides when
reference data is fetched, which matters on a compute node with no outbound network.
Deeper reading
Everything answering "why does it do that" rather than "how do I run it" is in README_EXT.md, so this page stays the length of a page:
| Batch and threads | read before scripting a loop |
| Caching calibration across jobs | the one environment variable a cluster run wants |
| Composition is not ranking | why a cassette is a set problem, and what lam compares |
What rank costs |
where the time goes, and which stages ship off |
| The two axes | presentation and recognition, and what each carries |
| Presentation and affinity are not the same term | why both are fitted and why binder is neither |
| Data | the four staging tiers, and what each fetches |
| Deployment | Nextflow, Snakemake, an overlay for a pipeline you already run, containers and SLURM |
| Benchmarks | what is measured where |
Development
bash setup.sh # repo-local .venv + editable install (uses a sibling ../seqtree if present)
bash setup.sh --tests # + pytest
pytest -q
Theory and derivations are in the manuscript repo (latex_sn/, Methods and Supplementary Note 1);
what is planned and in flight is in ROADMAP.md.
Release files for mhcmatch 1.20.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| mhcmatch-1.20.0.tar.gz | 10.1 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| mhcmatch-1.20.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 19.4 MB
Release files / mhcmatch-1.20.0.tar.gz
| Download URL | mhcmatch-1.20.0.tar.gz |
|---|---|
| Size | 10.1 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
19356c668f11fc0429254f8898bde45344fc63c74780f5fc369364355dd573b9
|
|
BLAKE2b-256 checksum How to use checksums |
b806a81dede2c1c5d8a8cb346811eabf3acb969ae8a664fbe2fddf467b9f04e6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 20, 2026.
Transparency logRelease files / mhcmatch-1.20.0-py3-none-any.whl
| Download URL | mhcmatch-1.20.0-py3-none-any.whl |
|---|---|
| Size | 9.3 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b262aa18a64f8c27ddced9c0dd96314141db9676d9524f385d3c315b18c9e00e
|
|
BLAKE2b-256 checksum How to use checksums |
571410f9bb930fb227cf383b1c521b9408693fa37a5b4706d52dec420f2f21b9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 20, 2026.
Transparency log