Minjun Kang commited on
Commit
62a01bd
Β·
1 Parent(s): e15df09

update feature

Browse files
__pycache__/examples.cpython-310.pyc ADDED
Binary file (7.99 kB). View file
 
__pycache__/t5_utils.cpython-310.pyc ADDED
Binary file (3.26 kB). View file
 
app.py CHANGED
@@ -9,7 +9,6 @@ Pipeline:
9
  (4) Tab 2 – Condition Screening: pick one condition to vary, fix the rest β†’ plot
10
  """
11
 
12
- import re
13
  import sys
14
  import warnings
15
  from pathlib import Path
@@ -20,11 +19,23 @@ import matplotlib
20
  matplotlib.use("Agg")
21
  import matplotlib.pyplot as plt
22
  import joblib
23
- import torch
24
  import gradio as gr
25
  import spaces
26
  from huggingface_hub import snapshot_download
27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  warnings.filterwarnings("ignore")
29
 
30
  # ── Paths ─────────────────────────────────────────────────────────────────────
@@ -36,7 +47,6 @@ BASE_DIR = Path(__file__).parent
36
  # at module import (app startup, plain CPU context) β€” before any @spaces.GPU
37
  # call β€” so extract_t5_feature() never blocks on a network download while
38
  # holding a ZeroGPU allocation (which has a short time budget).
39
- T5_REPO_ID = "Rostlab/prot_t5_xl_half_uniref50-enc"
40
  snapshot_download(T5_REPO_ID)
41
 
42
  # ── Physical constants (from preprocess/misc.py) ──────────────────────────────
@@ -69,23 +79,10 @@ SCREEN_LABELS = {
69
  VALID_AA = set("ACDEFGHIKLMNPQRSTVWY")
70
  ALLOW_AA = VALID_AA | set("XBJOUZ")
71
 
72
- # Ξ±-synuclein (used as built-in example)
73
- EXAMPLE_SEQ = (
74
- "MDVFMKGLSKAKEGVVAAAEKTKQGVAEAAGKTKEGVLYVGSKTKEGVVHGVATVAEKTK"
75
- "EQVTNVGGAVVTGVTAVAQKTVEGAGSIAAATGFVKKDQLGKNEEGAPQEGILEDMPVDP"
76
- "DNEAYEMPSEEGYQDYEPEA"
77
- )
78
-
79
  # ── Lazy-loaded singletons ────────────────────────────────────────────────────
80
- _t5_tokenizer = None
81
- _t5_model = None
82
  _llps_model = None
83
 
84
 
85
- def get_device() -> str:
86
- return "cuda" if torch.cuda.is_available() else "cpu"
87
-
88
-
89
  def load_llps_model():
90
  global _llps_model
91
  if _llps_model is None:
@@ -103,78 +100,15 @@ def load_llps_model():
103
  return _llps_model
104
 
105
 
106
- def load_t5():
107
- global _t5_tokenizer, _t5_model
108
- if _t5_model is None:
109
- from transformers import AutoTokenizer, T5EncoderModel
110
- import transformers.utils.import_utils as _hf_utils
111
-
112
- # Rostlab/prot_t5_xl_half_uniref50-enc is only available as .bin (no safetensors).
113
- # transformers 5.x blocks torch.load on torch < 2.6 due to CVE-2025-32434.
114
- # We bypass that gate for this specific trusted checkpoint from the official
115
- # HuggingFace Hub. The check lives in two places β€” import_utils AND the
116
- # locally-imported name in modeling_utils β€” so both must be patched.
117
- import transformers.modeling_utils as _modeling_utils
118
-
119
- _noop = lambda: None
120
- _orig_hf = _hf_utils.check_torch_load_is_safe
121
- _orig_mdl = _modeling_utils.check_torch_load_is_safe
122
-
123
- _hf_utils.check_torch_load_is_safe = _noop
124
- _modeling_utils.check_torch_load_is_safe = _noop
125
-
126
- try:
127
- # Already fetched by the module-level snapshot_download() above, so
128
- # ZeroGPU calls never hit the network here β€” a cold download would
129
- # blow past the GPU-allocation time budget and get aborted.
130
- # local_files_only=True enforces that guarantee.
131
- repo_id = T5_REPO_ID
132
- dev = get_device()
133
- dtype = torch.float16 if dev == "cuda" else torch.float32
134
-
135
- _t5_tokenizer = AutoTokenizer.from_pretrained(
136
- repo_id, do_lower_case=False, local_files_only=True
137
- )
138
- _t5_model = (
139
- T5EncoderModel.from_pretrained(
140
- repo_id, torch_dtype=dtype, local_files_only=True
141
- )
142
- .to(dev)
143
- .eval()
144
- )
145
- _t5_model.requires_grad_(False)
146
- finally:
147
- _hf_utils.check_torch_load_is_safe = _orig_hf # always restore
148
- _modeling_utils.check_torch_load_is_safe = _orig_mdl
149
-
150
- return _t5_tokenizer, _t5_model
151
-
152
-
153
  # ── Feature extraction ────────────────────────────────────────────────────────
 
 
 
 
 
154
  @spaces.GPU
155
  def extract_t5_feature(sequence: str) -> np.ndarray:
156
- """
157
- Return mean-pooled ProtT5-XL embedding for a single sequence.
158
- Output shape: (1024,) β€” matches the feature dimension LLPSense was trained on.
159
- """
160
- tok, mdl = load_t5()
161
- dev = get_device()
162
-
163
- # Replace ambiguous residues with X (same as original pipeline)
164
- clean = re.sub(r"[UZOB]", "X", sequence.strip().upper())
165
- spaced = " ".join(list(clean))
166
-
167
- enc = tok([spaced], add_special_tokens=True, padding="longest", return_tensors="pt")
168
- input_ids = enc["input_ids"].to(dev)
169
- attention_mask = enc["attention_mask"].to(dev)
170
-
171
- with torch.no_grad():
172
- hidden = mdl(input_ids=input_ids, attention_mask=attention_mask).last_hidden_state
173
- # Mean-pool over sequence positions (ignoring padding tokens)
174
- feat = (hidden * attention_mask[..., None]).sum(dim=1) / \
175
- attention_mask.sum(dim=1, keepdim=True)
176
-
177
- return feat[0].float().cpu().numpy() # (1024,)
178
 
179
 
180
  # ── Condition vector builder ──────────────────────────────────────────────────
@@ -344,7 +278,14 @@ def cb_extract(sequence: str):
344
  yield None, _SPINNER_HTML, None, "", None, ""
345
 
346
  try:
347
- feat = extract_t5_feature(seq)
 
 
 
 
 
 
 
348
  yield feat, _status_ok(len(seq), feat.shape[0]), None, "", None, ""
349
  except Exception as e:
350
  yield None, _status_err(str(e)), None, "", None, ""
@@ -509,14 +450,21 @@ with gr.Blocks(
509
  label="Amino Acid Sequence (1-letter code)",
510
  placeholder="Paste your protein sequence here (e.g. MDVFMKGLSK…)",
511
  lines=5,
512
- value=EXAMPLE_SEQ,
513
  elem_id="seq-input",
514
  )
515
- gr.Examples(
516
- examples=[[EXAMPLE_SEQ]],
517
- inputs=[seq_box],
518
- label="Example: Ξ±-synuclein (SNCA)",
519
- )
 
 
 
 
 
 
 
520
 
521
  with gr.Group():
522
  gr.Markdown("## Step 2 β€” Extract ProtT5 Feature")
@@ -675,6 +623,18 @@ with gr.Blocks(
675
  outputs=[feat_state, extract_status, pred_plot, pred_text, screen_plot, screen_text],
676
  )
677
 
 
 
 
 
 
 
 
 
 
 
 
 
678
  # ── Footer ────────────────────────────────────────────────────────────────
679
  gr.Markdown("""
