Download make_mb_panels.py from ChatterjeeLab/ScooBDoob: direct link, hf CLI and curl.
- Browser
- Download file 11.7 kB
-
https://huggingface.co/ChatterjeeLab/ScooBDoob/resolve/main/make_mb_panels.py
- Command line
-
hf download hf://ChatterjeeLab/ScooBDoob/make_mb_panels.py
-
curl -L -o make_mb_panels.py https://huggingface.co/ChatterjeeLab/ScooBDoob/resolve/main/make_mb_panels.py
11.7 kB
| #!/usr/bin/env python | |
| from __future__ import annotations | |
| import json, os | |
| import numpy as np, torch | |
| import matplotlib | |
| matplotlib.use("Agg") | |
| import matplotlib.pyplot as plt | |
| from matplotlib.gridspec import GridSpec | |
| from scoobdoob.mb_potential import mb_potential_xy | |
| from scoobdoob.validation import msm_build as mb, tpt, bridge | |
| from scoobdoob.validation import mb_ground_truth as gt | |
| from scoobdoob.validation import mb_data as V | |
| plt.rcParams["svg.fonttype"] = "none" | |
| plt.rcParams["pdf.fonttype"] = 42 | |
| DATA_DIR = "data/mb_panels_data" | |
| LAG_OPT, N_OPT = 5, 16 | |
| TICA_LAG = 20 | |
| def channel(paths, q, xy, on, thru, start, end, nbins=18): | |
| """Dominant route = the maximum-bottleneck (widest) path through the ensemble's OWN empirical transition graph, from the start set to the target set.""" | |
| import heapq | |
| from collections import defaultdict | |
| W = defaultdict(float) | |
| for p in paths: | |
| for a, b in zip(p[:-1], p[1:]): | |
| if a != b and on[a] and on[b]: | |
| W[(int(a), int(b))] += 1.0 | |
| if not W: | |
| return np.zeros((0, 2)) | |
| adj = defaultdict(list) | |
| for (a, b), c in W.items(): | |
| adj[a].append((b, c)) | |
| S, T = set(map(int, start)), set(map(int, end)) | |
| best = defaultdict(float) | |
| prev = {} | |
| heap = [] | |
| for a in S: | |
| best[a] = float("inf") | |
| heapq.heappush(heap, (-best[a], a)) | |
| seen = set() | |
| while heap: | |
| nb, u = heapq.heappop(heap) | |
| nb = -nb | |
| if u in seen: | |
| continue | |
| seen.add(u) | |
| if u in T: | |
| path = [u] | |
| while path[-1] not in S: | |
| path.append(prev[path[-1]]) | |
| return xy[path[::-1]] | |
| for v, c in adj[u]: | |
| w = min(nb, c) | |
| if w > best[v]: | |
| best[v] = w | |
| prev[v] = u | |
| heapq.heappush(heap, (-w, v)) | |
| return np.zeros((0, 2)) | |
| _CACHE_KEYS = [ | |
| "gx", | |
| "gy", | |
| "gv", | |
| "S1", | |
| "S2", | |
| "Axy", | |
| "Bxy", | |
| "xlo", | |
| "xhi", | |
| "ylo", | |
| "yhi", | |
| "cx", | |
| "cy", | |
| "q", | |
| "keep", | |
| "xy", | |
| "mep", | |
| "arc", | |
| "Emep", | |
| "ch_raw", | |
| "ch_fk", | |
| "praw", | |
| "pfk", | |
| "s1_raw", | |
| "s1_fk", | |
| "counts", | |
| "thru", | |
| "on", | |
| ] | |
| CACHE_NPZ = "data/mb_panels_data/panels_cache.npz" | |
| def save_cache(D, path=CACHE_NPZ): | |
| os.makedirs(os.path.dirname(path), exist_ok=True) | |
| out = {} | |
| for k, v in D.items(): | |
| if k in ("praw", "pfk"): | |
| out[k] = np.array([np.asarray(p) for p in v], dtype=object) | |
| else: | |
| out[k] = np.asarray(v) | |
| np.savez_compressed(path, **out) | |
| print(f"[cache] -> {path}") | |
| def load_cache(path=CACHE_NPZ): | |
| if not os.path.exists(path): | |
| raise SystemExit(f"missing {path}\nrun: python make_mb_panels.py --cache-only") | |
| z = np.load(path, allow_pickle=True) | |
| D = {k: z[k] for k in z.files} | |
| for k in ("praw", "pfk"): | |
| D[k] = list(D[k]) | |
| for k in ("xlo", "xhi", "ylo", "yhi", "s1_raw", "s1_fk"): | |
| D[k] = float(D[k]) | |
| print(f"[cache] loaded {path}") | |
| return D | |
| def compute(): | |
| """Everything numeric. Returns the dict the plot stage needs; touches no matplotlib.""" | |
| trajs = V.load_trajs(str(V.DEFAULT_DATA)) | |
| model = mb.build_full_msm( | |
| trajs, | |
| trajs, | |
| lag=LAG_OPT, | |
| tica_lag=TICA_LAG, | |
| k=V.K, | |
| n_comp=2, | |
| seed=0, | |
| use_tica=True, | |
| ) | |
| A, B, _ = mb.q2_sign_endpoints(model) | |
| start, end = V.orient_endpoints(model, A, B) | |
| xy = model.state_xy | |
| _, q, _ = tpt.committor_agreement(model, start, end) | |
| rf = tpt.reactive_flux(model, start, end) | |
| thru = np.asarray(rf.gross_flux).sum(1) | |
| sad = gt.saddle_points() | |
| S1 = sad[np.argmin(sad[:, 0])] | |
| S2 = sad[np.argmax(sad[:, 0])] | |
| q1 = q[np.argmin(np.linalg.norm(xy - S1, axis=1))] | |
| q2 = q[np.argmin(np.linalg.norm(xy - S2, axis=1))] | |
| share = thru / thru.sum() | |
| on = share > 0.5 / model.n_active | |
| on[list(start)] = True | |
| on[list(end)] = True | |
| Axy = model.active_to_xy(start).mean(0) | |
| Bxy = model.active_to_xy(end).mean(0) | |
| land = np.vstack([Axy, Bxy, sad]) | |
| lo = land.min(0) - 0.35 | |
| hi = land.max(0) + 0.35 | |
| inbox = ( | |
| (xy[:, 0] >= lo[0]) | |
| & (xy[:, 0] <= hi[0]) | |
| & (xy[:, 1] >= lo[1]) | |
| & (xy[:, 1] <= hi[1]) | |
| ) | |
| on = on & inbox | |
| keep = np.where(on)[0] | |
| cx, cy = xy[keep, 0], xy[keep, 1] | |
| labels = np.concatenate(model.dtrajs) | |
| counts = np.bincount(labels, minlength=model.n_active)[: model.n_active].astype( | |
| float | |
| ) | |
| rho = np.maximum(counts, 1) / counts.mean() | |
| react = (q > 0.05) & (q < 0.95) | |
| rho_floor = np.percentile(rho[react], 25) | |
| w_fk = np.exp(-LAG_OPT * 1.5 * np.maximum(rho, rho_floor)) | |
| w_fk[~on] = 0.0 | |
| w_raw = np.ones_like(w_fk) | |
| w_raw[~on] = 0.0 | |
| def rollout(w): | |
| Ph = bridge.doob_kernels(model.P, end, N_OPT, w=w) | |
| return bridge.sample_bridge_paths(Ph, start, n_paths=3000, seed=0) | |
| praw = rollout(w_raw) | |
| pfk = rollout(w_fk) | |
| ch_raw = channel(praw, q, xy, on, thru, start, end) | |
| ch_fk = channel(pfk, q, xy, on, thru, start, end) | |
| n1 = np.linalg.norm(xy - S1, axis=1) < 0.25 | |
| s1_raw = np.mean([any(n1[s] for s in p) for p in praw]) * 100 | |
| s1_fk = np.mean([any(n1[s] for s in p) for p in pfk]) * 100 | |
| M = np.load("data/mb_mep.npz") | |
| mep = M["mep"] | |
| Emep = M["Emep"] | |
| arc = M["arc"] | |
| _keep_mep = mep[:, 1] >= -0.05 | |
| mep = mep[_keep_mep] | |
| xlo, xhi = cx.min() - 0.12, cx.max() + 0.12 | |
| ylo, yhi = cy.min() - 0.12, cy.max() + 0.12 | |
| gx, gy = np.meshgrid(np.linspace(xlo, xhi, 300), np.linspace(ylo, yhi, 300)) | |
| with torch.no_grad(): | |
| gv = np.clip( | |
| mb_potential_xy(torch.tensor(gx), torch.tensor(gy)).numpy(), -150, 100 | |
| ) | |
| return {k: v for k, v in locals().items() if k in _CACHE_KEYS} | |
| def render(D): | |
| os.makedirs("figures", exist_ok=True) | |
| """Plotting only. Reads the cache dict; recomputes nothing. Safe to re-run while restyling.""" | |
| gx = D["gx"] | |
| gy = D["gy"] | |
| gv = D["gv"] | |
| S1 = D["S1"] | |
| S2 = D["S2"] | |
| Axy = D["Axy"] | |
| Bxy = D["Bxy"] | |
| xlo = D["xlo"] | |
| xhi = D["xhi"] | |
| ylo = D["ylo"] | |
| yhi = D["yhi"] | |
| cx = D["cx"] | |
| cy = D["cy"] | |
| q = D["q"] | |
| keep = D["keep"] | |
| xy = D["xy"] | |
| mep = D["mep"] | |
| arc = D["arc"] | |
| Emep = D["Emep"] | |
| ch_raw = D["ch_raw"] | |
| ch_fk = D["ch_fk"] | |
| praw = D["praw"] | |
| pfk = D["pfk"] | |
| s1_raw = D["s1_raw"] | |
| s1_fk = D["s1_fk"] | |
| counts = D["counts"] | |
| thru = D["thru"] | |
| on = D["on"] | |
| fig = plt.figure(figsize=(15, 5.4)) | |
| gs = GridSpec(1, 4, width_ratios=[1, 1, 1, 0.9], wspace=0.28) | |
| def base(ax): | |
| ax.contour(gx, gy, gv, levels=22, colors="0.82", linewidths=0.5, zorder=0) | |
| ax.scatter(*S1, marker="X", s=150, color="black", zorder=6) | |
| ax.scatter(*S2, marker="X", s=150, color="0.5", zorder=6) | |
| ax.scatter(*Axy, marker="*", s=260, color="#228833", ec="white", zorder=7) | |
| ax.scatter(*Bxy, marker="*", s=260, color="#ee6677", ec="white", zorder=7) | |
| ax.set_xlim(xlo, xhi) | |
| ax.set_ylim(ylo, yhi) | |
| ax.set_xlabel("x") | |
| ax = fig.add_subplot(gs[0]) | |
| base(ax) | |
| sc = ax.scatter(cx, cy, c=q[keep], cmap="coolwarm", s=22, zorder=3, vmin=0, vmax=1) | |
| ax.plot( | |
| mep[:, 0], | |
| mep[:, 1], | |
| "--", | |
| color="0.25", | |
| lw=1.6, | |
| zorder=4, | |
| label="min-energy path", | |
| ) | |
| ax.set_ylabel("y") | |
| ax.set_title("a MSM microstates + committor", fontsize=10.5, loc="left") | |
| ax.legend(fontsize=7.5, loc="lower right", frameon=False) | |
| ax.annotate( | |
| "S1", S1, xytext=(S1[0] - 0.13, S1[1] + 0.10), fontsize=8.5, weight="bold" | |
| ) | |
| ax.annotate( | |
| "S2", | |
| S2, | |
| xytext=(S2[0] + 0.06, S2[1] + 0.10), | |
| fontsize=8.5, | |
| color="0.4", | |
| weight="bold", | |
| ) | |
| cax = ax.inset_axes([0.60, 0.90, 0.36, 0.045]) | |
| cb = fig.colorbar(sc, cax=cax, orientation="horizontal") | |
| cb.set_label("committor $q^+$", fontsize=7.5) | |
| cb.ax.tick_params(labelsize=6.5) | |
| ax = fig.add_subplot(gs[1]) | |
| base(ax) | |
| ax.scatter(cx, cy, s=9, color="0.72", zorder=1) | |
| for p in praw[:150]: | |
| pp = xy[list(p)] | |
| ax.plot(pp[:, 0], pp[:, 1], "-", color="#4477aa", lw=0.5, alpha=0.06, zorder=2) | |
| ax.plot(mep[:, 0], mep[:, 1], "--", color="0.35", lw=1.3, zorder=4) | |
| ax.plot( | |
| ch_raw[:, 0], | |
| ch_raw[:, 1], | |
| "-o", | |
| color="#22447a", | |
| lw=2.6, | |
| ms=4, | |
| zorder=5, | |
| label="dominant route (max-flux)", | |
| ) | |
| ax.set_title(f"b Without FK ({s1_raw:.0f}% via S1)", fontsize=10.5, loc="left") | |
| ax.legend(fontsize=7.5, loc="lower right", frameon=False) | |
| ax = fig.add_subplot(gs[2]) | |
| base(ax) | |
| ax.scatter(cx, cy, s=9, color="0.72", zorder=1) | |
| for p in pfk[:150]: | |
| pp = xy[list(p)] | |
| ax.plot(pp[:, 0], pp[:, 1], "-", color="#cc6677", lw=0.5, alpha=0.06, zorder=2) | |
| ax.plot(mep[:, 0], mep[:, 1], "--", color="0.35", lw=1.3, zorder=4) | |
| ax.plot( | |
| ch_fk[:, 0], | |
| ch_fk[:, 1], | |
| "-o", | |
| color="#a03050", | |
| lw=2.6, | |
| ms=4, | |
| zorder=5, | |
| label="dominant route (max-flux)", | |
| ) | |
| ax.set_title(f"c With floored FK ({s1_fk:.0f}% via S1)", fontsize=10.5, loc="left") | |
| ax.legend(fontsize=7.5, loc="lower right", frameon=False) | |
| ax = fig.add_subplot(gs[3]) | |
| ax.plot(arc, Emep, "-", color="0.2", lw=2) | |
| ipk = np.argmax(Emep) | |
| ax.scatter([arc[ipk]], [Emep[ipk]], s=90, color="black", zorder=5) | |
| ax.annotate( | |
| "S1 (rate-limiting)\nbarrier 106 = 6.4 $k_BT$", | |
| (arc[ipk], Emep[ipk]), | |
| xytext=(arc[ipk] - 0.05, Emep[ipk] - 30), | |
| fontsize=8, | |
| ha="center", | |
| arrowprops=dict(arrowstyle="->"), | |
| ) | |
| iS2 = np.argmin(np.linalg.norm(mep - S2, axis=1)) | |
| ax.scatter([arc[iS2]], [Emep[iS2]], s=70, color="0.5", zorder=5) | |
| ax.annotate( | |
| "S2\nbarrier 9 = 0.5 $k_BT$", | |
| (arc[iS2], Emep[iS2]), | |
| xytext=(arc[iS2] - 0.05, Emep[iS2] - 34), | |
| fontsize=8, | |
| color="0.4", | |
| ha="center", | |
| arrowprops=dict(arrowstyle="->", color="0.4"), | |
| ) | |
| ax.set_xlabel("path length A$\\to$B") | |
| ax.set_ylabel("potential energy (MB units)") | |
| ax.set_title("d barrier heights along the MEP", fontsize=10.5, loc="left") | |
| ax.spines[["top", "right"]].set_visible(False) | |
| fig.suptitle( | |
| "The transition is obligated through S1 (d); the raw MSM under-samples it (b), " | |
| "floored FK recovers it (c). $\\tau$=5, N=16, $\\alpha$=1.5, TICA lag 20", | |
| fontsize=11.5, | |
| y=1.0, | |
| ) | |
| fig.tight_layout(rect=[0, 0, 1, 0.95]) | |
| fig.savefig("figures/mb_panels.svg", bbox_inches="tight") | |
| fig.savefig("figures/mb_panels.png", dpi=200, bbox_inches="tight") | |
| fig.savefig("figures/mb_panels.pdf", bbox_inches="tight") | |
| print( | |
| f"[stats] S1 via raw {s1_raw:.0f}% -> FK {s1_fk:.0f}%; MEP peak at S1 (+106 vs S2 +9)" | |
| ) | |
| print("[done] -> figures/mb_panels.svg / .png / .pdf") | |
| def main(): | |
| import argparse | |
| ap = argparse.ArgumentParser(description="Muller-Brown panel figure.") | |
| ap.add_argument( | |
| "--cache-only", action="store_true", help="compute and cache, do not plot" | |
| ) | |
| ap.add_argument( | |
| "--from-cache", | |
| action="store_true", | |
| help="plot from the cache without recomputing (fast; for restyling)", | |
| ) | |
| ap.add_argument("--cache", default=CACHE_NPZ) | |
| a = ap.parse_args() | |
| if a.from_cache: | |
| render(load_cache(a.cache)) | |
| return | |
| D = compute() | |
| save_cache(D, a.cache) | |
| if a.cache_only: | |
| return | |
| render(D) | |
| if __name__ == "__main__": | |
| main() | |