Spaces:
Sleeping
Sleeping
Deploy BioInteract Gradio demo
Browse files- README.md +3 -3
- app.py +110 -6
- examples/davis_applicability_reference.json +0 -0
- examples/interpretability_report.json +11 -11
- requirements.txt +15 -14
- src/data/protein_feat.py +1 -1
- src/models/interaction.py +7 -7
- src/models/target_encoder.py +10 -10
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 |
-
|
|
|
|
|
|
|
|
|
|
| 202 |
_EXAMPLE_SEQUENCE = (
|
| 203 |
-
"
|
| 204 |
-
"
|
| 205 |
-
"
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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,
|