AI4deeperScience commited on
Commit
e907de7
·
verified ·
1 Parent(s): 959aa07

Deploy BioInteract Gradio demo

Browse files
README.md CHANGED
@@ -1,15 +1,15 @@
1
  ---
2
  title: BioInteract Drug-Target Interaction
3
- emoji: 🧬
4
  colorFrom: blue
5
  colorTo: green
6
  sdk: gradio
7
- sdk_version: 5.20.0
8
  python_version: "3.11"
9
  app_file: app.py
10
  pinned: false
11
  license: mit
12
- short_description: Binary high-affinity DTI with model-native attribution
13
  ---
14
 
15
  # BioInteract
 
1
  ---
2
  title: BioInteract Drug-Target Interaction
3
+ emoji: 🧬
4
  colorFrom: blue
5
  colorTo: green
6
  sdk: gradio
7
+ sdk_version: 5.20.0
8
  python_version: "3.11"
9
  app_file: app.py
10
  pinned: false
11
  license: mit
12
+ short_description: Binary high-affinity DTI with model-native attribution
13
  ---
14
 
15
  # BioInteract
app.py CHANGED
@@ -10,6 +10,7 @@ import warnings
10
  from pathlib import Path
11
 
12
  import gradio as gr
 
13
  import matplotlib
14
  matplotlib.use("Agg")
15
  import matplotlib as mpl
@@ -17,6 +18,7 @@ import matplotlib.pyplot as plt
17
  import numpy as np
18
  import seaborn as sns
19
  import torch
 
20
  import yaml
21
  from PIL import Image
22
 
@@ -198,14 +200,112 @@ def _plot_top_residues(
198
  return Image.open(buffer).copy()
199
 
200
 
201
- _EXAMPLE_SMILES = "Cc1ccc(NC(=O)c2ccc(CN3CCN(C)CC3)cc2)cc1Nc1nccc(-c2cccnc2)n1"
 
 
 
202
  _EXAMPLE_SEQUENCE = (
203
- "MGPSENDPNLFVALYDFVASGDNTLSITKGEKLRVLGYNHNGEWCEAQTKNGQGWVPSNYITPVNSLEKHSWYHGPVSRNAAE"
204
- "YLLSSGINGSFLVRESESSPGQRSISLRYEGRVYHYRINTASDGKLYVSSESRFNTLAELVHHHSTLVQHSDSVESAYRSKLLNSG"
205
- "VYHYRINTASDGKLYVSSESRFNTLAELVHHHSTLVQ"
 
 
 
206
  )
207
 
208
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
  def run_prediction(smiles: str, sequence: str, progress=gr.Progress()):
210
  """Run binary classification and return model-native attribution views."""
211
  smiles = (smiles or "").strip()
@@ -274,6 +374,7 @@ def run_prediction(smiles: str, sequence: str, progress=gr.Progress()):
274
  f"| Drug atoms analysed | {n_real_atoms} |\n"
275
  f"| Protein residues analysed | {length} |\n"
276
  "\n_Model-native attribution is hypothesis-generating, not physical contacts._\n"
 
277
  )
278
  return "Inference complete.", result, heatmap, residue_chart
279
 
@@ -341,12 +442,15 @@ with gr.Blocks(
341
  "Provide a drug SMILES string and a protein amino-acid sequence for binary high-affinity interaction classification and model-native atom-residue attention attribution.\n\n"
342
  f"> {_DEMONSTRATION_LIMIT}\n\n"
343
  f"> {_CUSTOM_DOMAIN_NOTICE}\n\n"
344
- "> ESM-2 is initialised during application startup; no first-request model initialisation is performed."
 
 
 
345
  )
346
  smiles_box = gr.Textbox(label="Drug SMILES", lines=2)
347
  sequence_box = gr.Textbox(label="Protein amino-acid sequence (single-letter code)", lines=4, max_lines=8)
348
  with gr.Row():
349
- example_button = gr.Button("Load Aurora kinase C (Q9UQB9) example", variant="secondary", size="sm")
350
  predict_button = gr.Button("Run prediction", variant="primary", size="lg")
351
  status_box = gr.Textbox(label="Status", interactive=False, lines=1, placeholder="Awaiting input")
352
  score_markdown = gr.Markdown(min_height=100)
 
10
  from pathlib import Path
11
 
12
  import gradio as gr
13
+ from Bio import Align
14
  import matplotlib
15
  matplotlib.use("Agg")
16
  import matplotlib as mpl
 
18
  import numpy as np
19
  import seaborn as sns
20
  import torch
21
+ from rdkit import Chem
22
  import yaml
23
  from PIL import Image
24
 
 
200
  return Image.open(buffer).copy()
