andreamena commited on
Commit
8e848bf
·
verified ·
1 Parent(s): ee5b338

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +120 -70
app.py CHANGED
@@ -12,7 +12,7 @@ import tf_keras as keras
12
  import numpy as np
13
  import pandas as pd
14
  import torch
15
- from transformers import AutoTokenizer, EsmModel # CHANGED: Switched to ESM
16
  from huggingface_hub import snapshot_download
17
  from model import PredictModel
18
 
@@ -24,10 +24,8 @@ from encoding import one_hot_padding, position_encoding, blosum62_padding, zscal
24
  SEEDS = [2023, 2024, 2025]
25
  TASK = 'PDC'
26
 
27
- # --- 3. LOAD ESM-2 (The Correct "Brain" for your weights) ---
28
  print("📦 Downloading ESM-2 Model safely...")
29
-
30
- # We use the 150M parameter model which has dim=640 (Matches your 1664 input size)
31
  REPO_ID = "facebook/esm2_t30_150M_UR50D"
32
 
33
  model_path = snapshot_download(
@@ -38,43 +36,21 @@ model_path = snapshot_download(
38
 
39
  tokenizer = AutoTokenizer.from_pretrained(model_path)
40
  esm_model = EsmModel.from_pretrained(model_path)
41
-
42
- # Move to CPU
43
  esm_model.eval()
44
 
45
- # --- 4. HELPER FUNCTIONS ---
46
 
47
  def get_esm_embedding(sequence):
48
- """
49
- Generates the t1 tensor dynamically using ESM-2 (640 dim).
50
- """
51
- # ESM does NOT need spaces, but handles standard strings
52
- # Clean sequence
53
- clean_seq = re.sub(r"[UZOB]", "X", sequence)
54
-
55
- # Tokenize
56
  encoded_input = tokenizer(clean_seq, return_tensors='pt')
57
-
58
- # Run Model
59
  with torch.no_grad():
60
  output = esm_model(**encoded_input)
61
-
62
- # Get the Last Hidden State [1, Seq_Len, 640]
63
- embedding = output.last_hidden_state
64
-
65
- # Remove [CLS] and [SEP] (first and last tokens)
66
- embedding = embedding[:, 1:-1, :]
67
-
68
- # Mean Pooling: [1, Seq_Len, 640] -> [1, 640]
69
  pooled_embedding = torch.mean(embedding, dim=1)
70
-
71
- # Convert to TensorFlow
72
- tf_embedding = tf.convert_to_tensor(pooled_embedding.numpy(), dtype=tf.float32)
73
-
74
- return tf_embedding
75
 
76
  def get_t2_features(sequence, padding=71):
77
- selected_sequences = [sequence]
78
  one_hot = one_hot_padding(selected_sequences, padding)
79
  pos = position_encoding(padding)
80
  blosum = blosum62_padding(selected_sequences, padding)
@@ -84,7 +60,7 @@ def get_t2_features(sequence, padding=71):
84
  return tf.constant(combined, dtype=tf.float32)
85
 
86
  def preprocess_input(payload_smiles, linker_smiles, peptide_seq):
87
- # 1. Create dummy input
88
  df = pd.DataFrame({
89
  'Payload SMILES': [payload_smiles],
90
  'Linker SMILES': [linker_smiles],
@@ -93,7 +69,7 @@ def preprocess_input(payload_smiles, linker_smiles, peptide_seq):
93
  })
94
  df.to_excel('temp_input.xlsx', index=False)
95
 
96
- # 2. Get Graph Features
97
  dataset_gen = Graph_Classification_Dataset(
98
  'temp_input.xlsx',
99
  smiles_field1='Payload SMILES',
@@ -106,7 +82,7 @@ def preprocess_input(payload_smiles, linker_smiles, peptide_seq):
106
  addH=True
107
  ).get_data()
108
 
109
- # Robust Batch Retrieval (Check Train/Test/Val)
110
  train_ds, test_ds, val_ds = dataset_gen
111
  target_batch = None
112
  for ds in [train_ds, test_ds, val_ds]:
@@ -115,49 +91,42 @@ def preprocess_input(payload_smiles, linker_smiles, peptide_seq):
115
  if target_batch is not None: break
116
  except: continue
117
 
118
- if target_batch is None:
119
- return None, None, None, None, None, None, None, None
120
 
121
  x1, adjoin1, _, x2, adjoin2, _ = target_batch
122
 
123
- # Masks
124
  seq1 = tf.cast(tf.math.equal(x1, 0), tf.float32)
125
  mask1 = seq1[:, tf.newaxis, tf.newaxis, :]
126
  seq2 = tf.cast(tf.math.equal(x2, 0), tf.float32)
127
  mask2 = seq2[:, tf.newaxis, tf.newaxis, :]
128
 
129
- # 3. Get Features
130
  t2 = get_t2_features(peptide_seq)
131
- t1 = get_esm_embedding(peptide_seq) # Use New ESM Function
132
 
133
  return x1, mask1, adjoin1, x2, mask2, adjoin2, t1, t2
134
 
135
- def predict_ensemble(payload, linker, peptide):
136
- if not payload or not linker or not peptide:
137
- return "⚠️ Please fill in all fields."
138
-
139
  try:
140
- x1, mask1, adj1, x2, mask2, adj2, t1, t2 = preprocess_input(payload, linker, peptide)
 
141
 
142
- if x1 is None:
143
- return "Error: Could not process chemical structures. Check SMILES validity."
144
-
145
  total_prob = 0.0
146
  models_ran = 0
147
 
148
  for seed in SEEDS:
149
  weight_file = f'PDC_{seed}.h5'
150
- if not os.path.exists(weight_file):
151
- continue
152
 
153
  keras.backend.clear_session()
154
- # Re-init model
155
  model = PredictModel(num_layers=6, d_model=256, dff=512, num_heads=8, vocab_size=18, a=1, dense_dropout=0.4)
156
 
157
- # Dummy call
158
  _ = model(x1=x1, mask1=mask1, training=False, adjoin_matrix1=adj1,
159
  x2=x2, mask2=mask2, adjoin_matrix2=adj2, t1=t1, t2=t2)
160
-
161
  model.load_weights(weight_file)
162
 
163
  logits = model(x1=x1, mask1=mask1, training=False, adjoin_matrix1=adj1,
@@ -168,31 +137,112 @@ def predict_ensemble(payload, linker, peptide):
168
  models_ran += 1
169
  del model
170
 
171
- if models_ran == 0:
172
- return "Error: No model weights found on server."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
 
174
- avg_score = total_prob / models_ran
175
- label = "Active PDC ✅" if avg_score > 0.5 else "Inactive ❌"
176
- return f"{label}\nConfidence: {avg_score:.2%}"
 
 
 
 
 
 
177
 
178
- except Exception as e:
179
- return f"Error: {str(e)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
 
181
- # --- 5. GRADIO INTERFACE ---
182
  with gr.Blocks(title="PDCNet Ensemble Demo") as demo:
183
  gr.Markdown("# 💊 PDCNet Ensemble Predictor")
184
- gr.Markdown("Predict the activity of Peptide-Drug Conjugates. Supports **Any Peptide Sequence** via live ESM-2 embedding.")
185
 
186
- with gr.Row():
187
- with gr.Column():
188
- in_payload = gr.Textbox(label="Payload SMILES", placeholder="e.g. COc1cc2c...", lines=2)
189
- in_linker = gr.Textbox(label="Linker SMILES", placeholder="e.g. C(=O)CCCC(=O)O", lines=2)
190
- in_peptide = gr.Textbox(label="Peptide Sequence", placeholder="e.g. ACDEFGH", lines=2)
191
- btn_predict = gr.Button("Predict Activity", variant="primary")
 
 
 
 
 
 
192
 
193
- with gr.Column():
194
- out_result = gr.Textbox(label="Prediction Result", lines=4)
 
 
 
 
195
 
196
- btn_predict.click(predict_ensemble, inputs=[in_payload, in_linker, in_peptide], outputs=out_result)
197
 
198
  demo.launch()
 
12
  import numpy as np
13
  import pandas as pd
14
  import torch
15
+ from transformers import AutoTokenizer, EsmModel
16
  from huggingface_hub import snapshot_download
17
  from model import PredictModel
18
 
 
24
  SEEDS = [2023, 2024, 2025]
25
  TASK = 'PDC'
26
 
27
+ # --- 3. LOAD ESM-2 ---
28
  print("📦 Downloading ESM-2 Model safely...")
 
 
29
  REPO_ID = "facebook/esm2_t30_150M_UR50D"
30
 
31
  model_path = snapshot_download(
 
36
 
37
  tokenizer = AutoTokenizer.from_pretrained(model_path)
38
  esm_model = EsmModel.from_pretrained(model_path)
 
 
39
  esm_model.eval()
40
 
41
+ # --- 4. CORE HELPER FUNCTIONS ---
42
 
43
  def get_esm_embedding(sequence):
44
+ clean_seq = re.sub(r"[UZOB]", "X", str(sequence)) # Ensure string
 
 
 
 
 
 
 
45
  encoded_input = tokenizer(clean_seq, return_tensors='pt')
 
 
46
  with torch.no_grad():
47
  output = esm_model(**encoded_input)
48
+ embedding = output.last_hidden_state[:, 1:-1, :]
 
 
 
 
 
 
 
49
  pooled_embedding = torch.mean(embedding, dim=1)
50
+ return tf.convert_to_tensor(pooled_embedding.numpy(), dtype=tf.float32)
 
 
 
 
51
 
52
  def get_t2_features(sequence, padding=71):
53
+ selected_sequences = [str(sequence)]
54
  one_hot = one_hot_padding(selected_sequences, padding)
55
  pos = position_encoding(padding)
56
  blosum = blosum62_padding(selected_sequences, padding)
 
60
  return tf.constant(combined, dtype=tf.float32)
61
 
62
  def preprocess_input(payload_smiles, linker_smiles, peptide_seq):
63
+ # Create temp dataframe for the Graph Loader
64
  df = pd.DataFrame({
65
  'Payload SMILES': [payload_smiles],
66
  'Linker SMILES': [linker_smiles],
 
69
  })
70
  df.to_excel('temp_input.xlsx', index=False)
71
 
72
+ # Use existing loader
73
  dataset_gen = Graph_Classification_Dataset(
74
  'temp_input.xlsx',
75
  smiles_field1='Payload SMILES',
 
82
  addH=True
83
  ).get_data()
84
 
85
+ # Robust Batch Retrieval
86
  train_ds, test_ds, val_ds = dataset_gen
87
  target_batch = None
88
  for ds in [train_ds, test_ds, val_ds]:
 
91
  if target_batch is not None: break
92
  except: continue
93
 
94
+ if target_batch is None: return None
 
95
 
96
  x1, adjoin1, _, x2, adjoin2, _ = target_batch
97
 
98
+ # Create Masks
99
  seq1 = tf.cast(tf.math.equal(x1, 0), tf.float32)
100
  mask1 = seq1[:, tf.newaxis, tf.newaxis, :]
101
  seq2 = tf.cast(tf.math.equal(x2, 0), tf.float32)
102
  mask2 = seq2[:, tf.newaxis, tf.newaxis, :]
103
 
104
+ # Get Features
105
  t2 = get_t2_features(peptide_seq)
106
+ t1 = get_esm_embedding(peptide_seq)
107
 
108
  return x1, mask1, adjoin1, x2, mask2, adjoin2, t1, t2
109
 
110
+ def get_raw_score(payload, linker, peptide):
111
+ """Calculates the raw probability (0.0 to 1.0)"""
 
 
112
  try:
113
+ data = preprocess_input(payload, linker, peptide)
114
+ if data is None: return None
115
 
116
+ x1, mask1, adj1, x2, mask2, adj2, t1, t2 = data
 
 
117
  total_prob = 0.0
118
  models_ran = 0
119
 
120
  for seed in SEEDS:
121
  weight_file = f'PDC_{seed}.h5'
122
+ if not os.path.exists(weight_file): continue
 
123
 
124
  keras.backend.clear_session()
 
125
  model = PredictModel(num_layers=6, d_model=256, dff=512, num_heads=8, vocab_size=18, a=1, dense_dropout=0.4)
126
 
127
+ # Init and Load
128
  _ = model(x1=x1, mask1=mask1, training=False, adjoin_matrix1=adj1,
129
  x2=x2, mask2=mask2, adjoin_matrix2=adj2, t1=t1, t2=t2)
 
130
  model.load_weights(weight_file)
131
 
132
  logits = model(x1=x1, mask1=mask1, training=False, adjoin_matrix1=adj1,
 
137
  models_ran += 1
138
  del model
139
 
140
+ if models_ran == 0: return None
141
+ return total_prob / models_ran
142
+
143
+ except Exception:
144
+ return None
145
+
146
+ # --- 5. UI HANDLERS ---
147
+
148
+ def predict_single(payload, linker, peptide):
149
+ if not payload or not linker or not peptide:
150
+ return "⚠️ Please fill in all fields."
151
+
152
+ score = get_raw_score(payload, linker, peptide)
153
+
154
+ if score is None:
155
+ return "Error processing input. Check SMILES validity."
156
+
157
+ label = "Active PDC ✅" if score > 0.5 else "Inactive ❌"
158
+ return f"{label}\nConfidence: {score:.2%}"
159
 
160
+ def predict_batch(file_obj, progress=gr.Progress()):
161
+ if file_obj is None:
162
+ return None
163
+
164
+ # Read File
165
+ if file_obj.name.endswith('.csv'):
166
+ df = pd.read_csv(file_obj.name)
167
+ else:
168
+ df = pd.read_excel(file_obj.name)
169
 
170
+ # --- SMART COLUMN DETECTION ---
171
+ # We map the "Keywords" we look for -> to the "Standard Names" our code needs
172
+ keyword_map = {
173
+ 'payload': 'Payload SMILES',
174
+ 'linker': 'Linker SMILES',
175
+ 'peptide': 'Peptide Sequence'
176
+ }
177
+
178
+ col_map = {}
179
+ missing = []
180
+
181
+ for keyword, standard_name in keyword_map.items():
182
+ # Look for a column that contains the keyword (e.g. "payload") case-insensitive
183
+ match = next((c for c in df.columns if keyword in c.lower()), None)
184
+
185
+ if match:
186
+ col_map[standard_name] = match # Map found column to standard name
187
+ else:
188
+ missing.append(keyword)
189
+
190
+ if missing:
191
+ return f"Error: Could not find columns containing keywords: {missing}. Found columns: {list(df.columns)}"
192
+
193
+ results = []
194
+
195
+ # Iterate with Progress Bar
196
+ for index, row in progress.tqdm(df.iterrows(), total=len(df)):
197
+ # Use the mapped column names to get data
198
+ p_smiles = row[col_map['Payload SMILES']]
199
+ l_smiles = row[col_map['Linker SMILES']]
200
+ peptide = row[col_map['Peptide Sequence']]
201
+
202
+ score = get_raw_score(p_smiles, l_smiles, peptide)
203
+
204
+ if score is None:
205
+ pred = "Error"
206
+ conf = 0.0
207
+ else:
208
+ pred = "Active" if score > 0.5 else "Inactive"
209
+ conf = score
210
+
211
+ results.append([p_smiles, l_smiles, peptide, pred, f"{conf:.4f}"])
212
+
213
+ # Create Result DataFrame
214
+ res_df = pd.DataFrame(results, columns=['Payload', 'Linker', 'Peptide', 'Prediction', 'Score'])
215
+
216
+ output_file = "predictions.csv"
217
+ res_df.to_csv(output_file, index=False)
218
+
219
+ return output_file
220
 
221
+ # --- 6. GRADIO INTERFACE ---
222
  with gr.Blocks(title="PDCNet Ensemble Demo") as demo:
223
  gr.Markdown("# 💊 PDCNet Ensemble Predictor")
224
+ gr.Markdown("Predict Peptide-Drug Conjugate activity. Upload a file or type manually.")
225
 
226
+ with gr.Tabs():
227
+ # TAB 1: SINGLE INPUT
228
+ with gr.TabItem("Single Prediction"):
229
+ with gr.Row():
230
+ with gr.Column():
231
+ in_payload = gr.Textbox(label="Payload SMILES", placeholder="e.g. COc1cc2c...", lines=2)
232
+ in_linker = gr.Textbox(label="Linker SMILES", placeholder="e.g. C(=O)CCCC(=O)O", lines=2)
233
+ in_peptide = gr.Textbox(label="Peptide Sequence", placeholder="e.g. ACDEFGH", lines=2)
234
+ btn_predict = gr.Button("Predict Activity", variant="primary")
235
+ with gr.Column():
236
+ out_result = gr.Textbox(label="Prediction Result", lines=4)
237
+ btn_predict.click(predict_single, inputs=[in_payload, in_linker, in_peptide], outputs=out_result)
238
 
239
+ # TAB 2: BATCH INPUT
240
+ with gr.TabItem("Batch Prediction (File)"):
241
+ gr.Markdown("Upload a CSV or Excel file. Must have columns for Payload, Linker, and Peptide.")
242
+ file_input = gr.File(label="Upload Dataset (.csv or .xlsx)")
243
+ btn_batch = gr.Button("Run Batch Prediction", variant="primary")
244
+ file_output = gr.File(label="Download Results")
245
 
246
+ btn_batch.click(predict_batch, inputs=file_input, outputs=file_output)
247
 
248
  demo.launch()