Delower's picture
Upload app.py with huggingface_hub
0fb3946 verified
Raw History Blame Contribute Delete
9.56 kB
"""DPP-4 pIC50 Inference Web App — Gradio UI, models loaded from Hugging Face.
Deploy: Hugging Face Space (Gradio) AND GitHub Pages link to the Space.
This app is prepared now; it is TESTED only after training finishes,
once the HF repos below actually exist.
Models (HF repos, loaded on demand by model name):
- extra-trees Delower/dpp4-pic50-extra-trees
- random-forest Delower/dpp4-pic50-random-forest
- xgboost Delower/dpp4-pic50-xgboost
- dnn Delower/dpp4-pic50-dnn
- ltn Delower/dpp4-pic50-ltn (NeSy / LTN)
- gat Delower/dpp4-pic50-gat (graph)
- transformer Delower/dpp4-pic50-transformer (SMILES Transformer)
- chemberta Delower/dpp4-pic50-chemberta (LLM / ChemBERTa)
Features: single SMILES input, or batch CSV upload (smiles column,
optional compound_id). Preview table, predicted pIC50 / IC50 / class,
download CSV, and two plots (pIC50 distribution, predicted vs wet-lab
for the FDA-12) downloadable as PNG.
"""
import io, os, tempfile
import numpy as np
import pandas as pd
import gradio as gr
import spaces
@spaces.GPU
def _zerogpu_dummy():
# Dummy GPU function so the Space starts on free ZeroGPU hardware.
# Never called; prediction code runs on CPU as before.
return None
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
MODEL_REPOS = {
"ExtraTrees (best)": "Delower/dpp4-pic50-extra-trees",
"RandomForest": "Delower/dpp4-pic50-random-forest",
"XGBoost (tuned)": "Delower/dpp4-pic50-xgboost",
"DNN": "Delower/dpp4-pic50-dnn",
"LTN (NeSy)": "Delower/dpp4-pic50-ltn",
"GAT (graph)": "Delower/dpp4-pic50-gat",
"Transformer": "Delower/dpp4-pic50-transformer",
"ChemBERTa (LLM)": "Delower/dpp4-pic50-chemberta",
}
_cache = {}
def load_model(name):
"""Load a model from its HF repo on first use, cache after."""
if name in _cache:
return _cache[name]
from huggingface_hub import hf_hub_download
import pickle
repo = MODEL_REPOS[name]
# model file name is <model>_<features>.pkl, recorded in config.json
from huggingface_hub import list_repo_files
files = list_repo_files(repo, repo_type="model")
pkl = [f for f in files if f.endswith(".pkl")][0]
path = hf_hub_download(repo_id=repo, filename=pkl, repo_type="model")
with open(path, "rb") as f:
model = pickle.load(f)
_cache[name] = model
return model
def _ltn_weights():
"""Tuned LTN weights (npz) from Delower/dpp4-pic50-ltn, cached."""
if "ltn_weights" in _cache:
return _cache["ltn_weights"]
from huggingface_hub import hf_hub_download
path = hf_hub_download(repo_id="Delower/dpp4-pic50-ltn",
filename="ltn_model.npz", repo_type="model")
z = np.load(path)
keys = sorted((k for k in z.files if k.startswith("w")),
key=lambda k: int(k[1:]))
w = [z[k].astype(np.float64) for k in keys]
_cache["ltn_weights"] = w
return w
def featurise_4608(smiles_list):
"""Morgan1024 + RDKitFP1024 + Morgan512 + Morgan2048 (training order)."""
from rdkit import Chem, DataStructs
from rdkit.Chem import AllChem
def bits(fp, n):
a = np.zeros(n, dtype=np.float32)
DataStructs.ConvertToNumpyArray(fp, a)
return a
rows = []
for s in smiles_list:
mol = Chem.MolFromSmiles(s)
if mol is None:
rows.append(np.zeros(4608, dtype=np.float32))
continue
rows.append(np.concatenate([
bits(AllChem.GetMorganFingerprintAsBitVect(mol, 2, nBits=1024), 1024),
bits(Chem.RDKFingerprint(mol, fpSize=1024), 1024),
bits(AllChem.GetMorganFingerprintAsBitVect(mol, 2, nBits=512), 512),
bits(AllChem.GetMorganFingerprintAsBitVect(mol, 2, nBits=2048), 2048)]))
return np.vstack(rows).astype(np.float64)
def predict_ltn(df):
"""Tuned LTN (768-384-192, ELU) forward pass in pure NumPy — the
Space needs no TensorFlow. Verified vs the training run (<1e-5)."""
w = _ltn_weights()
h = featurise_4608(list(df["smiles"]))
for i in range(0, len(w) - 2, 2):
h = h @ w[i] + w[i + 1]
h = np.where(h > 0, h, np.exp(h) - 1.0) # ELU
pred = (h @ w[-2] + w[-1]).ravel()
df = df.copy()
df["predicted_pIC50"] = pred
df["predicted_IC50_nM"] = 10 ** (9 - df["predicted_pIC50"])
df["predicted_class"] = np.where(df["predicted_pIC50"] >= 7, "potent",
np.where(df["predicted_pIC50"] >= 6, "moderate", "weak"))
return df
def predict_df(df, model_name):
if model_name == "LTN (NeSy)":
return predict_ltn(df)
model = load_model(model_name)
# featurise with the model's own feature code (Morgan+physchem
# for tree models; the model pipeline carries its preprocessor)
from rdkit import Chem
from rdkit.Chem import AllChem, Descriptors
X = []
for s in df["smiles"]:
mol = Chem.MolFromSmiles(s)
fp = list(AllChem.GetMorganFingerprintAsBitVect(mol, 2, nBits=1024))
phys = [Descriptors.MolWt(mol), Descriptors.MolLogP(mol),
Descriptors.TPSA(mol), Descriptors.NumHDonors(mol),
Descriptors.NumHAcceptors(mol),
Descriptors.NumRotatableBonds(mol)]
X.append(fp + phys)
df = df.copy()
Xdf = pd.DataFrame(X, columns=[f"feature_{i}" for i in range(1030)])
df["predicted_pIC50"] = model.predict(Xdf)
df["predicted_IC50_nM"] = 10 ** (9 - df["predicted_pIC50"])
df["predicted_class"] = np.where(df["predicted_pIC50"] >= 7, "potent",
np.where(df["predicted_pIC50"] >= 6, "moderate", "weak"))
return df
def plot_dist(df):
fig, ax = plt.subplots()
ax.hist(df["predicted_pIC50"], bins=20)
ax.set_xlabel("Predicted pIC50"); ax.set_ylabel("Count")
ax.set_title("Predicted pIC50 distribution")
return fig
def plot_fda(df):
# predicted vs wet-lab, only for rows that have a wet-lab value
if "wetlab_pIC50" not in df.columns or df["wetlab_pIC50"].dropna().empty:
return None
sub = df.dropna(subset=["wetlab_pIC50"])
fig, ax = plt.subplots()
ax.scatter(sub["wetlab_pIC50"], sub["predicted_pIC50"])
ax.plot([4, 10], [4, 10], "--")
ax.set_xlabel("Wet-lab pIC50"); ax.set_ylabel("Predicted pIC50")
ax.set_title("Predicted vs wet-lab pIC50 (FDA-12)")
return fig
def smiles_to_2d(smiles):
"""Return a 2D depiction image (PIL) of the SMILES, or None if invalid."""
from rdkit import Chem
from rdkit.Chem import Draw, AllChem
mol = Chem.MolFromSmiles(smiles)
if mol is None:
return None
AllChem.Compute2DCoords(mol)
return Draw.MolToImage(mol, size=(400, 300))
def single(smiles, model_name):
df = pd.DataFrame({"compound_id": ["input"], "smiles": [smiles]})
out = predict_df(df, model_name)
return out, smiles_to_2d(smiles), plot_dist(out), plot_fda(out)
def batch(file, model_name):
df = pd.read_csv(file.name)
out = predict_df(df, model_name)
fd, path = tempfile.mkstemp(prefix="dpp4_predictions_", suffix=".csv")
with os.fdopen(fd, "w") as fh:
out.to_csv(fh, index=False)
return out, plot_dist(out), plot_fda(out), path
with gr.Blocks(title="Computational Chemistry and Drug Discovery — DPP-4 pIC50 Predictor") as demo:
gr.Markdown("# Computational Chemistry and Drug Discovery\n## DPP-4 pIC50 Predictor\nSMILES in → predicted pIC50 / IC50. Models are loaded from Hugging Face on demand. Not for clinical use.")
gr.Markdown("---")
gr.Markdown("© 2026 Delower Hossain — Computational Chemistry and Drug Discovery. All rights reserved. | Models are in-silico predictions only, not for clinical use.")
model = gr.Dropdown(list(MODEL_REPOS), value="ExtraTrees (best)", label="Model")
with gr.Tab("Single SMILES"):
smi = gr.Textbox(label="SMILES", placeholder="CC(=O)Oc1ccccc1C(=O)O")
btn = gr.Button("Predict")
out1 = gr.Dataframe(label="Prediction preview")
img1 = gr.Image(label="2D structure")
p1 = gr.Plot(label="pIC50 distribution")
p2 = gr.Plot(label="Predicted vs wet-lab (FDA-12, if applicable)")
btn.click(single, [smi, model], [out1, img1, p1, p2])
with gr.Tab("Batch CSV"):
f = gr.File(label="CSV with a 'smiles' column (optional 'compound_id')")
btn2 = gr.Button("Predict batch")
out2 = gr.Dataframe(label="Prediction preview")
q1 = gr.Plot(label="pIC50 distribution")
q2 = gr.Plot(label="Predicted vs wet-lab (FDA-12, if applicable)")
dl = gr.DownloadButton(label="Download predictions (CSV)")
btn2.click(batch, [f, model], [out2, q1, q2, dl])
with gr.Tab("Computational Experiment Results"):
gr.Markdown("FDA-12 inference with ExtraTrees (original LazyPredict model, RMSE 0.4449). Wet-lab values are comparison-only. RMSE 0.9637, MAE 0.6794 over the 11 drugs with wet-lab values (Gosogliptin excluded).")
fda_btn = gr.Button("Show FDA-12 results as dataframe")
fda_out = gr.Dataframe(label="FDA-12 results")
def show_fda():
import pandas as pd
return pd.read_csv("fda.csv") if __import__("os").path.exists("fda.csv") else pd.DataFrame({"note": ["fda.csv is in the Hugging Face repo Delower/dpp4-pic50-extra-trees"]})
fda_btn.click(show_fda, [], [fda_out])
gr.Markdown("---\n© 2026 Delower Hossain — Computational Chemistry and Drug Discovery. All rights reserved.")
if __name__ == "__main__":
demo.launch()