Skip to content

Commit f5bea86

Browse files
committed
Add predictor MHC context metadata
1 parent efb63a2 commit f5bea86

16 files changed

Lines changed: 357 additions & 77 deletions

README.md

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,10 +152,12 @@ df = ms.predict_dataframe(["SIINFEKL"])
152152
df = ms.predict_proteins_dataframe({"TP53": "MEEPQ..."})
153153
```
154154

155-
### Measurement kinds
155+
### Measurement kinds and MHC context
156156

157157
Each `Prediction` has a `kind` string describing what it measures:
158158

159+
The canonical prediction kind strings are defined in `mhctools.pred.Kind`.
160+
159161
| Kind | Meaning |
160162
|---|---|
161163
| `pMHC_affinity` | Peptide-MHC binding affinity |
@@ -167,6 +169,43 @@ Each `Prediction` has a `kind` string describing what it measures:
167169
| `tap_transport` | TAP transport score (reserved, not yet used) |
168170
| `erap_trimming` | ERAP trimming score (reserved, not yet used) |
169171

172+
Predictors also expose `kind_support()` so downstream code can tell what MHC
173+
context is meaningful for each emitted kind:
174+
175+
```python
176+
support = predictor.kind_support()
177+
support["pMHC_affinity"]
178+
# {"mhc_dependence": "single_allele", "mhc_class": "I"}
179+
```
180+
181+
`mhc_dependence` is one of:
182+
183+
| Value | Meaning |
184+
|---|---|
185+
| `none` | The prediction is MHC-independent; `Prediction.allele` is empty. |
186+
| `single_allele` | The prediction is for one peptide/MHC allele pair; `Prediction.allele` is part of the key. |
187+
| `haplotype` | The prediction uses the requested MHC repertoire jointly; `Prediction.allele` may carry best-allele attribution but is not the prediction key. |
188+
189+
`mhc_class` is one of `none`, `I`, `II`, or `both`.
190+
191+
The allowed metadata values are defined in `mhctools.pred` as
192+
`MHC_DEPENDENCE_VALUES` and `MHC_CLASS_VALUES`.
193+
194+
Examples:
195+
196+
| Predictor | Kind | `mhc_dependence` | `mhc_class` |
197+
|---|---|---|---|
198+
| `NetMHCpan41` | `pMHC_affinity` | `single_allele` | `I` |
199+
| `NetMHCpan41` | `pMHC_presentation` | `single_allele` | `I` |
200+
| `MHCflurry` | `pMHC_affinity` | `single_allele` | `I` |
201+
| `MHCflurry` | `pMHC_presentation` | `haplotype` | `I` |
202+
| `Pepsickle` | `proteasome_cleavage` | `none` | `none` |
203+
204+
For MHCflurry presentation, mhctools calls the upstream presentation model with
205+
the full requested allele set and emits one `pMHC_presentation` record per
206+
peptide. The `allele` field carries MHCflurry's `best_allele` attribution when
207+
available.
208+
170209
### The Prediction object
171210

172211
Every prediction is a frozen, self-contained `Prediction` dataclass:

mhctools/__init__.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
from .binding_prediction import BindingPrediction
22
from .binding_prediction_collection import BindingPredictionCollection
3-
from .pred import Prediction, Pred, PeptideResult, PeptidePreds, Kind, preds_from_rows
3+
from .pred import (
4+
Kind,
5+
MHC_CLASS_VALUES,
6+
MHC_DEPENDENCE_VALUES,
7+
PeptidePreds,
8+
PeptideResult,
9+
Pred,
10+
Prediction,
11+
preds_from_rows,
12+
)
413
from .sample import MultiSample
514
from .iedb import (
615
IedbNetMHCcons,
@@ -63,14 +72,16 @@ def __getattr__(name):
6372
raise AttributeError(
6473
"module %r has no attribute %r" % (__name__, name))
6574

66-
__version__ = "3.13.6"
75+
__version__ = "3.13.7"
6776

6877
__all__ = [
6978
"Prediction",
7079
"Pred", # backward compat alias
7180
"PeptideResult",
7281
"PeptidePreds", # backward compat alias
7382
"Kind",
83+
"MHC_CLASS_VALUES",
84+
"MHC_DEPENDENCE_VALUES",
7485
"preds_from_rows",
7586
"MultiSample",
7687
"BindingPrediction",

mhctools/base_predictor.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@
1919

2020
from .unsupported_allele import UnsupportedAllele
2121
from .binding_prediction_collection import BindingPredictionCollection
22-
from .pred import Prediction, Kind, PeptideResult
22+
from .pred import (
23+
Kind,
24+
PeptideResult,
25+
Prediction,
26+
)
2327

2428
logger = logging.getLogger(__name__)
2529

@@ -101,6 +105,7 @@ class BasePredictor(object):
101105
flank_length = 15
102106
n_flank_length = None
103107
c_flank_length = None
108+
mhc_class = "I"
104109

105110
def __init__(
106111
self,
@@ -331,6 +336,20 @@ def _default_pred_kind(self):
331336
"""Override in subclasses to set the Kind for compat conversion."""
332337
return Kind.pMHC_affinity
333338

339+
def kind_support(self):
340+
"""Predictor-specific MHC context for supported prediction kinds."""
341+
return {
342+
self._default_pred_kind(): {
343+
"mhc_dependence": "single_allele",
344+
"mhc_class": self.mhc_class,
345+
}
346+
}
347+
348+
@property
349+
def supported_kinds(self):
350+
"""Prediction kind strings this predictor can emit."""
351+
return tuple(self.kind_support())
352+
334353
# --- deprecated API (still works) ---
335354

336355
def predict_peptides(self, peptides):

mhctools/bigmhc.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,12 @@
3131
import pandas as pd
3232
import torch
3333

34-
from .pred import Kind, Prediction, PeptideResult, COLUMNS
34+
from .pred import (
35+
COLUMNS,
36+
Kind,
37+
PeptideResult,
38+
Prediction,
39+
)
3540

3641

3742
def _find_bigmhc_dir(bigmhc_path=None):
@@ -133,6 +138,18 @@ def _pred_kind(self):
133138
return Kind.immunogenicity
134139
return Kind.pMHC_presentation
135140

141+
def kind_support(self):
142+
return {
143+
self._pred_kind(): {
144+
"mhc_dependence": "single_allele",
145+
"mhc_class": "I",
146+
}
147+
}
148+
149+
@property
150+
def supported_kinds(self):
151+
return tuple(self.kind_support())
152+
136153
def _predictor_name(self):
137154
return "bigmhc_%s" % self.mode
138155

mhctools/iedb.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,8 @@ def __init__(
325325
IEDB_MHC_CLASS_II_URL = "http://tools-cluster-interface.iedb.org/tools_api/mhcii/"
326326

327327
class IedbNetMHCIIpan(IedbBasePredictor):
328+
mhc_class = "II"
329+
328330
def __init__(
329331
self,
330332
alleles,

mhctools/mhcflurry.py

Lines changed: 62 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from .base_predictor import _check_flank_inputs
1919
from .binding_prediction import BindingPrediction
2020
from .binding_prediction_collection import BindingPredictionCollection
21-
from .pred import Prediction, Kind
21+
from .pred import Kind, Prediction
2222
from .unsupported_allele import UnsupportedAllele
2323

2424
logger = logging.getLogger(__name__)
@@ -99,9 +99,10 @@ class MHCflurry(BasePredictor):
9999
"""
100100
MHCflurry predictor using the modern Class1PresentationPredictor API.
101101
102-
Produces both ``pMHC_affinity`` and ``pMHC_presentation`` predictions
103-
per peptide-allele pair. The legacy ``predict_peptides`` method returns
104-
BindingPrediction objects based on affinity values for backward compat.
102+
Produces per-allele ``pMHC_affinity`` predictions and one haplotype-level
103+
``pMHC_presentation`` prediction per peptide. The legacy
104+
``predict_peptides`` method returns BindingPrediction objects based on
105+
affinity values for backward compat.
105106
106107
See https://github.com/openvax/mhcflurry
107108
"""
@@ -214,11 +215,12 @@ def predict(self, peptides, n_flanks=None, c_flanks=None):
214215
"""
215216
Predict for a list of peptide sequences.
216217
217-
Returns a list of PeptideResult, each containing both
218-
pMHC_affinity and pMHC_presentation Prediction objects per allele.
218+
Returns a list of PeptideResult, each containing one pMHC_affinity
219+
Prediction per allele and one haplotype-level pMHC_presentation
220+
Prediction with best_allele attribution when MHCflurry reports it.
219221
220-
Uses batch prediction across all alleles in a single call for
221-
both affinity and presentation scores.
222+
Uses batch prediction across alleles for affinity and a genotype-level
223+
presentation call for presentation scores.
222224
"""
223225
from .pred import PeptideResult
224226

@@ -243,34 +245,38 @@ def predict(self, peptides, n_flanks=None, c_flanks=None):
243245
include_percentile_ranks=self.include_affinity_percentile_ranks,
244246
)
245247

246-
# Per-allele presentation calls (presentation predictor does
247-
# deconvolution across alleles, so we call per-allele to get
248-
# per-allele presentation scores). Key by mhcflurry's output
249-
# allele string so lookups with aff_df.allele always match.
250-
pres_by_pep_allele = {}
251-
for input_allele in allele_list:
252-
kwargs = {
253-
"peptides": peptide_list,
254-
"alleles": [input_allele],
255-
"include_affinity_percentile": False,
256-
"verbose": 0,
257-
}
258-
if n_flank_list is not None:
259-
kwargs["n_flanks"] = n_flank_list
260-
if c_flank_list is not None:
261-
kwargs["c_flanks"] = c_flank_list
262-
df = self.predictor.predict(**kwargs)
263-
if len(df) != len(peptide_list):
248+
# The presentation predictor deconvolves across the full genotype.
249+
# Preserve that haplotype-level contract here: one presentation
250+
# Prediction per peptide, with best_allele carried as attribution
251+
# when MHCflurry reports it.
252+
kwargs = {
253+
"peptides": peptide_list,
254+
"alleles": allele_list,
255+
"include_affinity_percentile": False,
256+
"verbose": 0,
257+
}
258+
if n_flank_list is not None:
259+
kwargs["n_flanks"] = n_flank_list
260+
if c_flank_list is not None:
261+
kwargs["c_flanks"] = c_flank_list
262+
pres_df = self.predictor.predict(**kwargs)
263+
if len(pres_df) != len(peptide_list):
264+
raise ValueError(
265+
"MHCflurry returned %d presentation row(s) for %d "
266+
"peptide input(s)" % (len(pres_df), len(peptide_list)))
267+
268+
pres_by_peptide_index = {}
269+
for row_position, row in enumerate(pres_df.itertuples(index=False)):
270+
row_index = int(getattr(row, "peptide_num", row_position))
271+
if row_index in pres_by_peptide_index:
264272
raise ValueError(
265-
"MHCflurry returned %d presentation row(s) for %d "
266-
"peptide input(s) and allele '%s'" % (
267-
len(df), len(peptide_list), input_allele))
268-
for row_index, row in enumerate(df.itertuples(index=False)):
269-
output_allele = getattr(row, 'allele', input_allele)
270-
pres_by_pep_allele[(row_index, output_allele)] = (
271-
row.presentation_score,
272-
row.presentation_percentile,
273-
)
273+
"MHCflurry returned duplicate presentation row for "
274+
"peptide index %d" % row_index)
275+
pres_by_peptide_index[row_index] = (
276+
row.presentation_score,
277+
row.presentation_percentile,
278+
getattr(row, "best_allele", getattr(row, "allele", "")),
279+
)
274280

275281
groups = [list() for _ in peptide_list]
276282
for row_index, row in zip(batch_indices, aff_df.itertuples(index=False)):
@@ -301,19 +307,21 @@ def predict(self, peptides, n_flanks=None, c_flanks=None):
301307
predictor_name="mhcflurry",
302308
))
303309

304-
key = (row_index, allele)
305-
if key not in pres_by_pep_allele:
310+
for row_index, pep in enumerate(peptide_list):
311+
if row_index not in pres_by_peptide_index:
306312
raise ValueError(
307313
"MHCflurry: missing presentation score for "
308-
"peptide='%s' allele='%s' (this indicates an allele or "
309-
"peptide string mismatch between the affinity and "
310-
"presentation predictor outputs)" % (pep, allele))
311-
pres_score, pres_pct = pres_by_pep_allele[key]
314+
"peptide index %d, peptide='%s'" % (row_index, pep))
315+
pres_score, pres_pct, best_allele = pres_by_peptide_index[row_index]
316+
n_flank = (
317+
n_flank_list[row_index] if n_flank_list is not None else "")
318+
c_flank = (
319+
c_flank_list[row_index] if c_flank_list is not None else "")
312320
groups[row_index].append(Prediction(
313321
kind=Kind.pMHC_presentation,
314322
score=pres_score,
315323
peptide=pep,
316-
allele=allele,
324+
allele=best_allele or "",
317325
n_flank=n_flank,
318326
c_flank=c_flank,
319327
percentile_rank=pres_pct,
@@ -331,6 +339,18 @@ def predict_with_flanks(self, peptides, n_flanks, c_flanks):
331339
def _default_pred_kind(self):
332340
return Kind.pMHC_affinity
333341

342+
def kind_support(self):
343+
return {
344+
Kind.pMHC_affinity: {
345+
"mhc_dependence": "single_allele",
346+
"mhc_class": "I",
347+
},
348+
Kind.pMHC_presentation: {
349+
"mhc_dependence": "haplotype",
350+
"mhc_class": "I",
351+
},
352+
}
353+
334354

335355
class MHCflurry_Affinity(BasePredictor):
336356
"""

mhctools/netmhc_pan4.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@
1010
# See the License for the specific language governing permissions and
1111
# limitations under the License.
1212

13+
from functools import partial
14+
1315
from .base_commandline_predictor import BaseCommandlinePredictor
1416
from .parsing import parse_netmhcpan4_stdout, parse_netmhcpan_to_preds
15-
from functools import partial
17+
from .pred import Kind
18+
1619

1720
class NetMHCpan4(BaseCommandlinePredictor):
1821
def __init__(
@@ -37,6 +40,7 @@ def __init__(
3740
flags = []
3841
else:
3942
raise ValueError("Unsupported mode", mode)
43+
self.mode = mode
4044

4145
BaseCommandlinePredictor.__init__(
4246
self,
@@ -52,6 +56,18 @@ def __init__(
5256
extra_flags=flags + extra_flags,
5357
process_limit=process_limit)
5458

59+
def kind_support(self):
60+
kind = (
61+
Kind.pMHC_affinity
62+
if self.mode == "binding_affinity" else Kind.pMHC_presentation)
63+
return {
64+
kind: {
65+
"mhc_dependence": "single_allele",
66+
"mhc_class": "I",
67+
}
68+
}
69+
70+
5571
class NetMHCpan4_EL(NetMHCpan4):
5672
"""
5773
Wrapper for NetMHCpan4 when the preferred mode is elution score

0 commit comments

Comments
 (0)