Presentation: the P of EPIC#
What this page is for. Two of the nine terms fitted by mhc1.*.neoantigen are presentation,
and they are not the same quantity measured twice. This page says what each one asks, why both
are needed, and which call produces which.
The one-line version. binder asks whether a peptide out-competes the self peptidome its
allele normally loads — allele-relative. log10a asks how many copies reach the surface at a
stated free-peptide concentration — absolute. Winning a groove does not imply reaching the copy
number a T cell needs, and reaching it does not imply winning the groove: they sit at Spearman
\(\rho\) = +0.7431, not 1.
The two terms#
term |
what it is |
|---|---|
|
\(-\log_{10}\) of the calibrated combined %rank — the Fisher statistic over the
presentation rank and the Potts affinity rank, read as a percentage. Allele-relative. The
presentation rank alone is the separate key |
|
the density axis on its log-odds scale, \(\log_{10} a\) for \(a = [P]/K_d\). Exactly
the logit of |
Occupancy itself is still computed and emitted; a probability entered linearly into a log-odds model is the mis-specification, not the axis.
Getting a number#
mhcmatch binder NLVPMVATV # one number: the combined binder %rank
mhcmatch restriction NLVPMVATV --calibrated # which allele presents it, ranked
mhcmatch affinity NLVPMVATV --allele 'HLA-A*02:01' --wt NLVPMVATL # IC50 + agretopicity
mhcmatch explain NLVPMVATV --allele 'HLA-A*02:01' # every term, side by side
store = mhcmatch.Store.from_pmhc(tier="shortlist", species="human")
store.binder_score("NLVPMVATV", alleles="HLA-A*02:01,HLA-B*07:02", cls="mhc1")
store.restriction("NLVPMVATV", diffuse=True, calibrated=True) # %rank / P(present) / band
Prefer binder_score to a raw model score. It is the calibrated, cross-allele-comparable
single number, and it is a soft-AND: strong only when a peptide is both presented and binds.
What is behind the rank#
Three pieces, each with its own reason to exist:
A learned anchor model, per allele. Position-specific residue log-odds against a chosen null
(mhcmatch.diffusion.AnchorModel). The null is the main per-task knob: "ligand" asks
which allele, "proteome" asks is it presented at all.
Cross-allele diffusion, so rare alleles are not guesses. An allele with a handful of ligands
borrows its motif from groove-similar frequent ones over the 34-mer pseudosequence. A rare
class-II allele’s motif is 67–77 % borrowed. am.score(..., raw=True) disables the
borrowing; the shipped path leaves it on.
Per-allele calibration, so two alleles can be compared. A raw score is on its own allele’s
scale and nothing more. mhcmatch.calibrate.RankCalibrator turns it into a %rank against a
background of that allele’s own distribution, which is what makes P(present), the band and
any cross-allele ranking meaningful. Building the backgrounds is the real cost of a cold run —
about 5 s for the presentation and affinity calibrators, ~45 s for the binder calibrator — and it
is cached for the life of the process and on disk under $MHCMATCH_CALIBRATION_CACHE.
Class-II absolute thresholds require a background conditioned on the query’s peptide length:
cal.percent_rank(allele, score, length=len(peptide)). The default 10,000 draws and seeded
random stream are unchanged. Calibration uses mhcmatch.diffusion.AnchorModel.score_many()
to score bounded batches of core frames, without retaining query vectors on the fitted model.
For direct scoring, model.score_many(peptides, allele) returns scalar-identical scores in
input order. Its batch_bytes argument bounds the temporary array working set; no Python
worker pool or multithreaded BLAS operation is added. mhcmatch.calibrate.RankCalibrator.clear()
releases loaded calibration distributions between independent work units.
Native searches default to one thread and accept a single explicit threads budget. Use
threads=0 only to opt into available CPUs (including process affinity and SLURM’s task
allocation). When processes own parallelism, set numerical-library thread limits in the parent
before spawning/importing workers and keep each worker’s native search budget at one. The
Nextflow and Snakemake integrations apply these limits before starting the CLI.
Bands and cut-offs are per class#
NetMHCpan calls class I strong at %rank <= 0.5 and weak at <= 2.0; NetMHCIIpan calls class II
strong at <= 2.0 and weak at <= 10.0. A single number is therefore the weak cut in one
class and the strong cut in the other, which is why the tiers are named rather than numeric, and
why band takes the queried class’s own cut-offs. Nothing is dropped by default. Full table:
What gets dropped, and what can never be.
Class II: the register is chosen, not assumed#
A class-II ligand is longer than its 9-mer core, so the model must decide which nine positions sit
in the groove. AnchorModel fits a mixture over registers by EM and scores the register it
chose — and the anchors, tcr_facing and the agretopicity comparison all report from that
register rather than from a heuristic one, so the reported core is the core that was scored.
footprint="anchor" is never right on the predict path: MHC2_ANCHORS = (1,4,6,9) reaches only
the restriction / vote helpers, while build_scorer ships adaptive, which maps all
nine core positions. A benchmark arm left at anchor understates mhcmatch.
Where to go next#
Ranking neoantigens — how these two terms enter the fitted aggregate, and the other seven.
Command-line reference — every command that produces a presentation number.
API reference —
mhcmatch.store,mhcmatch.diffusion,mhcmatch.calibrate,mhcmatch.affinity.