dpp4-kd-ltn / predict_kd.py
Delower's picture
Upload predict_kd.py with huggingface_hub
72d1407 verified
Raw History Blame Contribute Delete
4.86 kB
#!/usr/bin/env python
"""Inference for the DPP-4 Kd LTN (Delower/dpp4-kd-ltn).
Predicts pKd for human DPP-4 (UniProt P27487 / CHEMBL284) from a SMILES string.
Features are exactly those used in training: Morgan fingerprint (r=2, 2048 bits)
concatenated with the fixed DPP-4 ESM-2 650M embedding (1280 dims) = 3328, no scaling.
Usage:
python predict_kd.py "Cn1c(=O)cc(N2CCC[C@@H](N)C2)n(Cc2ccccc2C#N)c1=O"
python predict_kd.py --csv compounds.csv --smiles-col canonical_smiles --out preds.csv
Outputs per compound: predicted pKd, Kd in nM (= 10^(9-pKd)),
deltaG in kcal/mol (= -1.3642 * pKd), and potency class
(potent pKd>=7 / moderate 6-7 / weak <6).
Requires: torch, rdkit, numpy. Files ltn_stage2.pt, esm2_dpp4.npz and config.json
must sit next to this script (or pass --model-dir).
"""
import argparse, csv, json, os, sys
import numpy as np
import torch
import torch.nn as nn
HERE = os.path.dirname(os.path.abspath(__file__))
class LtnNet(nn.Module):
"""Same architecture as training (train_kd_ltn.py)."""
def __init__(self, in_dim=3328):
super().__init__()
self.trunk = nn.Sequential(
nn.Linear(in_dim, 768), nn.GELU(), nn.Dropout(0.1),
nn.Linear(768, 384), nn.GELU(), nn.Dropout(0.1),
nn.Linear(384, 192), nn.GELU())
self.reg = nn.Linear(192, 1)
self.clf = nn.Linear(192, 3)
def forward(self, x):
h = self.trunk(x)
return self.reg(h).squeeze(-1), self.clf(h)
def band_name(pkd):
return "potent" if pkd >= 7.0 else ("moderate" if pkd >= 6.0 else "weak")
def fingerprints(smiles_list):
from rdkit import Chem
from rdkit.Chem import rdFingerprintGenerator
gen = rdFingerprintGenerator.GetMorganGenerator(radius=2, fpSize=2048)
out = np.zeros((len(smiles_list), 2048), dtype=np.float32)
for i, s in enumerate(smiles_list):
m = Chem.MolFromSmiles(s)
if m is not None:
out[i] = np.asarray(gen.GetFingerprint(m), dtype=np.float32)
return out
class KdLtnPredictor:
def __init__(self, model_dir=HERE):
with open(os.path.join(model_dir, "config.json")) as f:
self.config = json.load(f)
z = np.load(os.path.join(model_dir, "esm2_dpp4.npz"), allow_pickle=True)
keys = z["target_keys"].tolist()
key = self.config["target"]["esm2_target_key"]
self.protein_vec = z["embeddings"][keys.index(key)].astype(np.float32)
self.net = LtnNet(self.config["features"]["total_dim"])
sd = torch.load(os.path.join(model_dir, self.config["weights_file"]),
map_location="cpu", weights_only=True)
self.net.load_state_dict(sd)
self.net.eval()
def predict(self, smiles_list):
fp = fingerprints(smiles_list)
X = np.concatenate([fp, np.tile(self.protein_vec, (len(smiles_list), 1))], axis=1)
with torch.no_grad():
pkd, _ = self.net(torch.tensor(X, dtype=torch.float32))
out = []
for s, p in zip(smiles_list, pkd.numpy()):
p = float(p)
out.append({
"smiles": s,
"predicted_pKd": round(p, 4),
"predicted_Kd_nM": 10 ** (9 - p),
"predicted_deltaG_kcal_mol": round(-1.3642 * p, 4),
"potency_class": band_name(p),
})
return out
def main():
ap = argparse.ArgumentParser(description="DPP-4 Kd LTN inference (human DPP-4, P27487)")
ap.add_argument("smiles", nargs="*", help="one or more SMILES strings")
ap.add_argument("--csv", help="input CSV with a SMILES column")
ap.add_argument("--smiles-col", default="smiles", help="SMILES column name for --csv")
ap.add_argument("--out", help="write results to this CSV instead of stdout")
ap.add_argument("--model-dir", default=HERE)
args = ap.parse_args()
smiles = list(args.smiles)
if args.csv:
with open(args.csv, newline="") as f:
smiles += [r[args.smiles_col] for r in csv.DictReader(f)]
if not smiles:
ap.error("give at least one SMILES or --csv")
rows = KdLtnPredictor(args.model_dir).predict(smiles)
fields = ["smiles", "predicted_pKd", "predicted_Kd_nM", "predicted_deltaG_kcal_mol", "potency_class"]
if args.out:
with open(args.out, "w", newline="") as f:
w = csv.DictWriter(f, fieldnames=fields)
w.writeheader()
for r in rows:
w.writerow({**r, "predicted_Kd_nM": f"{r['predicted_Kd_nM']:.4g}"})
print(f"wrote {len(rows)} predictions to {args.out}")
else:
for r in rows:
print(f"pKd {r['predicted_pKd']:7.4f} | Kd {r['predicted_Kd_nM']:12.4g} nM | "
f"dG {r['predicted_deltaG_kcal_mol']:8.4f} kcal/mol | {r['potency_class']:8s} | {r['smiles']}")
if __name__ == "__main__":
main()