Download CupOFMoCA-PROTAC/md/rg_basins.py from ChatterjeeLab/CupOFMoCA: direct link, hf CLI and curl.
- Browser
- Download file 4.95 kB
-
https://huggingface.co/ChatterjeeLab/CupOFMoCA/resolve/main/CupOFMoCA-PROTAC/md/rg_basins.py
- Command line
-
hf download hf://ChatterjeeLab/CupOFMoCA/CupOFMoCA-PROTAC/md/rg_basins.py
-
curl -L -o rg_basins.py https://huggingface.co/ChatterjeeLab/CupOFMoCA/resolve/main/CupOFMoCA-PROTAC/md/rg_basins.py
4.95 kB
| #!/usr/bin/env python | |
| """Rg free-energy profiles and basin populations for one or more finished runs. | |
| Mean Rg does not separate permeable from non-permeable PROTACs in the J. Med. | |
| Chem. 2022 study: their most permeable compound sits mainly just above 8 A, | |
| overlapping their least permeable one at 7-9.5 A. What separates them is whether | |
| a compact minimum near 5.5 A is populated at all. This script computes, per | |
| compound and solvent, | |
| PMF(Rg) = -kT ln P(Rg) | |
| locates its basins (local minima), and reports each basin's position and | |
| population, plus the fraction of frames below 6 and 7 A. | |
| Ligand selection and the Rg calculation match analyze_conformations.py exactly | |
| (non-water residue with >5 atoms, md.compute_rg on the centred ligand-only | |
| trajectory). | |
| Usage: | |
| python rg_basins.py egfrvhl_rank4 ms39_ref --solvents chloroform water | |
| """ | |
| import argparse, glob, os | |
| import numpy as np | |
| import mdtraj as md | |
| from scipy.stats import gaussian_kde | |
| from scipy.signal import find_peaks | |
| import matplotlib; matplotlib.use("Agg") | |
| import matplotlib.pyplot as plt | |
| KT = 0.593 # kcal/mol at 300 K | |
| def pooled_rg(run_dir, solvent): | |
| """Rg in Angstrom, pooled over replicas.""" | |
| name = os.path.basename(run_dir.rstrip("/")) | |
| tops = glob.glob(os.path.join(run_dir, solvent, f"{name}_*_solvated.pdb")) | |
| dcds = sorted(glob.glob(os.path.join(run_dir, solvent, "rep*", f"{name}_prod.dcd"))) | |
| if not tops or not dcds: | |
| 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] | |
| lt = tr.atom_slice(lig) | |
| lt.center_coordinates() | |
| out.append(md.compute_rg(lt) * 10.0) # nm -> A | |
| return np.concatenate(out) | |
| def basins(rg, prom_frac=0.05): | |
| """Basins = peaks of P(Rg). Returns (grid, pmf, [(rg_position, population)]).""" | |
| grid = np.linspace(rg.min() - 0.3, rg.max() + 0.3, 400) | |
| dens = gaussian_kde(rg, bw_method=0.12)(grid) | |
| pmf = -KT * np.log(np.clip(dens, 1e-12, None)) | |
| pmf -= pmf.min() | |
| pk, _ = find_peaks(dens, prominence=prom_frac * dens.max()) | |
| if len(pk) == 0: | |
| pk = np.array([int(np.argmax(dens))]) | |
| # valley boundaries between adjacent peaks assign frames to basins | |
| edges = [rg.min() - 1e-6] | |
| for a, b in zip(pk[:-1], pk[1:]): | |
| edges.append(grid[a + int(np.argmin(dens[a:b]))]) | |
| edges.append(rg.max() + 1e-6) | |
| out = [] | |
| for i, p in enumerate(pk): | |
| pop = float(((rg > edges[i]) & (rg <= edges[i + 1])).mean()) | |
| out.append((float(grid[p]), pop)) | |
| return grid, pmf, out | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("runs", nargs="+", help="run directories, one per compound") | |
| ap.add_argument("--solvents", nargs="+", default=["chloroform", "water"]) | |
| ap.add_argument("--plot-solvent", default="chloroform", | |
| help="solvent whose PMFs go into the figure") | |
| ap.add_argument("--out", default="rg_basins.png") | |
| a = ap.parse_args() | |
| print(f"{'compound':22s}{'solv':6s}{'mean':>7s}{'basins (Rg A, pop%)':>44s}{'<6A':>7s}{'<7A':>7s}") | |
| print("-" * 96) | |
| results = {} | |
| for run in a.runs: | |
| label = os.path.basename(run.rstrip("/")) | |
| for sol in a.solvents: | |
| rg = pooled_rg(run, sol) | |
| if rg is None: | |
| continue | |
| grid, pmf, bs = basins(rg) | |
| results[(label, sol)] = (rg, grid, pmf, bs) | |
| btxt = " ".join(f"{p:.2f}({100*w:.0f}%)" for p, w in bs) | |
| print(f"{label:22s}{sol[:5]:6s}{rg.mean():7.2f}{btxt:>44s}" | |
| f"{100*(rg<6.0).mean():6.0f}%{100*(rg<7.0).mean():6.0f}%") | |
| have = [k for k in results if k[1] == a.plot_solvent] | |
| if not have: | |
| return | |
| fig, ax = plt.subplots(figsize=(11, 6.5), dpi=150) | |
| fig.patch.set_facecolor("white") | |
| cmap = plt.get_cmap("viridis") | |
| for i, key in enumerate(have): | |
| _, grid, pmf, _ = results[key] | |
| ax.plot(grid, pmf, lw=2.2, color=cmap(i / max(1, len(have) - 1)), label=key[0]) | |
| ax.axvspan(5.4, 5.7, color="#009E73", alpha=0.12) | |
| ax.text(5.55, ax.get_ylim()[1] * 0.94, "reference compact\nminimum 5.5-5.6 A", | |
| ha="center", fontsize=8.5, color="#009E73", weight="bold") | |
| ax.axvspan(7.0, 9.5, color="#C0392B", alpha=0.07) | |
| ax.text(8.25, ax.get_ylim()[1] * 0.94, "reference least-permeable\ncompound 7-9.5 A", | |
| ha="center", fontsize=8.5, color="#C0392B", weight="bold") | |
| ax.set_xlabel("Radius of gyration (A)") | |
| ax.set_ylabel("PMF -kT ln P(Rg) (kcal/mol)") | |
| ax.set_title(f"Rg free-energy profiles in {a.plot_solvent}", | |
| fontsize=13, weight="bold", loc="left") | |
| ax.set_ylim(0, 3.2); 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() | |