Peptide–MHC presentation, cross-reactivity, and motif tools — the applied peptide–MHC layer on top
of the seqtree fuzzy-search substrate. mhcmatch
productionizes the reference seqtree.pmhc methodology (anchor-masked TCR-facing homology,
presentation-aware E-values, allele guessing) and adds a pseudosequence-based cross-allele
diffusion model that rescues rare alleles by borrowing from groove-similar frequent ones.
The mathematical/statistical theory is in appendix/mhcmatch.tex; the
development plan is in ROADMAP.md.
- MHC restriction & presentation — rank presenting alleles for a peptide (single / set / all, human & mouse), flag non-binders, and scan a whole protein for presented peptides.
- Large-scale similarity search — find similar peptides across big sets / proteomes, either by same-MHC binding (presentation signature) or similar TCR recognition (anchor-masked, TCR-facing); neoantigen molecular mimicry with per-allele E-values.
- Anchor / TCR-facing split — decompose a peptide into anchor and TCR-facing parts (
Xmasks). - Near-exact source lookup — find the self peptide a neoantigen derives from + its parent protein / mutated position, against a reference proteome.
- Motif logos — per-allele information-content logos with length distributions.
- Pseudosequence diffusion — allele similarity, clustering, and kernel-shrinkage pooling over 34-mer groove pseudosequences (rare-allele rescue).
- Quantitative affinity (IC50 nM) — a pan-allele Potts / direct-coupling model (peptide×pocket
couplings, fit on measured IEDB IC50) predicts nM affinity and the neoantigen-fitness differentials
— Łuksza amplitude
A = Kd_WT/Kd_MTand the differential agretopicity index — for MHC-I and MHC-II, human and mouse. Optional structure-based MJ ΔΔG via the[structure]extra (tcren).
bash setup.sh # repo-local .venv + editable install (uses sibling ../seqtree if present)
bash setup.sh --tests # + pytest
bash setup.sh --logo # + logomaker/matplotlib for rendering logosimport mhcmatch
# build from the isalgo/pmhc_data table (full or shortlist tier)
store = mhcmatch.Store.from_pmhc("pmhc_full.tsv.gz", species="human")
store.restriction("NLVPMVATV") # ranked presenting alleles + binder flags
store.is_binder("NLVPMVATV", "HLA-A*02:01")
store.scan_protein(my_protein, cls="mhc1") # presented peptides in a protein
store.decompose("NLVPMVATV", cls="mhc1") # (tcr_facing, presentation) with X masks
# similarity at scale
mhcmatch.search.search("NLVPMVATV", big_peptide_set, mode="tcr") # TCR-facing homologs
mhcmatch.search.find_mimics("EAAGIGILTV", self_set, bacterial_sets={...})
# near-exact source of a neoantigen
pm = mhcmatch.Proteome.from_hf("human") # auto-fetched from HF (or .from_fasta(<your FASTA>))
pm.find_source("NLVPMVATV", max_subs=1)
# pseudosequence allele similarity + rare-allele diffusion
ps = mhcmatch.Pseudoseq("mhc1")
ps.neighbors("HLA-A*02:01", candidates=store.alleles("mhc1"))
# diffusion-powered forward scorer (rescues rare alleles by borrowing from groove-neighbours)
am = store.anchor_model("mhc1") # learned anchor weights + bounded-prior shrinkage
am.score("NLVPMVATV", "HLA-A*02:01") # anchor log-odds; am.score(..., raw=True) disables borrowing
# footprint (which core positions) and background (the log-odds null) tune the model to the question:
store.anchor_model("mhc1", footprint="adaptive") # anchors for rare alleles, full core otherwise
store.anchor_model("mhc1", background="proteome") # presentation null (is it presented at all?)
store.anchor_model("mhc1", background="ligand") # specificity null (which allele? — default)
# calibrated, cross-allele-comparable output (NetMHCpan %Rank_EL analogue + P(present) + band)
for r in store.restriction("NLVPMVATV", cls="mhc1", calibrated=True):
print(r.allele, r.rank, r.p_present, r.band) # e.g. HLA-A*02:01 1.6 0.98 weak
mhcmatch.logo.motif(store, "HLA-A*02:01", "mhc1")
# quantitative affinity + neoantigen-fitness differentials (Potts model, vendored weights)
aff = store.affinity_model("mhc1")
aff.predict_ic50("NLVPMVATV", "HLA-A*02:01") # -> ~64 nM
aff.amplitude("NLVPMVATL", "NLVPMVATV", "HLA-A*02:01") # Kd_WT/Kd_MT (Łuksza eq. 9) -> ~2.05
aff.dai("NLVPMVATL", "NLVPMVATV", "HLA-A*02:01") # differential agretopicity (log10 ratio)
store.affinity_model("mhc2").predict_ic50("PKYVKQNTLKLAT", "HLA-DRB1*15:01") # MHC-II, core auto-locatedmhcmatch decompose NLVPMVATV # anchor / TCR-facing split (no data)
set -x MHCMATCH_PMHC /path/to/pmhc_data # or pass --pmhc to each command
mhcmatch restriction NLVPMVATV --allele 'A*02:01' --diffuse # allele name auto-resolved; rare-aware
mhcmatch restriction NLVPMVATV --calibrated # + %rank, P(present), binding band
mhcmatch scan my_protein.fasta --correction bh # presented windows, BH-FDR controlled
mhcmatch source MKTAYIAKW --proteome human # HF name auto-fetched (or a FASTA path)
mhcmatch logo 'HLA-A*02:01'
mhcmatch affinity NLVPMVATV --allele 'A*02:01' --wt NLVPMVATL # IC50 nM + amplitude A=Kd_WT/Kd_MT + DAI- Reference ligands: the public HF dataset
isalgo/pmhc_data(full / shortlist tiers).Store.from_pmhc()auto-fetchespmhc/pmhc_<tier>.tsv.gzon first use (cached byhuggingface_hub) — no manual download, which is what lets the container/nextflow deploy bootstrap with no pre-staged data. Override with a local copy viaStore.from_pmhc(path=...)or$MHCMATCH_PMHC. - Pseudosequences: 34-mer groove pseudosequences vendored in
src/mhcmatch/data/(see itsPROVENANCE.md). - Reference proteomes: the human (UP000005640) and mouse (UP000000589) UniProt proteomes — plus
pathogen proteomes for mimicry — live in the same HF dataset.
Proteome.from_hf("human")/mhcmatch source --proteome humanauto-fetch them (cached), ormhcmatch bootstrap --proteome human,mouseto pre-fetch. Pass your own FASTA toProteome.from_fastato override.
Benchmarks live in a separate repo.
bench/moved to2026-mhcmatch-benchmark— the head-to-head harness, thebench/results/*.mdtables referenced throughout, and their provenance notes. Paths likebench/results/...below resolve there, not here.
A reproducible head-to-head against NetMHCpan-4.2b and NetMHCIIpan-4.3i lives in
bench/compare/ (results in bench/results/compare_*.md, provenance and caveats in
bench/compare/SOURCES.md). It compares the two tools on the same
per-(peptide, allele) task, stratified by allele rarity, with AUROC / AUPRC / PPV@k, bootstrap CIs and
paired significance. Headline results (shortlist tier, human, seed 0):
- Allele-specificity (which allele presents a peptide — the restriction problem): mhcmatch beats NetMHCpan on MHC-I medium and frequent alleles on AUROC, AUPRC and PPV@k (all p < 0.001; e.g. frequent AUPRC 0.850 vs 0.769). Rare MHC-I is a wash (+0.008 AUROC, p = 0.41).
- Presented-vs-random screening (
background="proteome"): mhcmatch beats NetMHCpan on MHC-I frequent alleles (AUROC p < 0.001, AUPRC 0.881 vs 0.846, p = 0.001). Medium and rare are a wash — the deltas sit inside the CI. This task is much easier for both tools (every AUROC ≥ 0.97). - MHC-II: NetMHCIIpan leads on medium and frequent alleles (p < 0.001); rare is a wash (Δ AUROC
+0.013 specificity / −0.015 screening, both inside the CI). Read these with
compare/SOURCES.mdin hand: NetMHCIIpan trained on essentially all public IEDB eluted-ligand data, so the in-corpus medium/frequent strata are contaminated in its favour and the rare/zero-shot axis is the fair one. - Speed: mhcmatch scores ~68× faster (pure Python, ~195k peptide-allele scores/s).
python bench/compare/run_compare.py --cls mhc1 --decoy-mode hard --background ligand # specificity
python bench/compare/run_compare.py --cls mhc1 --decoy-mode random --background proteome # screeningThe affinity head is benchmarked head-to-head against NetMHCpan-4.2 −BA / NetMHCIIpan-4.3 −BA
on held-out measured IEDB IC50 (bench/affinity/; provenance in
bench/affinity/SOURCES.md). Metric: median per-allele Spearman ρ against
measured log-IC50, and AUROC at the 500 nM binder threshold, on the same held-out (peptide, allele)
pairs. Honest numbers (per-allele held-out split, seed 0):
| class | stratum | alleles | mhcmatch ρ | netMHCpan ρ | mhcmatch AUROC | netMHCpan AUROC |
|---|---|---|---|---|---|---|
| MHC-I | human common | 31 | 0.702 | 0.792 | 0.851 | 0.913 |
| MHC-I | human rare | 37 | 0.485 | 0.765 | 0.754 | 0.930 |
| MHC-II | human common | 28 | 0.531 | 0.774 | 0.798 | 0.923 |
| MHC-II | human rare | 12 | 0.457 | 0.755 | 0.749 | 0.914 |
| MHC-II | mouse (rare) | 5 | 0.507 | 0.716 | 0.787 | 0.893 |
NetMHCpan/IIpan lead on this eval, but the gap is inflated by train/test overlap we cannot undo: both tools trained on much of IEDB, so the held-out pairs are in-sample for them and out-of-sample for mhcmatch. On truly unseen alleles (leave-20-alleles-out, zero training rows for the allele) the Potts model still generalizes — MHC-I common orphan ρ ≈ 0.57 — because its peptide×pocket couplings interpolate across groove-similar alleles. The affinity head is a compact, dependency-light linear model (numpy-only dot product at predict time, ~µs/peptide) and gives the WT-vs-mutant ratio (amplitude / DAI) that percentile ranks cannot express.
python bench/affinity/train_potts.py --cls mhc1 --alpha 40 # MHC-I head-to-head
python bench/affinity/train_potts.py --cls mhc2 --species all --alpha 40 # MHC-II, human + mouseBeta (v0.4). Affinity (IC50 nM) + neoantigen amplitude/DAI shipped for MHC-I/II, human & mouse; optional
structure-based MJ ΔΔG via the [structure] extra. See ROADMAP.md for what's next
(order-k Markov / covariance null, a learned reranker for rare-allele screening, full-tier + temporal
cluster sweeps, and the stability/cleavage/immunogenicity predictors).