CupOFMoCA / CupOFMoCA-PROTAC /md /psa_ensemble.py
OkayBoboLi's picture
Add the PROTAC release
f12d3b3 verified
Raw History Blame Contribute Delete
2.92 kB
#!/usr/bin/env python
"""3D polar surface area over an MD ensemble.
Element-based PSA: N, O and polar H, Bondi radii, 1.4 A probe, mdtraj
shrake_rupley. validate_psa.py checks this code path against the published
grazoprevir numbers. Lower PSA in the membrane mimic tracks higher passive
permeability.
Polar atoms are indexed from each run's SDF, so the SDF and MD atom orders must
agree. A run whose ligand atom count does not match its SDF is reported and
dropped from the table rather than summed over the wrong atoms.
Usage:
python psa_ensemble.py egfrvhl_rank4 ms39_ref --solvent chloroform --out psa.npy
"""
import argparse, glob, os
import numpy as np
import mdtraj as md
from rdkit import Chem
def polar_idx_from_sdf(sdf):
m = Chem.MolFromMolFile(sdf, removeHs=False)
idx = []
for a in m.GetAtoms():
s = a.GetSymbol()
if s in ("N", "O"):
idx.append(a.GetIdx())
elif s == "H" and any(nb.GetSymbol() in ("N", "O") for nb in a.GetNeighbors()):
idx.append(a.GetIdx())
return idx, m.GetNumAtoms()
def main():
ap = argparse.ArgumentParser()
ap.add_argument("runs", nargs="+", help="run directories, one per compound")
ap.add_argument("--solvent", default="chloroform")
ap.add_argument("--stride", type=int, default=20)
ap.add_argument("--out", default=None, help="optional .npy dump of the per-frame values")
a = ap.parse_args()
print(f"{'compound':16s}{'n_pol':6s}{'3D PSA (A^2) mean(std)':24s}{'frames':8s}")
print("-" * 56)
out = {}
for run in a.runs:
name = os.path.basename(run.rstrip("/"))
pol, nat = polar_idx_from_sdf(os.path.join(run, f"{name}.sdf"))
tops = glob.glob(os.path.join(run, a.solvent, "*_solvated.pdb"))
if not tops:
print(f"{name:16s} no {a.solvent} topology"); continue
vals = []
for dcd in sorted(glob.glob(os.path.join(run, a.solvent, "rep*", f"{name}_prod.dcd"))):
tr = md.load(dcd, top=tops[0], stride=a.stride)
lig = [at.index for r in tr.topology.residues
if not r.is_water and r.n_atoms > 5 for at in r.atoms]
sub = tr.atom_slice(lig)
if sub.n_atoms != nat:
print(f"{name:16s} atom count mismatch md={sub.n_atoms} sdf={nat}")
vals = None
break
sasa = md.shrake_rupley(sub, mode="atom", probe_radius=0.14) # nm^2
vals.extend((sasa[:, pol].sum(1) * 100.0).tolist()) # A^2 per frame
if vals:
v = np.array(vals)
print(f"{name:16s}{len(pol):<6d}{f'{v.mean():.0f} ({v.std():.0f})':24s}{len(v):<8d}")
out[name] = dict(mean=float(v.mean()), std=float(v.std()), vals=v.tolist())
if a.out and out:
np.save(a.out, out, allow_pickle=True)
print(f"\nsaved {a.out}")
if __name__ == "__main__":
main()