680
  ---
 
9
  (4) Tab 2 – Condition Screening: pick one condition to vary, fix the rest β†’ plot
10
  """
11
 
 
12
  import sys
13
  import warnings
14
  from pathlib import Path
 
19
  matplotlib.use("Agg")
20
  import matplotlib.pyplot as plt
21
  import joblib
 
22
  import gradio as gr
23
  import spaces
24
  from huggingface_hub import snapshot_download
25
 
26
+ from examples import EXAMPLES, feature_path, find_example_by_seq
27
+
28
+ # Only surface examples that actually have a pre-computed T5 feature cached
29
+ # in assets/ β€” an entry added via preprocess/add_example.py but not yet run
30
+ # through preprocess/extract_example_feat.py would otherwise show up in the
31
+ # picker and silently fall back to a slow/GPU extraction on first click.
32
+ AVAILABLE_EXAMPLES = [example for example in EXAMPLES if feature_path(example["id"]).exists()]
33
+ from t5_utils import (
34
+ T5_REPO_ID,
35
+ extract_t5_feature as _extract_t5_feature_core,
36
+ read_feature_h5,
37
+ )
38
+
39
  warnings.filterwarnings("ignore")
40
 
41
  # ── Paths ─────────────────────────────────────────────────────────────────────
 
47
  # at module import (app startup, plain CPU context) β€” before any @spaces.GPU
48
  # call β€” so extract_t5_feature() never blocks on a network download while
49
  # holding a ZeroGPU allocation (which has a short time budget).
 
50
  snapshot_download(T5_REPO_ID)
51
 
52
  # ── Physical constants (from preprocess/misc.py) ──────────────────────────────
 
79
  VALID_AA = set("ACDEFGHIKLMNPQRSTVWY")
80
  ALLOW_AA = VALID_AA | set("XBJOUZ")
81
 
 
 
 
 
 
 
 
82
  # ── Lazy-loaded singletons ────────────────────────────────────────────────────
 
 
83
  _llps_model = None
84
 
85
 
 
 
 
 
86
  def load_llps_model():
87
  global _llps_model
88
  if _llps_model is None:
 
100
  return _llps_model
101
 
102
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  # ── Feature extraction ────────────────────────────────────────────────────────
104
+ # The actual tokenize/forward/mean-pool logic lives in t5_utils.py, shared
105
+ # with preprocess/extract_example_feat.py so the offline-cached example
106
+ # features in assets/ always match what this would compute live. Only the
107
+ # @spaces.GPU wrapping (ZeroGPU allocation) is app-specific, and cb_extract()
108
+ # below skips calling this entirely when a cached feature is available.
109
  @spaces.GPU
110
  def extract_t5_feature(sequence: str) -> np.ndarray:
111
+ return _extract_t5_feature_core(sequence)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
 
114
  # ── Condition vector builder ──────────────────────────────────────────────────
 
278
  yield None, _SPINNER_HTML, None, "", None, ""
279
 
280
  try:
281
+ # Known example sequence with a pre-computed feature? Skip the T5
282
+ # model/GPU call entirely and load it straight from assets/.
283
+ example = find_example_by_seq(seq)
284
+ cached_path = feature_path(example["id"]) if example else None
285
+ if cached_path and cached_path.exists():
286
+ feat = read_feature_h5(cached_path)
287
+ else:
288
+ feat = extract_t5_feature(seq)
289
  yield feat, _status_ok(len(seq), feat.shape[0]), None, "", None, ""
290
  except Exception as e:
291
  yield None, _status_err(str(e)), None, "", None, ""
 
450
  label="Amino Acid Sequence (1-letter code)",
451
  placeholder="Paste your protein sequence here (e.g. MDVFMKGLSK…)",
452
  lines=5,
453
+ value=AVAILABLE_EXAMPLES[0]["seq"] if AVAILABLE_EXAMPLES else "",
454
  elem_id="seq-input",
455
  )
456
+ # Clicking an example fills seq_box; the matching Step-2 extraction
457
+ # is chained onto it further down (once the Step-3 output components
458
+ # exist) so selecting an example runs extraction automatically.
459
+ # Skipped entirely if no example has a cached feature yet.
460
+ example_picker = None
461
+ if AVAILABLE_EXAMPLES:
462
+ with gr.Accordion("πŸ§ͺ Examples (We provide preprocessed T5 feature)", open=False):
463
+ example_picker = gr.Examples(
464
+ examples=[[example["seq"]] for example in AVAILABLE_EXAMPLES],
465
+ example_labels=[example["name"] for example in AVAILABLE_EXAMPLES],
466
+ inputs=[seq_box],
467
+ )
468
 
469
  with gr.Group():
470
  gr.Markdown("## Step 2 β€” Extract ProtT5 Feature")
 
623
  outputs=[feat_state, extract_status, pred_plot, pred_text, screen_plot, screen_text],
624
  )
625
 
626
+ # Selecting a built-in example runs extraction automatically (no manual
627
+ # Step-2 click needed) β€” cache-hit examples resolve near-instantly since
628
+ # cb_extract reads their pre-computed feature from assets/ instead of
629
+ # calling the T5 model. example_picker is None when no example has a
630
+ # cached feature yet (see AVAILABLE_EXAMPLES above).
631
+ if example_picker is not None:
632
+ example_picker.load_input_event.then(
633
+ fn=cb_extract,
634
+ inputs=[seq_box],
635
+ outputs=[feat_state, extract_status, pred_plot, pred_text, screen_plot, screen_text],
636
+ )
637
+
638
  # ── Footer ────────────────────────────────────────────────────────────────
639
  gr.Markdown("""
