Skip to content

Add TULIP-TCR pMHC:TCR binding predictor - #232

Merged
iskandr merged 1 commit into
masterfrom
tulip-predictor
Jul 9, 2026
Merged

Add TULIP-TCR pMHC:TCR binding predictor#232
iskandr merged 1 commit into
masterfrom
tulip-predictor

Conversation

@iskandr

@iskandr iskandr commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Adds a Tulip predictor wrapping TULIP-TCR — pMHC:TCR binding, like NetTCR, but it also takes the presenting MHC allele as an input.

Supersedes #222 (auto-closed when its stacked base branch was deleted on the #221 merge). Same commit, now rebased directly onto master. Version 3.19.0 → 3.20.0.

The licensing/isolation problem (why this looks different from other predictors)

TULIP-TCR is GPLv3 and is coupled to transformers==4.32.1 (its model code is a fork of that release's internals). mhctools is Apache-2.0 and depends on neither torch nor transformers. So this wrapper vendors none of TULIP — no source, no weights, no tokenizers. It runs a user-provided checkout out-of-process, in a separate interpreter, by invoking TULIP's own predict.py via subprocess — the same "shell out to a user-provided install" pattern already used for the DTU netMHC* tools and NetTCR. Nothing in mhctools imports TULIP's GPL code, so the Apache package stays clean.

This is a deliberate deviation from "bundle the weights": bundling GPL weights/source into an Apache package isn't license-clean, and the external-checkout pattern is both correct and consistent with the existing predictors.

Setup (two env vars)

  • TULIP_HOME — a clone of TULIP-TCR (predict.py, src/, tokenizers, released model_weights/).
  • TULIP_PYTHON — an isolated Python 3.11 interpreter with torch + transformers==4.32.1.

scripts/setup_tulip_env.sh does both (clones TULIP, builds the env via uv or venv+pip, smoke-tests the import).

Why 3.11 specifically: transformers==4.32.1 resolves tokenizers 0.13.x, which has no cp312 wheel — on 3.12 it builds from source and needs a Rust toolchain. On 3.11 a prebuilt wheel exists, so the install is wheels-only. (Confirmed empirically: modern transformers does not work — TULIP breaks at model construction on BertConfig.cross_attention_hidden_size, removed in 5.x — so the old pin is required, not optional.)

Usage

from mhctools import Tulip, TCR

tcr = TCR(cdr3a="CAGASGNTGKLIF", cdr3b="CASSIRASYEQYF", name="clone1")
predictor = Tulip()                       # reads TULIP_HOME + TULIP_PYTHON
results = predictor.predict(["GILGFVFTL"], [tcr], mhc="HLA-A*02:01")
results[0].preds[0].score                 # TULIP log-likelihood; higher = more likely binding

predict(peptides, tcrs, mhc=...), predict_pairs([(peptide, TCR[, mhc]), ...]), and predict_dataframe(...) mirror NetTCR. Kind is pMHC_TCR_binding with mhc_dependence="single_allele".

How the bridge works

The wrapper writes an input CSV, runs predict.py in the checkout (cwd=TULIP_HOME so aatok/, mhctok/, src/ resolve), and maps the per-peptide output scores back to each (peptide, MHC, CDR3a, CDR3b) by positionpredict.py doesn't echo the MHC column, so content-joining wouldn't disambiguate same-peptide/different-MHC rows; position within each peptide group does.

Tests & CI

  • tests/test_tulip.py: binary-free unit tests mock the subprocess to cover input-CSV construction, per-peptide output parsing, position/score mapping, MHC pass-through, dedup, and error propagation (added to the public CI subset, runs on all Python versions). End-to-end tests run only when TULIP_HOME + TULIP_PYTHON are set.
  • New integration-tulip CI job: builds the isolated Python-3.11 sidecar, clones the public TULIP repo, and runs the wrapper end-to-end.

Local full suite: 526 passed, 38 skipped, 2 xfailed (real netMHCpan 4.2 + a real TULIP sidecar; end-to-end scores match predict.py's own output exactly).

https://claude.ai/code/session_01LZahFhBSCiehXTESCYQ7wG

Adds a `Tulip` predictor wrapping TULIP-TCR
(https://github.com/barthelemymp/TULIP-TCR), producing pMHC_TCR_binding
predictions like NetTCR but also taking the presenting MHC allele as input.

License / isolation: TULIP-TCR is GPLv3 and pinned to transformers==4.32.1;
mhctools is Apache-2.0 and depends on neither torch nor transformers. So the
wrapper vendors NONE of TULIP — no source, weights, or tokenizers. It runs a
user-provided checkout out-of-process, in a separate interpreter, via TULIP's
own predict.py (the same "shell out to a user-provided install" pattern used
for the DTU netMHC tools and NetTCR). Nothing here imports TULIP's GPL code.

Two things are supplied via constructor args or env vars:
  * TULIP_HOME   — a TULIP-TCR checkout (predict.py, src/, tokenizers, weights)
  * TULIP_PYTHON — an isolated Python 3.11 interpreter with torch and
    transformers==4.32.1. 3.11 matters: transformers 4.32.1 resolves
    tokenizers 0.13.x, which has no cp312 wheel and would otherwise build from
    source (Rust); 3.11 has a prebuilt wheel, so the install needs no compiler.

scripts/setup_tulip_env.sh builds that env (uv or venv+pip) and clones TULIP.

The wrapper writes an input CSV, invokes predict.py in the checkout, and maps
its per-peptide output scores back to (peptide, MHC, CDR3a, CDR3b) by position
(predict.py doesn't echo the MHC column). Scores are TULIP's log-likelihood
(higher = more likely to bind).

Tests (tests/test_tulip.py): binary-free unit tests mock the subprocess to
cover input-CSV construction, per-peptide output parsing, position/score
mapping, MHC pass-through, dedup, and error propagation (added to the public
CI subset); end-to-end tests run only when TULIP_HOME + TULIP_PYTHON are set.
A new integration-tulip CI job builds the isolated sidecar (Python 3.11),
clones the public TULIP repo, and runs the wrapper end-to-end.

Full suite: 526 passed, 38 skipped, 2 xfailed (netMHCpan 4.2 + TULIP sidecar).
Bump version to 3.20.0.

Claude-Session: https://claude.ai/code/session_01LZahFhBSCiehXTESCYQ7wG
@iskandr
iskandr merged commit 71e2a07 into master Jul 9, 2026
5 checks passed
@iskandr
iskandr deleted the tulip-predictor branch July 9, 2026 16:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant