ScooBDoob / summarize_trajectory_cv_tilt.py
ynuozhang
Upload ScooBDoob code without binary caches
31d1d63
Raw History Blame Contribute Delete
6.97 kB
#!/usr/bin/env python
import argparse
import json
import itertools
from pathlib import Path
import numpy as np
from scipy.stats import wilcoxon
HERE = Path(__file__).resolve().parent
_ap = argparse.ArgumentParser()
_ap.add_argument(
"--pattern",
default="trajectory_cv_tilt_k*_seed*.json",
help="glob over data/p53 selecting the K/seed runs to pool; the default "
"matches the original site_drug sweep",
)
_ap.add_argument(
"--out", type=Path, default=HERE / "data/p53/trajectory_cv_tilt_robustness.json"
)
_args = _ap.parse_args()
files = sorted((HERE / "data/p53").glob(_args.pattern))
RNG_SEED = 20260729
def mean_bootstrap_ci(x, n_boot=200_000):
"""Trajectory-fold bootstrap interval for the arithmetic mean."""
x = np.asarray(x, float)
rng = np.random.default_rng(RNG_SEED)
idx = rng.integers(0, len(x), size=(n_boot, len(x)))
return np.percentile(x[idx].mean(axis=1), [2.5, 97.5]).tolist()
def exact_signflip_p(x, null=1.0, alternative="two-sided"):
"""Exact paired randomization test aligned to a mean-difference estimand."""
delta = np.asarray(x, float) - null
observed = delta.mean()
signs = np.asarray(list(itertools.product((-1.0, 1.0), repeat=len(delta))))
stats = (signs * delta).mean(axis=1)
if alternative == "greater":
return float(np.mean(stats >= observed - 1e-15))
return float(np.mean(np.abs(stats) >= abs(observed) - 1e-15))
print("TRAJECTORY-HELD-OUT FK ROBUSTNESS")
print(f"{'file':<42}{'mean':>8}{'median':>9}{'>1':>7}{'p':>9}")
all_means = []
for path in files:
d = json.loads(path.read_text())
x = d["summary"]["fk_doob"]
mean = x["mean_ratio_to_equilibrium"]
all_means.append(mean)
print(
f"{path.name:<42}{mean:>8.3f}"
f"{x['median_ratio_to_equilibrium']:>9.3f}"
f"{x['folds_above_1']:>3}/{x['n_folds']:<3}"
f"{x['wilcoxon_p_vs_1']:>9.3g}"
)
if all_means:
print(
f"\nAcross completed discretizations: mean={np.mean(all_means):.3f}, "
f"range=[{np.min(all_means):.3f}, {np.max(all_means):.3f}]"
)
else:
print("\nNo completed robustness outputs.")
if files:
_base = ("equilibrium", "uniform_states", "tpt_flux", "doob_untilted", "fk_doob")
_per_file = [set(json.loads(p.read_text())["folds"][0]["selectors"]) for p in files]
_common = set.intersection(*_per_file) if _per_file else set()
_alg1 = sorted(
(s for s in _common if s.startswith("alg1[a=")),
key=lambda s: float(s.split("=")[1].rstrip("]")),
)
selectors = tuple(s for s in _base if s in _common) + tuple(_alg1)
dropped = sorted((set().union(*_per_file) - _common) if _per_file else [])
if dropped:
print(f"[note] selectors absent from some runs, excluded: {dropped}")
raw = {}
for selector in selectors:
raw[selector] = np.asarray(
[
[
fold["selectors"][selector]["ratio_to_equilibrium"]
for fold in json.loads(path.read_text())["folds"]
]
for path in files
]
)
collapsed = {k: v.mean(axis=0) for k, v in raw.items()}
print("\nINFERENCE AFTER COLLAPSING K/SEED REPEATS WITHIN HELD-OUT FOLD")
print(f"{'selector':<18}{'mean':>8}{'median':>9}{'>1':>7}{'p vs 1':>10}")
aggregate = {}
for selector in selectors:
x = collapsed[selector]
p = (
1.0
if selector == "equilibrium"
else float(wilcoxon(x - 1, alternative="two-sided").pvalue)
)
print(
f"{selector:<18}{x.mean():>8.3f}{np.median(x):>9.3f}"
f"{int((x > 1).sum()):>3}/{len(x):<3}{p:>10.3g}"
)
aggregate[selector] = {
"mean": float(x.mean()),
"mean_fold_bootstrap_95ci": (
[1.0, 1.0] if selector == "equilibrium" else mean_bootstrap_ci(x)
),
"median": float(np.median(x)),
"n_positive": int((x > 1).sum()),
"n_folds": int(len(x)),
"wilcoxon_p_vs_1": p,
"exact_mean_signflip_p_two_sided": (
1.0
if selector == "equilibrium"
else exact_signflip_p(x, null=1.0, alternative="two-sided")
),
"exact_mean_signflip_p_one_sided_greater": (
1.0
if selector == "equilibrium"
else exact_signflip_p(x, null=1.0, alternative="greater")
),
"fold_values": x.tolist(),
}
if _alg1:
print(
"\nALGORITHM 1 TILT-STRENGTH SWEEP (w=exp(-alpha_rho*rho), no committor shaping)"
)
print(
f"{'alpha_rho':>10}{'mean':>8}{'median':>9}{'>1':>7}{'95% CI':>18}{'p vs 1':>10}"
)
for s in _alg1:
a = aggregate[s]
ci = a["mean_fold_bootstrap_95ci"]
star = " *" if (ci[0] > 1 or ci[1] < 1) else ""
print(
f"{float(s.split('=')[1].rstrip(']')):>10.2f}{a['mean']:>8.3f}"
f"{a['median']:>9.3f}{a['n_positive']:>3}/{a['n_folds']:<3}"
f"{f'[{ci[0]:.3f}, {ci[1]:.3f}]':>18}{a['wilcoxon_p_vs_1']:>10.3g}{star}"
)
print(" * = 95% CI excludes 1. alpha_rho=0 is the no-density-tilt control.")
print("\nPAIRED FK ADVANTAGE")
fk = collapsed["fk_doob"]
paired = {}
for comparator in ("tpt_flux", "uniform_states", "doob_untilted"):
delta = fk - collapsed[comparator]
p = float(wilcoxon(delta, alternative="two-sided").pvalue)
print(
f"FK - {comparator:<14} mean={delta.mean():+.3f}; "
f"wins={int((delta > 0).sum())}/{len(delta)}; p={p:.3g}"
)
paired[comparator] = {
"mean_difference": float(delta.mean()),
"mean_difference_fold_bootstrap_95ci": mean_bootstrap_ci(delta),
"fk_wins": int((delta > 0).sum()),
"n_folds": int(len(delta)),
"wilcoxon_p": p,
"exact_mean_signflip_p_two_sided": exact_signflip_p(
delta, null=0.0, alternative="two-sided"
),
"exact_mean_signflip_p_one_sided_greater": exact_signflip_p(
delta, null=0.0, alternative="greater"
),
}
out = {
"n_configurations": len(files),
"configuration_mean_range": [
float(np.min(all_means)),
float(np.max(all_means)),
],
"aggregate": aggregate,
"paired_fk_advantage": paired,
"note": "K/seed configurations are averaged within fold before inference.",
}
out_path = _args.out
out["source_pattern"] = _args.pattern
out["source_files"] = [p.name for p in files]
out["observable"] = sorted(
{json.loads(p.read_text())["config"]["observable"] for p in files}
)
out_path.write_text(json.dumps(out, indent=2))
print(f"\n-> {out_path}")