640
  ---
assets/ascl1.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fec1ca3caebb001a1ec821900f7cb57c3b748ea6754b47a264a45cdbf600e88f
3
+ size 6160
assets/atn1.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fd29669387a1dae9268a8e75381ad6899f96908c403b16aa409abe3d7262402a
3
+ size 6157
assets/fbll1.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3be5535f596b4ff93013c21cddd8fcef9e3bcdde5391f42bfb26fb670b7d4157
3
+ size 6157
assets/foxp2.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:46cd534b6b9fadac71a887ea7ca9b02eb2616c9a89ee9babbfd09aa467288653
3
+ size 6174
assets/semg2.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:330004af079a2bf62cd36951258c0414da4a1e1f5acc9259e31dc520a0b60975
3
+ size 6180
assets/sgta.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:17d0cacc822200a1f62353f97dfe7bc9be6920abf2c99fd1e504f269cb50ae96
3
+ size 6164
assets/ss18.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:dabf03dddeb0e3cc6b45564a1978cd2a7d92befaf5f452dfc1dbbf39b1ab500b
3
+ size 6156
assets/synuclein.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fff3a1909b4c47d3ee84fd3e0601c9ca5dd1ce1cfc66df1f0a1066087beec7c5
3
+ size 6149
assets/ubqln4.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4e81867866cb676c5c6cab266a887711cc96227ce7ebbd06258db7fd413d07b2
3
+ size 6151
assets/ubqln_mut10.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6bac2120b88b2f792579f8ac36da89b2d36a40f8677d48689facca9550df5b2e
3
+ size 6144
assets/ubqln_mut20.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:588896444e29efdd1066719561ced11fcdf9d7149632a356051d55775d55b88c
3
+ size 6146
assets/ubqln_mut30.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:229cfa12dc1810d93e96da3058aba885c9efdaa0970cf852e36f088b8d9ab21c
3
+ size 6152
examples.py ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Built-in example sequences for the LLPSense demo.
2
+
3
+ Shared by app.py (UI + on-the-fly extraction fallback) and
4
+ preprocess/extract_example_feat.py (offline pre-computation), so both stay
5
+ in sync on which sequences have a cached ProtT5 feature under assets/.
6
+
7
+ To add another example, just append a dict with a unique "id" here, then
8
+ run `python preprocess/extract_example_feat.py` to populate its cached
9
+ feature in assets/ before deploying.
10
+ """
11
+
12
+ from pathlib import Path
13
+
14
+ ASSETS_DIR = Path(__file__).parent / "assets"
15
+
16
+ EXAMPLES = [
17
+ {
18
+ "id": "synuclein",
19
+ "name": "Ξ±-Synuclein",
20
+ "seq": (
21
+ "MDVFMKGLSKAKEGVVAAAEKTKQGVAEAAGKTKEGVLYVGSKTKEGVVHGVATVAEKTK"
22
+ "EQVTNVGGAVVTGVTAVAQKTVEGAGSIAAATGFVKKDQLGKNEEGAPQEGILEDMPVDP"
23
+ "DNEAYEMPSEEGYQDYEPEA"
24
+ ),
25
+ },
26
+ {
27
+ "id": "ubqln4",
28
+ "name": "UBQLN-4",
29
+ "seq": (
30
+ "MAEPSGAETRPPIRVTVKTPKDKEEIVICDRASVKEFKEEISRRFKAQQDQLVLIFAGKI"
31
+ "LKDGDTLNQHGIKDGLTVHLVIKTPQKAQDPAAATASSPSTPDPASAPSTTPASPATPAQ"
32
+ "PSTSGSASSDAGSGSRRSSGGGPSPGAGEGSPSATASILSGFGGILGLGSLGLGSANFME"
33
+ "LQQQMQRQLMSNPEMLSQIMENPLVQDMMSNPDLMRHMIMANPQMQQLMERNPEISHMLN"
34
+ "NPELMRQTMELARNPAMMQEMMRNQDRALSNLESIPGGYNALRRMYTDIQEPMFSAAREQ"
35
+ "FGNNPFSSLAGNSDSSSSQPLRTENREPLPNPWSPSPPTSQAPGSGGEGTGGSGTSQVHP"
36
+ "TVSNPFGINAASLGSGMFNSPEMQALLQQISENPQLMQNVISAPYMRSMMQTLAQNPDFA"
37
+ "AQMMVNVPLFAGNPQLQEQLRLQLPVFLQQMQNPESLSILTNPRAMQALLQIQQGLQTLQ"
38
+ "TEAPGLVPSLGSFGISRTPAPSAGSNAGSTPEAPTSSPATPATSSPTGASSAQQQLMQQM"
39
+ "IQLLAGSGNSQVQTPEVRFQQQLEQLNSMGFINREANLQALIATGGDINAAIERLLGSQL"
40
+ "S"
41
+ ),
42
+ },
43
+ {
44
+ "id": 'ascl1',
45
+ "name": 'ASCL1',
46
+ "seq": (
47
+ "MESSAKMESGGAGQQPQPQPQQPFLPPAACFFATAAAAAAAAAAAAAQSAQQQQQQQQQQ"
48
+ "QQAPQLRPAADGQPSGGGHKSAPKQVKRQRSSSPELMRCKRRLNFSGFGYSLPQQQPAAV"
49
+ "ARRNERERNRVKLVNLGFATLREHVPNGAANKKMSKVETLRSAVEYIRALQQLLDEHDAV"
50
+ "SAAFQAGVLSPTISPNYSNDLNSMAGSPVSSYSSDEGSYDPLSPEEQELLDFTNWF"
51
+ ),
52
+ },
53
+ {
54
+ "id": 'atn1',
55
+ "name": 'ATN1',
56
+ "seq": (
57
+ "MKTRQNKDSMSMRSGRKKEAPGPREELRSRGRASPGGVSTSSSDGKAEKSRQTAKKARVE"
58
+ "EASTPKVNKQGRSEEISESESEETNAPKKTKTEQELPRPQSPSDLDSLDGRSLNDDGSSD"
59
+ "PRDIDQDNRSTSPSIYSPGSVENDSDSSSGLSQGPARPYHPPPLFPPSPQPPDSTPRQPE"
60
+ "ASFEPHPSVTPTGYHAPMEPPTSRMFQAPPGAPPPHPQLYPGGTGGVLSGPPMGPKGGGA"
61
+ "ASSVGGPNGGKQHPPPTTPISVSSSGASGAPPTKPPTTPVGGGNLPSAPPPANFPHVTPN"
62
+ "LPPPPALRPLNNASASPPGLGAQPLPGHLPSPHAMGQGMGGLPPGPEKGPTLAPSPHSLP"
63
+ "PASSSAPAPPMRFPYSSSSSSSAAASSSSSSSSSSASPFPASQALPSYPHSFPPPTSLSV"
64
+ "SNQPPKYTQPSLPSQAVWSQGPPPPPPYGRLLANSNAHPGPFPPSTGAQSTAHPPVSTHH"
65
+ "HHHQQQQQQQQQQQQQQQQQQQHHGNSGPPPPGAFPHPLEGGSSHHAHPYAMSPSLGSLR"
66
+ "PYPPGPAHLPPPHSQVSYSQAGPNGPPVSSSSNSSSSTSQGSYPCSHPSPSQGPQGAPYP"
67
+ "FPPVPTVTTSSATLSTVIATVASSPAGYKTASPPGPPPYGKRAPSPGAYKTATPPGYKPG"
68
+ "SPPSFRTGTPPGYRGTSPPAGPGTFKPGSPTVGPGPLPPAGPSGLPSLPPPPAAPASGPP"
69
+ "LSATQIKQEPAEEYETPESPVPPARSPSPPPKVVDVPSHASQSARFNKHLDRGFNSCARS"
70
+ "DLYFVPLEGSKLAKKRADLVEKVRREAEQRAREEKEREREREREKEREREKERELERSVK"
71
+ "LAQEGRAPVECPSLGPVPHRPPFEPGSAVATVPPYLGPDTPALRTLSEYARPHVMSPGNR"
72
+ "NHPFYVPLGAVDPGLLGYNVPALYSSDPAAREREREARERDLRDRLKPGFEVKPSELEPL"
73
+ "HGVPGPGLDPFPRHGGLALQPGPPGLHPFPFHPSLGPLERERLALAAGPALRPDMSYAER"
74
+ "LAAERQHAERVAALGNDPLARLQMLNVTPHHHQHSHIHSHLHLHQQDAIHAASASVHPLI"
75
+ "DPLASGSHLTRIPYPAGTLPNPLLPHPLHENEVLRHQLFAAPYRDLPASLSAPMSAAHQL"
76
+ "QAMHAQSAELQRLALEQQQWLHAHHPLHSVPLPAQEDYYSHLKKESDKPL"
77
+ ),
78
+ },
79
+ {
80
+ "id": 'fbll1',
81
+ "name": 'FBLL1',
82
+ "seq": (
83
+ "MKSAASSRGGGGGGRGGGGWGSWGGGRGGGGGAGKGGGGDGGGQGGKGGFGARARGFGGG"
84
+ "GRGRGRGGGDGKDRGGGGQRRGGVAKSKSRRRKGAMVVSVEPHRHEGVFIYRGAEDALVT"
85
+ "LNMVPGQSVYGERRVTVTEGGVKQEYRTWNPFRSKLAAAILGGVDQIHIKPKSKVLYLGA"
86
+ "ASGTTVSHVSDIIGPDGLVYAVEFSHRAGRDLVNVAKKRTNIIPVLEDARHPLKYRMLIG"
87
+ "MVDVIFADVAQPDQSRIVALNAHTFLRNGGHFLISIKANCIDSTASAEAVFASEVRKLQQ"
88
+ "ENLKPQEQLTLEPYERDHAVVVGVYRPLPKSSSK"
89
+ ),
90
+ },
91
+ {
92
+ "id": 'foxp2',
93
+ "name": 'FOXP2',
94
+ "seq": (
95
+ "MMQESATETISNSSMNQNGMSTLSSQLDAGSRDGRSSGDTSSEVSTVELLHLQQQQALQA"
96
+ "ARQLLLQQQTSGLKSPKSSDKQRPLQVPVSVAMMTPQVITPQQMQQILQQQVLSPQQLQA"
97
+ "LLQQQQAVMLQQQQLQEFYKKQQEQLHLQLLQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ"
98
+ "QQQQQQQQQQQHPGKQAKEQQQQQQQQQQLAAQQLVFQQQLLQMQQLQQQQHLLSLQRQG"
99
+ "LISIPPGQAALPVQSLPQAGLSPAEIQQLWKEVTGVHSMEDNGIKHGGLDLTTNNSSSTT"
100
+ "SSNTSKASPPITHHSIVNGQSSVLSARRDSSSHEETGASHTLYGHGVCKWPGCESICEDF"
101
+ "GQFLKHLNNEHALDDRSTAQCRVQMQVVQQLEIQLSKERERLQAMMTHLHMRPSEPKPSP"
102
+ "KPLNLVSSVTMSKNMLETSPQSLPQTPTTPTAPVTPITQGPSVITPASVPNVGAIRRRHS"
103
+ "DKYNIPMSSEIAPNYEFYKNADVRPPFTYATLIRQAIMESSDRQLTLNEIYSWFTRTFAY"
104
+ "FRRNAATWKNAVRHNLSLHKCFVRVENVKGAVWTVDEVEYQKRRSQKITGSPTLVKNIPT"
105
+ "SLGYGAALNASLQAALAESSLPLLSNPGLINNASSGLLQAVHEDLNGSLDHIDSNGNSSP"
106
+ "GCSPQPHIHSIHVKEEPVIAEDEDCPMSLVTTANHSPELEDDREIEEEPLSEDLE"
107
+ ),
108
+ },
109
+ {
110
+ "id": 'semg2',
111
+ "name": 'SEMG2',
112
+ "seq": (
113
+ "MKSIILFVLSLLLILEKQAAVMGQKGGSKGQLPSGSSQFPHGQKGQHYFGQKDQQHTKSK"
114
+ "GSFSIQHTYHVDINDHDWTRKSQQYDLNALHKATKSKQHLGGSQQLLNYKQEGRDHDKSK"
115
+ "GHFHMIVIHHKGGQAHHGTQNPSQDQGNSPSGKGLSSQCSNTEKRLWVHGLSKEQASASG"
116
+ "AQKGRTQGGSQSSYVLQTEELVVNKQQRETKNSHQNKGHYQNVVDVREEHSSKLQTSLHP"
117
+ "AHQDRLQHGPKDIFTTQDELLVYNKNQHQTKNLSQDQEHGRKAHKISYPSSRTEERQLHH"
118
+ "GEKSVQKDVSKGSISIQTEEKIHGKSQNQVTIHSQDQEHGHKENKISYQSSSTEERHLNC"
119
+ "GEKGIQKGVSKGSISIQTEEQIHGKSQNQVRIPSQAQEYGHKENKISYQSSSTEERRLNS"
120
+ "GEKDVQKGVSKGSISIQTEEKIHGKSQNQVTIPSQDQEHGHKENKMSYQSSSTEERRLNY"
121
+ "GGKSTQKDVSQSSISFQIEKLVEGKSQIQTPNPNQDQWSGQNAKGKSGQSADSKQDLLSH"
122
+ "EQKGRYKQESSESHNIVITEHEVAQDDHLTQQYNEDRNPIST"
123
+ ),
124
+ },
125
+ {
126
+ "id": 'sgta',
127
+ "name": 'SGTA',
128
+ "seq": (
129
+ "MDNKKRLAYAIIQFLHDQLRHGGLSSDAQESLEVAIQCLETAFGVTVEDSDLALPQTLPE"
130
+ "IFEAAATGKEMPQDLRSPARTPPSEEDSAEAERLKTEGNEQMKVENFEAAVHFYGKAIEL"
131
+ "NPANAVYFCNRAAAYSKLGNYAGAVQDCERAICIDPAYSKAYGRMGLALSSLNKHVEAVA"
132
+ "YYKKALELDPDNETYKSNLKIAELKLREAPSPTGGVGSFDIAGLLNNPGFMSMASNLMNN"
133
+ "PQIQQLMSGMISGGNNPLGTPGTSPSQNDLASLIQAGQQFAQQMQQQNPELIEQLRSQIR"
134
+ "SRTPSASNDDQQE"
135
+ ),
136
+ },
137
+ {
138
+ "id": 'ss18',
139
+ "name": 'SS18',
140
+ "seq": (
141
+ "MSVAFAAPRQRGKGEITPAAIQKMLDDNNHLIQCIMDSQNKGKTSECSQYQQMLHTNLVY"
142
+ "LATIADSNQNMQSLLPAPPTQNMPMGPGGMNQSGPPPPPRSHNMPSDGMVGGGPPAPHMQ"
143
+ "NQMNGQMPGPNHMPMQGPGPNQLNMTNSSMNMPSSSHGSMGGYNHSVPSSQSMPVQNQMT"
144
+ "MSQGQPMGNYGPRPNMSMQPNQGPMMHQQPPSQQYNMPQGGGQHYQGQQPPMGMMGQVNQ"
145
+ "GNHMMGQRQIPPYRPPQQGPPQQYSGQEDYYGDQYSHGGQGPPEGMNQQYYPDGHNDYGY"
146
+ "QQPSYPEQGYDRPYEDSSQHYYEGGNSQYGQQQDAYQGPPPQQGYPPQQQQYPGQQGYPG"
147
+ "QQQGYGPSQGGPGPQYPNYPQGQGQQYGGYRPTQPGPPQPPQQRPYGYDQGQYGNYQQ"
148
+ ),
149
+ },
150
+ {
151
+ "id": 'ubqln_mut10',
152
+ "name": 'UBQLN_mut10',
153
+ "seq": (
154
+ "MAEPSGAETRPPIRVTVKTPKDKEEIVICDRASVKEFKEEISRRFKAQQDQLVLIFAGKI"
155
+ "LKDGDTLNQHGIKDGLTVHLVIKTPQKAQDPAAATASSPSTPDPASAPSTTPASPATPAQ"
156
+ "PSTSGSASSDAGSGSRRSSGGGPSPGAGEGSPSATASILSGFGGILGLGSLGLGSANFME"
157
+ "LQQQMQRQLMSNPEMLSQICENPLVQDMMSNPDLMRHCCMANPQYQQLCERNPEISHMLN"
158
+ "NPELMRQTMELARNPAMMQEMMRNQDRALSNCESIPGGYNALRRMYTDIQEPMFSAAREQ"
159
+ "FGNNPFSSLAGNSDSSSSQPLRTENREPLPNPWSPSPPTSQAPGSGGEGTGGSGTSQVHP"
160
+ "TVSNPFGINAASLGSGMCNSPEMQALLQQISENCQLMQNVISAPYMRSMMQTLAQNPDFA"
161
+ "AQMMVNVPLFAGNPQLQEQLRLQLPVFLQQCQNPESLSILTNPRAMQALLQIQQGLQTLQ"
162
+ "TEAPGLVPSLGSFGISRTPAPSAGSNAGSTPEAPTSSPATPATSSPTGASSAQQQLMQQM"
163
+ "IQLLAGSGNSQVQTPEVRFQQQLEQLNSCGFINREANLQALIATGGDINAAIERLLGSQL"
164
+ "S"
165
+ ),
166
+ },
167
+ {
168
+ "id": 'ubqln_mut20',
169
+ "name": 'UBQLN_mut20',
170
+ "seq": (
171
+ "MAEPSGAETNPNIRVTVKTPKDKEEIVICDIASVKEFKEEISRRFKAQQDQLVLIFAGKD"
172
+ "LKDGDTLNQHGIKDGETYHLVIKTPQKAQDPAAATASSPSTPDPASAPSTTPASPATPAQ"
173
+ "PSTSGSASSDAGSGSRRSSGGGPSPGAGEGSPSATASILSGFGGILGLGSLGLGSANFME"
174
+ "LQQQMQRQLMSNPEMLSQICENPLVQDMMSNPDLMRHCCMANPQYQQLCERNPEISHMLN"
175
+ "NPELMRQTMELARNPAMMQEMMRNQDRALSNCESIPGGYNALRRMYTDIQEPMFSAAREQ"
176
+ "FGNNPDSSLAGNSDSSSSQPLRTENREPLPNPWSPSPPTSQAPGSGGEGTGGSGTSQVHP"
177
+ "TVSNPFGINAASLGSGMCNSPEMQALLQQISENCQLMQNVISAPKMRSMMQTLAQNPDFA"
178
+ "AQMMVNVPLFAGNPQLQEQLRLQLPVFLQQCQNPESLSIDTNPRAMQALLQIQQGLQTLQ"
179
+ "TEAPGLVPSLGSFGISRTPAPSAGSNAGSTPEAPTSSPATPATSSPTGASSAQQQLKQQM"
180
+ "IQLLAGSGNSQVQTPEVRFQQQLEQLNSCGFINREANLQALIATGGDINAAIERLLGSQL"
181
+ "S"
182
+ ),
183
+ },
184
+ {
185
+ "id": 'ubqln_mut30',
186
+ "name": 'UBQLN_mut30',
187
+ "seq": (
188
+ "MAEPSGAETNPNIRVTVKTPKDKEEIVICDIASVKEFKEEISRRFKAQQDQLVLIFAGKD"
189
+ "DKDGDTDNQHGIKDGETYHLVIKTPQKAQDPAAATASSPSTPDPASAPSTTPASPATPAQ"
190
+ "PSTSGSASSDAGSGSRRSSGGGPSPGAGEGSPSATASILSGFGGILGLGSLGLGSANFME"
191
+ "LQQQMQRQLMSNPEMLSQPCENLLVQDMMSNPDLMRHCCMANPQYQQLCERNPEISHMLN"
192
+ "NPELMRQPMELARNPAMMQEMMRNQDRALSNCESIPGGYNADRRDYTDIQEPMFSAAREQ"
193
+ "FGNNPDSSLAGNSDSSSSQPLRTENREPLPNPWSPSPPTSQAPGSGGEGTGGSGTSQVHP"
194
+ "TVSNPFGINAASLGSGMCNSPEMQALLQQISENCQLMQNVISAPKMRSMMQTLAQNPDFA"
195
+ "AQMMVNVPLFAGNPQLQEQLRLQLPVFLQQCQNPESLSIDTNPRAMQALLQIQQGLQTLQ"
196
+ "TEAPGLVPSLGSFGISRTPAPSAGSNAGSTPEAPTSSPATPATSSPTGASSAQQQLKQQM"
197
+ "IQLLAGSGNSQVQTPEVRFQQQLEQLNSCGPINREANLQALPATGGDINADIERLLGSQL"
198
+ "S"
199
+ ),
200
+ },
201
+ ]
202
+
203
+
204
+ def feature_path(example_id: str) -> Path:
205
+ return ASSETS_DIR / f"{example_id}.h5"
206
+
207
+
208
+ def find_example_by_seq(seq: str):
209
+ """Return the EXAMPLES entry whose sequence matches `seq`, or None."""
210
+ norm = seq.strip().upper()
211
+ for example in EXAMPLES:
212
+ if example["seq"].strip().upper() == norm:
213
+ return example
214
+ return None
preprocess/add_example.py ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Add example sequence(s) to examples.py from a raw-data Excel file.
2
+
3
+ The training-data Excel files (e.g. LLPSense/data/raw/nonPSPex/ASCL1.xlsx)
4
+ carry a "Protein name" column and a sequence column (spelled either
5
+ "Sequence" or "Seqence" across files) alongside condition columns this demo
6
+ doesn't need. Some files hold one protein across several condition rows
7
+ (e.g. ASCL1.xlsx); others hold several distinct proteins, one per row, each
8
+ with its own name/sequence (e.g. UBQLN4_mut.xlsx's UBQLN_mut10/20/30). This
9
+ script groups rows by protein name and appends one EXAMPLES entry per
10
+ distinct (name, sequence) pair found in the file.
11
+
12
+ Requires pandas + openpyxl (not part of requirements.txt β€” those are only
13
+ needed to run this offline script, not the deployed app):
14
+ pip install pandas openpyxl
15
+
16
+ Usage:
17
+ python preprocess/add_example.py /path/to/ASCL1.xlsx
18
+ python preprocess/add_example.py /path/to/ASCL1.xlsx --id ascl1 --name "ASCL1"
19
+ python preprocess/add_example.py /path/to/UBQLN4_mut.xlsx # adds all 3 mutants
20
+
21
+ --id/--name only apply when the file contains a single protein.
22
+
23
+ After adding, run preprocess/extract_example_feat.py to cache the T5 feature(s).
24
+ """
25
+
26
+ import argparse
27
+ import re
28
+ import sys
29
+ from pathlib import Path
30
+
31
+ import pandas as pd
32
+
33
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
34
+ from examples import EXAMPLES # noqa: E402
35
+
36
+ EXAMPLES_FILE = Path(__file__).resolve().parent.parent / "examples.py"
37
+ VALID_AA = set("ACDEFGHIKLMNPQRSTVWY")
38
+ ALLOW_AA = VALID_AA | set("XBJOUZ")
39
+
40
+
41
+ def _find_col(df, *keywords):
42
+ for col in df.columns:
43
+ low = col.lower()
44
+ if all(keyword in low for keyword in keywords):
45
+ return col
46
+ return None
47
+
48
+
49
+ def slugify(name: str) -> str:
50
+ slug = re.sub(r"[^a-z0-9]+", "_", name.strip().lower()).strip("_")
51
+ if not slug:
52
+ raise ValueError(f"Could not derive an id from name: {name!r}")
53
+ return slug
54
+
55
+
56
+ def read_proteins(xlsx_path: Path) -> list[tuple[str, str]]:
57
+ """Return one (name, sequence) pair per distinct protein in the file.
58
+
59
+ Rows are grouped by protein name β€” a file may repeat the same protein
60
+ across several condition rows (ASCL1.xlsx: 1 protein, several conditions)
61
+ or list several distinct proteins one per row (UBQLN4_mut.xlsx: 3
62
+ mutants). Either shape yields one entry per unique protein name here.
63
+ """
64
+ df = pd.read_excel(xlsx_path, engine="openpyxl")
65
+
66
+ name_col = _find_col(df, "protein", "name")
67
+ seq_col = _find_col(df, "seq")
68
+ if name_col is None or seq_col is None:
69
+ raise ValueError(
70
+ f"{xlsx_path} is missing a protein-name or sequence column "
71
+ f"(found columns: {df.columns.tolist()})"
72
+ )
73
+
74
+ df = df[[name_col, seq_col]].dropna()
75
+ if df.empty:
76
+ raise ValueError(f"{xlsx_path} has no rows with both a name and a sequence")
77
+
78
+ proteins = []
79
+ for name, group in df.groupby(name_col, sort=False):
80
+ seqs = group[seq_col].astype(str).str.strip().str.upper().unique()
81
+ if len(seqs) != 1:
82
+ raise ValueError(
83
+ f"Protein '{name}' in {xlsx_path} has {len(seqs)} distinct "
84
+ f"sequences across its rows β€” expected exactly one"
85
+ )
86
+ proteins.append((str(name).strip(), seqs[0]))
87
+ return proteins
88
+
89
+
90
+ def format_seq_literal(seq: str, width: int = 60) -> str:
91
+ lines = [seq[i:i + width] for i in range(0, len(seq), width)]
92
+ body = "\n".join(f' "{line}"' for line in lines)
93
+ return f"(\n{body}\n )"
94
+
95
+
96
+ def append_example(entry: dict) -> None:
97
+ text = EXAMPLES_FILE.read_text()
98
+ match = re.search(r"(EXAMPLES\s*=\s*\[)(.*?)(\n\])", text, flags=re.DOTALL)
99
+ if not match:
100
+ raise RuntimeError(f"Could not find EXAMPLES list in {EXAMPLES_FILE}")
101
+
102
+ # The existing last entry may or may not have a trailing comma (both
103
+ # forms occur in practice) β€” normalize to one before appending.
104
+ body = match.group(2).rstrip()
105
+ if body and not body.endswith(","):
106
+ body += ","
107
+
108
+ new_block = (
109
+ "\n {\n"
110
+ f' "id": {entry["id"]!r},\n'
111
+ f' "name": {entry["name"]!r},\n'
112
+ f' "seq": {format_seq_literal(entry["seq"])},\n'
113
+ " },"
114
+ )
115
+ updated = text[:match.start(2)] + body + new_block + text[match.end(2):]
116
+ EXAMPLES_FILE.write_text(updated)
117
+
118
+
119
+ def main():
120
+ parser = argparse.ArgumentParser(description=__doc__)
121
+ parser.add_argument("xlsx_path", type=Path)
122
+ parser.add_argument(
123
+ "--id", type=str, default=None,
124
+ help="override the generated example id (only valid for single-protein files)",
125
+ )
126
+ parser.add_argument(
127
+ "--name", type=str, default=None,
128
+ help="override the display name (only valid for single-protein files)",
129
+ )
130
+ args = parser.parse_args()
131
+
132
+ proteins = read_proteins(args.xlsx_path)
133
+
134
+ if (args.id or args.name) and len(proteins) > 1:
135
+ parser.error(
136
+ f"{args.xlsx_path} contains {len(proteins)} distinct proteins β€” "
137
+ "--id/--name can only be used with a single-protein file"
138
+ )
139
+
140
+ # Track ids seen so far in this run too, so two proteins in the same
141
+ # file that happen to slugify to the same id are still caught.
142
+ known_ids = {example["id"] for example in EXAMPLES}
143
+ added = 0
144
+
145
+ for name, seq in proteins:
146
+ example_id = args.id or slugify(name)
147
+ display_name = args.name or name
148
+
149
+ invalid = set(seq) - ALLOW_AA
150
+ if invalid:
151
+ print(
152
+ f"Warning: '{name}' sequence contains unexpected characters: "
153
+ f"{''.join(sorted(invalid))}",
154
+ file=sys.stderr,
155
+ )
156
+
157
+ if example_id in known_ids:
158
+ print(f"'{example_id}' already exists in EXAMPLES β€” skipping.")
159
+ continue
160
+
161
+ append_example({"id": example_id, "name": display_name, "seq": seq})
162
+ known_ids.add(example_id)
163
+ added += 1
164
+ print(f"Added '{example_id}' ({display_name}, {len(seq)} aa) to {EXAMPLES_FILE}")
165
+
166
+ if added:
167
+ print("Run `python preprocess/extract_example_feat.py` to cache the T5 feature(s).")
168
+ else:
169
+ print("No new examples added.")
170
+
171
+
172
+ if __name__ == "__main__":
173
+ main()
preprocess/extract_example_feat.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Pre-extract ProtT5-XL features for the built-in example sequences.
2
+
3
+ Mirrors the offline pre-computation step in the training pipeline
4
+ (LLPSense/preprocess/extract_feat.py): run this once so the Gradio app never
5
+ has to hit the T5 model / GPU for a sequence it already knows about (see
6
+ `examples.find_example_by_seq` and `cb_extract` in app.py). This matters
7
+ most on the ZeroGPU-backed Space, where every GPU call consumes quota.
8
+
9
+ Usage:
10
+ python preprocess/extract_example_feat.py
11
+ """
12
+
13
+ import sys
14
+ from pathlib import Path
15
+
16
+ from huggingface_hub import snapshot_download
17
+ from tqdm import tqdm
18
+
19
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
20
+
21
+ from examples import EXAMPLES, feature_path, ASSETS_DIR # noqa: E402
22
+ from t5_utils import T5_REPO_ID, extract_t5_feature, write_feature_h5 # noqa: E402
23
+
24
+
25
+ def main():
26
+ snapshot_download(T5_REPO_ID)
27
+ ASSETS_DIR.mkdir(parents=True, exist_ok=True)
28
+
29
+ for example in tqdm(EXAMPLES, desc="Extracting example T5 features"):
30
+ out_path = feature_path(example["id"])
31
+ if out_path.exists():
32
+ continue
33
+ feat = extract_t5_feature(example["seq"])
34
+ write_feature_h5(out_path, feat)
35
+
36
+
37
+ if __name__ == "__main__":
38
+ main()
requirements.txt CHANGED
@@ -8,6 +8,7 @@ sentencepiece # ProtT5 tokenizer backend
8
  xgboost==2.0.3
9
  scikit-learn
10
  joblib
 
11
 
12
  # Gradio demo
13
  gradio>=4.0.0
 
8
  xgboost==2.0.3
9
  scikit-learn
10
  joblib
11
+ h5py # cached example T5 features in assets/
12
 
13
  # Gradio demo
14
  gradio>=4.0.0
t5_utils.py ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Shared ProtT5-XL loading / feature-extraction / h5 I/O helpers.
2
+
3
+ Used by both app.py (Gradio callbacks, wrapped in @spaces.GPU) and
4
+ preprocess/extract_example_feat.py (offline pre-computation of the built-in
5
+ example sequences' features into assets/). Keeping this logic in one place
6
+ means the cached features in assets/ are guaranteed to match what the app
7
+ would compute on the fly.
8
+ """
9
+
10
+ import re
11
+ from pathlib import Path
12
+
13
+ import h5py
14
+ import numpy as np
15
+ import torch
16
+
17
+ T5_REPO_ID = "Rostlab/prot_t5_xl_half_uniref50-enc"
18
+
19
+ _t5_tokenizer = None
20
+ _t5_model = None
21
+
22
+
23
+ def get_device() -> str:
24
+ return "cuda" if torch.cuda.is_available() else "cpu"
25
+
26
+
27
+ def load_t5():
28
+ global _t5_tokenizer, _t5_model
29
+ if _t5_model is None:
30
+ from transformers import AutoTokenizer, T5EncoderModel
31
+ import transformers.utils.import_utils as _hf_utils
32
+ import transformers.modeling_utils as _modeling_utils
33
+
34
+ # Rostlab/prot_t5_xl_half_uniref50-enc is only available as .bin (no
35
+ # safetensors). transformers 5.x blocks torch.load on torch < 2.6 due
36
+ # to CVE-2025-32434. We bypass that gate for this specific trusted
37
+ # checkpoint from the official HuggingFace Hub. The check lives in
38
+ # two places β€” import_utils AND the locally-imported name in
39
+ # modeling_utils β€” so both must be patched.
40
+ _noop = lambda: None
41
+ _orig_hf = _hf_utils.check_torch_load_is_safe
42
+ _orig_mdl = _modeling_utils.check_torch_load_is_safe
43
+
44
+ _hf_utils.check_torch_load_is_safe = _noop
45
+ _modeling_utils.check_torch_load_is_safe = _noop
46
+
47
+ try:
48
+ repo_id = T5_REPO_ID
49
+ dev = get_device()
50
+ dtype = torch.float16 if dev == "cuda" else torch.float32
51
+
52
+ _t5_tokenizer = AutoTokenizer.from_pretrained(
53
+ repo_id, do_lower_case=False, local_files_only=True
54
+ )
55
+ _t5_model = (
56
+ T5EncoderModel.from_pretrained(
57
+ repo_id, torch_dtype=dtype, local_files_only=True
58
+ )
59
+ .to(dev)
60
+ .eval()
61
+ )
62
+ _t5_model.requires_grad_(False)
63
+ finally:
64
+ _hf_utils.check_torch_load_is_safe = _orig_hf
65
+ _modeling_utils.check_torch_load_is_safe = _orig_mdl
66
+
67
+ return _t5_tokenizer, _t5_model
68
+
69
+
70
+ def extract_t5_feature(sequence: str) -> np.ndarray:
71
+ """
72
+ Return mean-pooled ProtT5-XL embedding for a single sequence.
73
+ Output shape: (1024,) β€” matches the feature dimension LLPSense was trained on.
74
+ """
75
+ tok, mdl = load_t5()
76
+ dev = get_device()
77
+
78
+ # Replace ambiguous residues with X (same as original pipeline)
79
+ clean = re.sub(r"[UZOB]", "X", sequence.strip().upper())
80
+ spaced = " ".join(list(clean))
81
+
82
+ enc = tok([spaced], add_special_tokens=True, padding="longest", return_tensors="pt")
83
+ input_ids = enc["input_ids"].to(dev)
84
+ attention_mask = enc["attention_mask"].to(dev)
85
+
86
+ with torch.no_grad():
87
+ hidden = mdl(input_ids=input_ids, attention_mask=attention_mask).last_hidden_state
88
+ # Mean-pool over sequence positions (ignoring padding tokens)
89
+ feat = (hidden * attention_mask[..., None]).sum(dim=1) / \
90
+ attention_mask.sum(dim=1, keepdim=True)
91
+
92
+ return feat[0].float().cpu().numpy() # (1024,)
93
+
94
+
95
+ # ── h5 cache I/O ──────────────────────────────────────────────────────────────
96
+ FEATURE_TAG = "protein_feat"
97
+
98
+
99
+ def write_feature_h5(filepath, feat: np.ndarray, tag: str = FEATURE_TAG) -> None:
100
+ filepath = Path(filepath)
101
+ filepath.parent.mkdir(parents=True, exist_ok=True)
102
+ with h5py.File(filepath, "w") as f:
103
+ f.create_dataset(tag, data=feat, dtype="f", compression="gzip")
104
+
105
+
106
+ def read_feature_h5(filepath, tag: str = FEATURE_TAG) -> np.ndarray:
107
+ with h5py.File(filepath, "r") as f:
108
+ return f[tag][:]