§12.3-12.4 first binding result on ARM Mac. - Toolchain SOLVED: AutoDock Vina 1.2.5 mac binary (Rosetta) + open-babel (brew). No conda, no MLX. dock_positive_controls.py runs end-to-end. - Cross-dock known binders + negatives into Hb (5E83) and PKR (8XFD), box centered on co-crystal ligands (5L7=voxelotor, WV2=mitapivat). Finding: raw Vina affinity ranks almost perfectly by MOLECULAR SIZE (mitapivat > voxelotor > decitabine/caffeine > hydroxyurea) in both pockets — mitapivat wins even on hemoglobin it doesn't target. Raw score can't distinguish target-specific binding: the docking analog of the connectivity specificity problem. Next: redocking-RMSD validation + ligand-efficiency normalization. Note: machine is 24GB (not 96GB per PLAN §2), capping local AF3-class inference. tools/ gitignored (vina binary). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
112 lines
4.6 KiB
Python
112 lines
4.6 KiB
Python
"""Structure-based track §12.4: dock known binders + negatives into Hb and PKR.
|
|
|
|
Positive-control recovery test: voxelotor should dock best into hemoglobin (5E83), mitapivat best
|
|
into PKR (8XFD), and unrelated drugs (caffeine, hydroxyurea) should score worse. The box is
|
|
centered on each co-crystal ligand (5L7=voxelotor, WV2=mitapivat). AutoDock Vina (mac binary,
|
|
Rosetta) + open-babel prep. Affinity in kcal/mol; more negative = stronger predicted binding.
|
|
|
|
This is a docking baseline, not an efficacy claim (PLAN §12.6).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import subprocess
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
import requests
|
|
|
|
VINA = "./tools/vina"
|
|
STRUCT = Path("data/raw/structures")
|
|
WORK = Path("data/processed/docking")
|
|
WORK.mkdir(parents=True, exist_ok=True)
|
|
|
|
TARGETS = {"hemoglobin": ("5E83", "5L7"), "PKR": ("8XFD", "WV2")}
|
|
LIGANDS = ["voxelotor", "mitapivat", "decitabine", "hydroxyurea", "caffeine"]
|
|
EXPECTED = {"voxelotor": "hemoglobin", "mitapivat": "PKR"}
|
|
|
|
|
|
def pubchem_smiles(name: str) -> str | None:
|
|
for prop in ("SMILES", "ConnectivitySMILES", "IsomericSMILES", "CanonicalSMILES"):
|
|
try:
|
|
d = requests.get(f"https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/name/{name}/property/{prop}/JSON",
|
|
timeout=30).json()["PropertyTable"]["Properties"][0]
|
|
if prop in d:
|
|
return d[prop]
|
|
except Exception:
|
|
continue
|
|
return None
|
|
|
|
|
|
def prep_receptor_and_box(pdb: str, lig_res: str):
|
|
"""Receptor pdbqt (protein only) + box center/size from one copy of the co-crystal ligand."""
|
|
atoms, lig, chosen = [], [], None
|
|
for ln in (STRUCT / f"{pdb}.pdb").read_text().splitlines():
|
|
if ln.startswith("ATOM"):
|
|
atoms.append(ln)
|
|
elif ln.startswith("HETATM") and ln[17:20].strip() == lig_res:
|
|
key = (ln[21], ln[22:26])
|
|
chosen = chosen or key
|
|
if key == chosen:
|
|
lig.append(ln)
|
|
rec_pdb = WORK / f"{pdb}_rec.pdb"
|
|
rec_pdb.write_text("\n".join(atoms) + "\nEND\n")
|
|
rec_pdbqt = WORK / f"{pdb}_rec.pdbqt"
|
|
subprocess.run(["obabel", str(rec_pdb), "-O", str(rec_pdbqt), "-xr", "-p", "7.4"],
|
|
capture_output=True, check=True)
|
|
xyz = np.array([[float(l[30:38]), float(l[38:46]), float(l[46:54])] for l in lig])
|
|
center = xyz.mean(0)
|
|
size = np.clip((xyz.max(0) - xyz.min(0)) + 9.0, 18.0, 28.0)
|
|
return rec_pdbqt, center, size
|
|
|
|
|
|
def prep_ligand(name: str, smiles: str) -> Path | None:
|
|
out = WORK / f"lig_{name}.pdbqt"
|
|
r = subprocess.run(["obabel", f"-:{smiles}", "-O", str(out), "--gen3d", "-p", "7.4"],
|
|
capture_output=True, text=True)
|
|
return out if out.exists() and out.stat().st_size > 0 else None
|
|
|
|
|
|
def dock(rec_pdbqt: Path, lig_pdbqt: Path, center, size) -> float | None:
|
|
out = subprocess.run([VINA, "--receptor", str(rec_pdbqt), "--ligand", str(lig_pdbqt),
|
|
"--center_x", f"{center[0]:.2f}", "--center_y", f"{center[1]:.2f}",
|
|
"--center_z", f"{center[2]:.2f}", "--size_x", f"{size[0]:.1f}",
|
|
"--size_y", f"{size[1]:.1f}", "--size_z", f"{size[2]:.1f}",
|
|
"--exhaustiveness", "8", "--cpu", "4",
|
|
"--out", str(WORK / "o.pdbqt")], capture_output=True, text=True)
|
|
m = re.search(r"^\s+1\s+(-?\d+\.\d+)", out.stdout, re.M)
|
|
return float(m.group(1)) if m else None
|
|
|
|
|
|
def main() -> None:
|
|
smis = {n: pubchem_smiles(n) for n in LIGANDS}
|
|
ligs = {n: prep_ligand(n, s) for n, s in smis.items() if s}
|
|
ligs = {n: p for n, p in ligs.items() if p}
|
|
print(f"prepared ligands: {list(ligs)}")
|
|
|
|
boxes = {t: prep_receptor_and_box(pdb, lr) for t, (pdb, lr) in TARGETS.items()}
|
|
print("prepared receptors:", list(boxes))
|
|
|
|
print(f"\n{'ligand':14s}" + "".join(f"{t:>13s}" for t in TARGETS))
|
|
results = {}
|
|
for lname, lpath in ligs.items():
|
|
row = {}
|
|
for tname, (rec, center, size) in boxes.items():
|
|
row[tname] = dock(rec, lpath, center, size)
|
|
results[lname] = row
|
|
cells = "".join(f"{(f'{row[t]:.1f}' if row[t] is not None else 'NA'):>13s}" for t in TARGETS)
|
|
print(f" {lname:12s}{cells}")
|
|
|
|
print("\n=== positive-control recovery test (§12.4) ===")
|
|
for lig, exp_target in EXPECTED.items():
|
|
if lig in results:
|
|
best = min(results[lig], key=lambda t: results[lig][t] if results[lig][t] is not None else 99)
|
|
ok = best == exp_target
|
|
print(f" {lig:12s} best target = {best:11s} (expected {exp_target}) -> {'PASS' if ok else 'FAIL'}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|