CupOFMoCA / CupOFMoCA-PROTAC /md /ternary_span.py
OkayBoboLi's picture
Add the PROTAC release
f12d3b3 verified
Raw History Blame Contribute Delete
4.57 kB
#!/usr/bin/env python
"""Warhead-to-E3 span distribution from the water ensembles.
Per frame, the distance between the centroid of the warhead core (the group that
occupies the target pocket) and the centroid of the E3 ligand core. In a
productive ternary complex that span has to bridge the two pockets, so the
distribution shows which separations a linker can hold. Water, not chloroform:
the ternary complex forms in the cytosol.
This is a description of reach, not a degradation score; scoring it needs a
target ternary geometry from docking. Replica spread is printed alongside so
poorly converged runs are visible.
Default SMARTS are for the EGFR-VHL series (anilinoquinazoline warhead,
4-hydroxyproline E3 anchor); override with --warhead-smarts / --e3-smarts.
Usage:
python ternary_span.py egfrvhl_rank4 ms39_ref --out span.png
"""
import argparse, glob, os
import numpy as np
import mdtraj as md
from rdkit import Chem
from scipy.stats import gaussian_kde
import matplotlib; matplotlib.use("Agg")
import matplotlib.pyplot as plt
WH_SMARTS = "c1cc2ncncc2cc1" # quinazoline bicyclic, target pocket
E3_SMARTS = "[#6]1[#6][#6](O)[#6][#7]1" # 4-hydroxypyrrolidine, VHL pocket
def anchors(sdf, wh_smarts, e3_smarts):
m = Chem.MolFromMolFile(sdf, removeHs=False)
wh = list(m.GetSubstructMatch(Chem.MolFromSmarts(wh_smarts)))
e3 = list(m.GetSubstructMatch(Chem.MolFromSmarts(e3_smarts)))
syms = [a.GetSymbol() for a in m.GetAtoms()]
return wh, e3, syms
def spans_for_run(run, solvent, wh_smarts, e3_smarts):
"""Per-replica span arrays (A), or None if the run can't be used."""
name = os.path.basename(run.rstrip("/"))
tops = glob.glob(os.path.join(run, solvent, "*_solvated.pdb"))
dcds = sorted(glob.glob(os.path.join(run, solvent, "rep*", f"{name}_prod.dcd")))
if not tops or not dcds:
print(f"{name:14s} no {solvent} data")
return None
wh, e3, sdf_syms = anchors(os.path.join(run, f"{name}.sdf"), wh_smarts, e3_smarts)
if not wh or not e3:
print(f"{name:14s} SMARTS match failed (wh={len(wh)} e3={len(e3)})")
return None
out = []
for d in dcds:
tr = md.load(d, top=tops[0])
lig = [a.index for r in tr.topology.residues
if not r.is_water and r.n_atoms > 5 for a in r.atoms]
sub = tr.atom_slice(lig)
if [a.element.symbol for a in sub.topology.atoms] != sdf_syms:
print(f"{name:14s} SDF/MD atom-order mismatch, skipping")
return None
xyz = sub.xyz * 10.0 # nm -> A
out.append(np.linalg.norm(xyz[:, wh, :].mean(1) - xyz[:, e3, :].mean(1), axis=1))
return out
def main():
ap = argparse.ArgumentParser()
ap.add_argument("runs", nargs="+", help="run directories, one per compound")
ap.add_argument("--solvent", default="water")
ap.add_argument("--warhead-smarts", default=WH_SMARTS)
ap.add_argument("--e3-smarts", default=E3_SMARTS)
ap.add_argument("--out", default="ternary_span.png")
a = ap.parse_args()
fig, ax = plt.subplots(figsize=(11, 6.5), dpi=150)
fig.patch.set_facecolor("white")
cmap = plt.get_cmap("viridis")
print(f"{'compound':14s}{'mean span':>10s}{'sd':>6s}{'min':>6s}{'max':>6s}"
f"{'range':>7s}{'rep spread':>12s}")
print("-" * 67)
drawn = 0
for i, run in enumerate(a.runs):
label = os.path.basename(run.rstrip("/"))
per_rep = spans_for_run(run, a.solvent, a.warhead_smarts, a.e3_smarts)
if not per_rep:
continue
s = np.concatenate(per_rep)
spread = float(np.std([r.mean() for r in per_rep]))
print(f"{label:14s}{s.mean():10.1f}{s.std():6.1f}{s.min():6.1f}"
f"{s.max():6.1f}{s.max()-s.min():7.1f}{spread:12.1f}")
grid = np.linspace(3, 25, 300)
ax.plot(grid, gaussian_kde(s, bw_method=0.15)(grid), lw=2.2,
color=cmap(i / max(1, len(a.runs) - 1)),
label=f"{label} ({s.mean():.0f} A)")
drawn += 1
if not drawn:
return
ax.set_xlabel("warhead to E3 pharmacophore separation (A)")
ax.set_ylabel("probability density")
ax.set_title("Warhead to E3 span\n"
f"centroid distance between the pocket-binding groups ({a.solvent})",
fontsize=12.5, weight="bold", loc="left")
ax.legend(fontsize=8.5, frameon=False, ncol=2)
ax.grid(alpha=0.25)
fig.tight_layout()
fig.savefig(a.out, facecolor="white")
print(f"\nwrote {a.out}")
if __name__ == "__main__":
main()