201
 
202
 
203
+ # Davis records D0017 (dasatinib) and T0210 (LCK, 509 residues, Kd = 0.2 nM).
204
+ # Both are taken verbatim from the released Davis input package, and LCK fits
205
+ # inside the 512-residue limit of this browser demonstration.
206
+ _EXAMPLE_SMILES = "CC1=C(C(=CC=C1)Cl)NC(=O)C2=CN=C(S2)NC3=NC(=NC(=C3)N4CCN(CC4)CCO)C"
207
  _EXAMPLE_SEQUENCE = (
208
+ "MGCGCSSHPEDDWMENIDVCENCHYPIVPLDGKGTLLIRNGSEVRDPLVTYEGSNPPASPLQDNLVIALHSYEPSHDGDLGFEK"
209
+ "GEQLRILEQSGEWWKAQSLTTGQEGFIPFNFVAKANSLEPEPWFFKNLSRKDAERQLLAPGNTHGSFLIRESESTAGSFSLSVRD"
210
+ "FDQNQGEVVKHYKIRNLDNGGFYISPRITFPGLHELVRHYTNASDGLCTRLSRPCQTQKPQKPWWEDEWEVPRETLKLVERLGAG"
211
+ "QFGEVWMGYYNGHTKVAVKSLKQGSMSPDAFLAEANLMKQLQHQRLVRLYAVVTQEPIYIITEYMENGSLVDFLKTPSGIKLTIN"
212
+ "KLLDMAAQIAEGMAFIEERNYIHRDLRAANILVSDTLSCKIADFGLARLIEDNEYTAREGAKFPIKWTAPEAINYGTFTIKSDVW"
213
+ "SFGILLTEIVTHGRIPYPGMTNPEVIQNLERGYRMVRPDNCPEELYQLMRLCWKERPEDRPTFDYLRSVLEDFFTATEGQYQPQP"
214
  )
215
 
216
 
217
+ # --- applicability domain -------------------------------------------------
218
+ _REFERENCE_PATH = ROOT / "examples" / "davis_applicability_reference.json"
219
+ try:
220
+ with open(_REFERENCE_PATH, encoding="utf-8") as handle:
221
+ _REFERENCE = json.load(handle)
222
+ _DAVIS_FP_BITS = [set(bits) for bits in _REFERENCE["ligand_fingerprint"]["on_bits"].values()]
223
+ _DAVIS_SEQUENCES = list(_REFERENCE["target_sequences"].values())
224
+ _THRESHOLDS = _REFERENCE["thresholds"]
225
+ except Exception as error: # the demonstration still runs without the bundle
226
+ print(f"[BioInteract] Applicability reference unavailable: {error}")
227
+ _REFERENCE, _DAVIS_FP_BITS, _DAVIS_SEQUENCES = None, [], []
228
+ _THRESHOLDS = {}
229
+
230
+
231
+ def _max_ligand_similarity(smiles: str) -> float | None:
232
+ """Return the maximum Tanimoto similarity to any Davis training compound."""
233
+ if not _DAVIS_FP_BITS:
234
+ return None
235
+ from rdkit.Chem import rdFingerprintGenerator as _rfg
236
+
237
+ molecule = Chem.MolFromSmiles(smiles)
238
+ if molecule is None:
239
+ return None
240
+ generator = _rfg.GetMorganGenerator(radius=2, fpSize=1024, includeChirality=False)
241
+ query = set(generator.GetFingerprint(molecule).GetOnBits())
242
+ best = 0.0
243
+ for reference in _DAVIS_FP_BITS:
244
+ union = len(query | reference)
245
+ if union:
246
+ best = max(best, len(query & reference) / union)
247
+ return best
248
+
249
+
250
+ def _max_target_identity(sequence: str) -> float | None:
251
+ """Return the maximum global sequence identity to any Davis training target."""
252
+ if not _DAVIS_SEQUENCES:
253
+ return None
254
+ aligner = Align.PairwiseAligner()
255
+ aligner.mode = "global"
256
+ aligner.match_score = 1
257
+ aligner.mismatch_score = 0
258
+ aligner.open_gap_score = -1
259
+ aligner.extend_gap_score = -0.5
260
+ best = 0.0
261
+ for reference in _DAVIS_SEQUENCES:
262
+ alignment = aligner.align(sequence, reference)[0]
263
+ matches = sum(
264
+ 1 for left, right in zip(alignment[0], alignment[1]) if left == right and left != "-"
265
+ )
266
+ if alignment.length:
267
+ best = max(best, matches / alignment.length)
268
+ return best
269
+
270
+
271
+ def _domain_verdict(score: float | None, near: float, far: float) -> str:
272
+ """Map a nearest-neighbour score onto the reported applicability label."""
273
+ if score is None:
274
+ return "not available"
275
+ if score >= near:
276
+ return "inside the Davis domain"
277
+ if score >= far:
278
+ return "borderline"
279
+ return "outside the Davis domain"
280
+
281
+
282
+ def applicability_report(smiles: str, sequence: str) -> str:
283
+ """Return the Markdown applicability-domain block shown under the result."""
284
+ if _REFERENCE is None:
285
+ return ""
286
+ ligand = _max_ligand_similarity(smiles)
287
+ target = _max_target_identity(sequence)
288
+ ligand_verdict = _domain_verdict(ligand, _THRESHOLDS["ligand_near"], _THRESHOLDS["ligand_far"])
289
+ target_verdict = _domain_verdict(target, _THRESHOLDS["target_near"], _THRESHOLDS["target_far"])
290
+ outside = "outside" in (ligand_verdict + target_verdict)
291
+ caution = (
292
+ "At least one axis falls outside the Davis training distribution. On a strict "
293
+ "external BindingDB cohort of exactly this kind, the frozen Davis checkpoint "
294
+ "reached only AUROC 0.560, so this prediction should be treated as unreliable."
295
+ if outside
296
+ else "Both axes lie at or near the Davis training distribution, which is the "
297
+ "regime in which the reported benchmark metrics were measured."
298
+ )
299
+ return (
300
+ "\n### Applicability domain relative to Davis training data\n\n"
301
+ "| Axis | Nearest Davis training neighbour | Assessment |\n"
302
+ "|------|----------------------------------|------------|\n"
303
+ f"| Ligand (max Tanimoto, Morgan r2/1024) | {ligand:.3f} | {ligand_verdict} |\n"
304
+ f"| Target (max global sequence identity) | {target:.3f} | {target_verdict} |\n"
305
+ f"\n_{caution}_\n"
306
+ )
307
+
308
+
309
  def run_prediction(smiles: str, sequence: str, progress=gr.Progress()):
