Skip to content

Commit 88860e6

Browse files
committed
tests/fep: replica-exchange opt-in path regression (5/5)
Pins the new use_replica_exchange flag end-to-end: - signature checks on compute_hydration_dg + compute_absolute_ binding_dg + sample_alchemical_windows (default False, opt-in) - CLI: cellsim fep-binding bench --replica-exchange surfaces - end-to-end: methane vacuum at 3 windows × 5 iter × 50 steps on the RE path runs cleanly and returns finite dG (~0 since methane has no intra nonbondeds) Wired into smoke.yml. Without this, a refactor could silently drop the RE path and we wouldn't notice until the next overnight bench failed.
1 parent 3ea81cf commit 88860e6

2 files changed

Lines changed: 120 additions & 0 deletions

File tree

.github/workflows/smoke.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,9 @@ jobs:
166166
- name: fep wall-time estimator (GPU-cost preview anchors stay honest)
167167
run: python -u tests/fep/test_wall_estimator_smoke.py
168168

169+
- name: fep replica-exchange opt-in path (5/5)
170+
run: python -u tests/fep/test_replica_exchange_smoke.py
171+
169172
- name: fep end-to-end smoke (Layer 1.3, methane hydration ΔG pipeline)
170173
run: python -u tests/fep/test_hydration_dg_smoke.py
171174

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
"""Regression test for the opt-in Hamiltonian replica exchange
2+
path in sample_alchemical_windows.
3+
4+
Why: the hand-rolled independent-replica path fails MBAR overlap
5+
on tight intramolecular charge networks (acetic_acid, acetamide,
6+
biotin/streptavidin). The replica-exchange path (opt-in via
7+
use_replica_exchange=True) is the fix. Without a regression test
8+
pinning that path, a future refactor could silently break it and
9+
we wouldn't notice until the next overnight bench failed."""
10+
from __future__ import annotations
11+
12+
import math
13+
import sys
14+
from pathlib import Path
15+
16+
REPO_ROOT = Path(__file__).resolve().parents[2]
17+
sys.path.insert(0, str(REPO_ROOT))
18+
19+
20+
def test_replica_exchange_flag_wired_through_compute_hydration_dg():
21+
"""compute_hydration_dg must accept use_replica_exchange and
22+
pass it down. Code-level inspection, no MD."""
23+
import inspect
24+
from src.fep import compute_hydration_dg
25+
sig = inspect.signature(compute_hydration_dg)
26+
assert "use_replica_exchange" in sig.parameters
27+
assert sig.parameters["use_replica_exchange"].default is False
28+
29+
30+
def test_replica_exchange_flag_wired_through_compute_binding_dg():
31+
"""compute_absolute_binding_dg must accept use_replica_exchange."""
32+
import inspect
33+
from src.fep.binding import compute_absolute_binding_dg
34+
sig = inspect.signature(compute_absolute_binding_dg)
35+
assert "use_replica_exchange" in sig.parameters
36+
assert sig.parameters["use_replica_exchange"].default is False
37+
38+
39+
def test_sample_alchemical_windows_accepts_replica_exchange_flag():
40+
import inspect
41+
from src.fep.sampling import sample_alchemical_windows
42+
sig = inspect.signature(sample_alchemical_windows)
43+
assert "use_replica_exchange" in sig.parameters
44+
assert sig.parameters["use_replica_exchange"].default is False
45+
46+
47+
def test_replica_exchange_runs_methane_vacuum_end_to_end():
48+
"""Run sample_alchemical_windows with use_replica_exchange=True
49+
on methane vacuum. Tiny sampling (3 windows × 5 iter × 50 steps,
50+
~30 s CPU). Just verifies the pipeline runs end-to-end on the
51+
opt-in path; numerical accuracy is irrelevant at this budget."""
52+
from src.fep import _build_alchemical_legs
53+
from src.fep.sampling import sample_alchemical_windows
54+
55+
(vac_alch, _solv_alch, vac_top, _solv_top,
56+
vac_pos, _solv_pos, _n) = _build_alchemical_legs("C")
57+
58+
r = sample_alchemical_windows(
59+
vac_alch, vac_top, vac_pos,
60+
n_windows=3,
61+
n_equilibration_steps=20,
62+
n_production_steps=250,
63+
sample_stride=50,
64+
seed=1,
65+
use_replica_exchange=True,
66+
)
67+
print(f" {r.summary()}")
68+
assert r.ok, (
69+
f"replica-exchange path must run end-to-end on methane "
70+
f"vacuum; got: {r.reason}")
71+
assert r.dG_kcalmol is not None
72+
assert math.isfinite(r.dG_kcalmol)
73+
assert math.isfinite(r.dG_uncertainty_kcalmol)
74+
# Methane vacuum (no intramolecular nonbondeds) → dG ≈ 0 at
75+
# ANY sampling budget. Generous bound for noise.
76+
assert abs(r.dG_kcalmol) < 5.0, (
77+
f"methane vacuum dG should be ~0 (no intra nonbondeds); "
78+
f"got {r.dG_kcalmol:+.3f}")
79+
80+
81+
def test_cli_bench_has_replica_exchange_flag():
82+
"""`cellsim fep-binding bench --help` must expose
83+
--replica-exchange so biologists can enable the fix without
84+
editing code."""
85+
import subprocess
86+
r = subprocess.run(
87+
["python", "-m", "src.fep.binding", "bench", "--help"],
88+
cwd=REPO_ROOT, capture_output=True, text=True)
89+
assert r.returncode == 0, r.stderr
90+
assert "--replica-exchange" in r.stdout, (
91+
f"--replica-exchange missing from bench --help; got:\n"
92+
f"{r.stdout[:1000]}")
93+
94+
95+
if __name__ == "__main__":
96+
funcs = [
97+
test_replica_exchange_flag_wired_through_compute_hydration_dg,
98+
test_replica_exchange_flag_wired_through_compute_binding_dg,
99+
test_sample_alchemical_windows_accepts_replica_exchange_flag,
100+
test_cli_bench_has_replica_exchange_flag,
101+
test_replica_exchange_runs_methane_vacuum_end_to_end,
102+
]
103+
fails = []
104+
for f in funcs:
105+
try:
106+
f()
107+
print(f"[PASS] {f.__name__}")
108+
except AssertionError as e:
109+
print(f"[FAIL] {f.__name__}: {e}")
110+
fails.append(f.__name__)
111+
except Exception as e:
112+
import traceback
113+
traceback.print_exc()
114+
print(f"[ERROR] {f.__name__}: {e}")
115+
fails.append(f.__name__)
116+
print(f"{len(funcs) - len(fails)}/{len(funcs)} PASS")
117+
sys.exit(0 if not fails else 1)

0 commit comments

Comments
 (0)