File size: 3,220 Bytes
f12d3b3 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | #!/usr/bin/env python
"""Compare polar-atom selection conventions for 3D PSA on a finished run.
The element-based selection (N, O, polar H) and the charge-threshold selection
used in the reference work give different absolute PSA values on the same
trajectory. This prints both, sweeping the charge threshold, so a gap against
published numbers can be attributed to convention rather than to conformation.
Charges come from the run's AM1-BCC cache, so nothing is recomputed.
Takes a run name rather than a path and resolves <name>/<solvent>/ against the
current directory, so run it from the md folder. Solvent defaults to chloroform.
Usage:
python psa_method_probe.py egfrvhl_rank4 [solvent]
"""
import json, glob, numpy as np, mdtraj as md
import xml.etree.ElementTree as ET
def cached_charges(cache_path):
d = json.load(open(cache_path))
entry = list(list(d.values())[0].values())[0] # {'openff-2.2.0': {'1': {'ffxml': ...}}}
ffxml = entry["ffxml"]
root = ET.fromstring(ffxml)
# type -> charge from NonbondedForce
q_by_type = {}
for a in root.iter("Atom"):
if a.get("charge") is not None and a.get("type") is not None:
q_by_type[a.get("type")] = float(a.get("charge"))
# atom order from the Residue template
res = root.find(".//Residues/Residue")
order = [a.get("type") for a in res.findall("Atom")]
return np.array([q_by_type[t] for t in order])
def main():
import sys
NAME = sys.argv[1] if len(sys.argv) > 1 else "egfrvhl_rank4"
SOL = sys.argv[2] if len(sys.argv) > 2 else "chloroform"
q = cached_charges(f"{NAME}/{SOL}/ff_cache.json")
print(f"charges from cache: n={len(q)} range [{q.min():.2f},{q.max():.2f}] "
f"#|q|>1.0={(np.abs(q) > 1.0).sum()}")
top = glob.glob(f"{NAME}/{SOL}/*_solvated.pdb")[0]
reps = sorted(glob.glob(f"{NAME}/{SOL}/rep*/{NAME}_prod.dcd"))
tr = md.join([md.load(r, top=top) for r in reps])
lig = [a.index for r in tr.topology.residues if not r.is_water and r.n_atoms > 5
for a in r.atoms]
t = tr.atom_slice(lig); t.center_coordinates(); L = t.topology
syms = [a.element.symbol for a in L.atoms]
if len(syms) != len(q):
print(f"atom-count mismatch: cache {len(q)} vs MD {len(syms)}"); return
sasa = md.shrake_rupley(t, mode="atom", probe_radius=0.14) # nm^2
def area(mask): return (sasa[:, mask].sum(1) * 100.0).mean()
nbr = {}
for b in L.bonds:
nbr.setdefault(b[0].index, []).append(b[1].index)
nbr.setdefault(b[1].index, []).append(b[0].index)
elem = np.array([i for i, a in enumerate(L.atoms)
if a.element.symbol in ("N", "O") or (a.element.symbol == "H" and
any(L.atom(j).element.symbol in ("N", "O") for j in nbr.get(a.index, [])))])
print(f"\nelement selection (N, O, polar H): {area(elem):.0f} A2 ({len(elem)} atoms)")
print("charge-threshold selection:")
print(f" {'|q|>thr':>8s}{'#atoms':>8s}{'PSA':>7s}")
for thr in [0.10, 0.15, 0.20, 0.25, 0.30, 0.40, 0.50]:
m = np.abs(q) > thr
print(f" {thr:>8.2f}{int(m.sum()):>8d}{area(m):>7.0f}")
print("\nreference permeable band = 190-265 A2")
if __name__ == "__main__":
main()
|