310
  """Run binary classification and return model-native attribution views."""
311
  smiles = (smiles or "").strip()
 
374
  f"| Drug atoms analysed | {n_real_atoms} |\n"
375
  f"| Protein residues analysed | {length} |\n"
376
  "\n_Model-native attribution is hypothesis-generating, not physical contacts._\n"
377
+ f"{applicability_report(smiles, sequence)}"
378
  )
379
  return "Inference complete.", result, heatmap, residue_chart
380
 
 
442
  "Provide a drug SMILES string and a protein amino-acid sequence for binary high-affinity interaction classification and model-native atom-residue attention attribution.\n\n"
443
  f"> {_DEMONSTRATION_LIMIT}\n\n"
444
  f"> {_CUSTOM_DOMAIN_NOTICE}\n\n"
445
+ "> ESM-2 is initialised during application startup; no first-request model initialisation is performed.\n\n"
446
+ "> Every prediction is accompanied by an applicability-domain readout giving the "
447
+ "submitted ligand's maximum Tanimoto similarity and the submitted target's maximum "
448
+ "global sequence identity to the Davis training entities."
449
  )
450
  smiles_box = gr.Textbox(label="Drug SMILES", lines=2)
451
  sequence_box = gr.Textbox(label="Protein amino-acid sequence (single-letter code)", lines=4, max_lines=8)
452
  with gr.Row():
453
+ example_button = gr.Button("Load Davis example: dasatinib + LCK", variant="secondary", size="sm")
454
  predict_button = gr.Button("Run prediction", variant="primary", size="lg")
455
  status_box = gr.Textbox(label="Status", interactive=False, lines=1, placeholder="Awaiting input")
456
  score_markdown = gr.Markdown(min_height=100)
examples/davis_applicability_reference.json ADDED
The diff for this file is too large to render. See raw diff
 
examples/interpretability_report.json CHANGED
@@ -1,11 +1,11 @@
1
- {
2
- "global_stats": {
3
- "residue_attention_mean": 0.083,
4
- "residue_attention_std": 0.121,
5
- "residue_attention_median": 0.036,
6
- "residue_attention_top1pct": 0.642,
7
- "residue_attention_top5pct": 0.411,
8
- "n_samples": 1506,
9
- "attention_sparsity": 0.992
10
- }
11
- }
 
1
+ {
2
+ "global_stats": {
3
+ "residue_attention_mean": 0.083,
4
+ "residue_attention_std": 0.121,
5
+ "residue_attention_median": 0.036,
6
+ "residue_attention_top1pct": 0.642,
7
+ "residue_attention_top5pct": 0.411,
8
+ "n_samples": 1506,
9
+ "attention_sparsity": 0.992
10
+ }
11
+ }
requirements.txt CHANGED
@@ -1,14 +1,15 @@
1
- --extra-index-url https://download.pytorch.org/whl/cpu
2
- torch==2.4.0
3
- torch-geometric==2.6.1
4
- rdkit
5
- transformers==5.6.1
6
- numpy==1.26.4
7
- pandas==2.0.3
8
- scipy==1.13.1
9
- scikit-learn==1.2.1
10
- PyYAML==6.0.2
11
- matplotlib==3.8.4
12
- seaborn==0.12.2
13
- tqdm==4.67.3
14
- Pillow==9.4.0
 
 
1
+ --extra-index-url https://download.pytorch.org/whl/cpu
2
+ torch==2.4.0
3
+ torch-geometric==2.6.1
4
+ rdkit
5
+ transformers==5.6.1
6
+ numpy==1.26.4
7
+ pandas==2.0.3
8
+ scipy==1.13.1
9
+ scikit-learn==1.2.1
10
+ PyYAML==6.0.2
11
+ matplotlib==3.8.4
12
+ seaborn==0.12.2
13
+ tqdm==4.67.3
14
+ Pillow==9.4.0
15
+ biopython==1.87
src/data/protein_feat.py CHANGED
@@ -22,7 +22,7 @@ UNK_AA_IDX = len(AMINO_ACIDS)
22
 
23
 
24
  # ============================================================
25
- # Residue-level physicochemical descriptors
26
  # ============================================================
27
 
28
  HYDROPHOBICITY = {
 
22
 
23
 
24
  # ============================================================
25
+ # Residue-level physicochemical descriptors
26
  # ============================================================
27
 
28
  HYDROPHOBICITY = {
src/models/interaction.py CHANGED
@@ -1,9 +1,9 @@
1
- """Cross-attention module for model-native atom--residue attribution.
2
-
3
- The module returns an attention matrix ``M`` over drug atoms and protein
4
- residues. Its values are learned model weights that help inspect a prediction;
5
- they are not molecular contacts, a binding pocket, or a structural mechanism.
6
- """
7
  import torch
8
  import torch.nn as nn
9
  import torch.nn.functional as F
@@ -16,7 +16,7 @@ class CrossAttentionInteraction(nn.Module):
16
 
17
  Returns:
18
  - Fused representation for prediction
19
- - Attention weights for model-native atom--residue attribution
20
  """
21
 
22
  def __init__(self,
 
1
+ """Cross-attention module for model-native atom--residue attribution.
2
+
3
+ The module returns an attention matrix ``M`` over drug atoms and protein
4
+ residues. Its values are learned model weights that help inspect a prediction;
5
+ they are not molecular contacts, a binding pocket, or a structural mechanism.
6
+ """
7
  import torch
8
  import torch.nn as nn
9
  import torch.nn.functional as F
 
16
 
17
  Returns:
18
  - Fused representation for prediction
19
+ - Attention weights for model-native atom--residue attribution
20
  """
21
 
22
  def __init__(self,
src/models/target_encoder.py CHANGED
@@ -1,10 +1,10 @@
1
- """Protein target encoder combining ESM-2 with residue descriptors.
2
-
3
- It accepts frozen ESM-2 embeddings, four physicochemical descriptors, and an
4
- optional categorical label channel. The browser demo supplies the ``NONE``
5
- label for arbitrary user inputs; that channel is not Pfam/InterPro annotation
6
- or evidence of protein function.
7
- """
8
  import torch
9
  import torch.nn as nn
10
 
@@ -21,9 +21,9 @@ class TargetEncoder(nn.Module):
21
  Output:
22
  - Residue representations: (B, L, projection_dim)
23
 
24
- The optional label embedding is a model input channel. In the browser demo
25
- it is the shared ``NONE`` label and is not a domain, binding-site, or
26
- functional assignment.
27
  """
28
 
29
  def __init__(self,
 
1
+ """Protein target encoder combining ESM-2 with residue descriptors.
2
+
3
+ It accepts frozen ESM-2 embeddings, four physicochemical descriptors, and an
4
+ optional categorical label channel. The browser demo supplies the ``NONE``
5
+ label for arbitrary user inputs; that channel is not Pfam/InterPro annotation
6
+ or evidence of protein function.
7
+ """
8
  import torch
9
  import torch.nn as nn
10
 
 
21
  Output:
22
  - Residue representations: (B, L, projection_dim)
23
 
24
+ The optional label embedding is a model input channel. In the browser demo
25
+ it is the shared ``NONE`` label and is not a domain, binding-site, or
26
+ functional assignment.
27
  """
28
 
29
  def __init__(self,