Skip to content

Commit 6f6c7d6

Browse files
committed
Update perturbable CMAP test to use actual mutation.
1 parent 301b9a2 commit 6f6c7d6

1 file changed

Lines changed: 46 additions & 63 deletions

File tree

tests/convert/test_openmm_cmap.py

Lines changed: 46 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -86,22 +86,24 @@ def test_openmm_cmap_energy(tmpdir, multichain_cmap, openmm_platform):
8686
"openmm" not in sr.convert.supported_formats(),
8787
reason="openmm support is not available",
8888
)
89-
def test_openmm_cmap_perturbable(multichain_cmap, openmm_platform):
89+
def test_openmm_cmap_perturbable(openmm_platform):
9090
"""
91-
Verify that CMAPTorsionForce grids are correctly handled for a perturbable
92-
molecule. The pre-merged stream file merged_molecule_cmap.s3 contains a
93-
perturbable molecule whose two end states are identical (an identity
94-
perturbation of a CHARMM protein chain), so both end states carry the same
95-
CMAP backbone correction terms. The test checks that the perturbable code
96-
path correctly applies the same grids at all lambda values and that
97-
set_lambda does not corrupt the force parameters.
91+
Verify that CMAPTorsionForce grids are correctly updated by the lambda lever
92+
for a perturbable molecule with a genuine single-residue mutation.
93+
94+
The pre-merged stream file merged_molecule_cmap.s3 contains a perturbable
95+
ubiquitin chain with a T9A mutation. cmap0 and cmap1 differ for exactly
96+
one torsion (the one centred on the mutated residue). The test checks that:
97+
98+
- CMAP torsions are present at both end states.
99+
- At least one torsion grid differs between lambda=0 and lambda=1.
100+
- Most torsion grids are unchanged (only the mutated residue is affected).
98101
"""
99102
import openmm
103+
import openmm.unit
100104

101105
platform_name = openmm_platform or "CPU"
102106

103-
mol0 = multichain_cmap[0]
104-
105107
mols_pert = sr.load_test_files("merged_molecule_cmap.s3")
106108
mols_pert = sr.morph.link_to_reference(mols_pert)
107109

@@ -113,14 +115,8 @@ def test_openmm_cmap_perturbable(multichain_cmap, openmm_platform):
113115
}
114116

115117
def get_cmap_torsion_grids(context):
116-
"""
117-
Return list of (size, grid) for each CMAP torsion, dereferencing the
118-
map index. Grid values are returned as plain floats (kJ/mol) so that
119-
pytest.approx can compare them. This is map-count-agnostic: the
120-
non-perturbable path deduplicates maps while the perturbable path
121-
allocates one map per torsion, but the per-torsion grid values must
122-
agree.
123-
"""
118+
"""Return list of (size, grid) for each CMAP torsion, dereferencing
119+
the map index. Grid values are plain floats (kJ/mol)."""
124120
system = context.getSystem()
125121
for force in system.getForces():
126122
if isinstance(force, openmm.CMAPTorsionForce):
@@ -138,50 +134,37 @@ def get_cmap_torsion_grids(context):
138134
return result
139135
return []
140136

141-
def unique_grids(torsion_grids, decimals=3):
142-
"""
143-
Return the sorted set of unique (size, rounded-grid) tuples.
144-
145-
Torsion ordering can differ between the perturbable and non-perturbable
146-
code paths, so we compare the sets of unique grid shapes rather than
147-
comparing torsion-by-torsion.
148-
"""
149-
seen = set()
150-
result = []
151-
for size, grid in torsion_grids:
152-
key = (size, tuple(round(v, decimals) for v in grid))
153-
if key not in seen:
154-
seen.add(key)
155-
result.append(key)
156-
return sorted(result)
157-
158-
# Reference: non-perturbable molecule.
159-
mols_ref = sr.system.System()
160-
mols_ref.add(mol0)
161-
omm_ref = sr.convert.to(mols_ref, "openmm", map=omm_map)
162-
ref_torsion_grids = get_cmap_torsion_grids(omm_ref)
163-
164-
assert len(ref_torsion_grids) > 0, "Reference context has no CMAP torsions"
165-
ref_unique = unique_grids(ref_torsion_grids)
166-
167-
# Perturbable context — one context, lambda changed in place.
168137
omm_pert = sr.convert.to(mols_pert, "openmm", map=omm_map)
169138

170-
# At both lambda=0 and lambda=1 the set of unique CMAP grids must match the
171-
# non-perturbable reference (cmap0 == cmap1 for an identity perturbation).
172-
# We compare sets of unique grids because the perturbable and non-perturbable
173-
# code paths may order torsions differently.
174-
for lam in (0.0, 1.0):
175-
omm_pert.set_lambda(lam)
176-
pert_torsion_grids = get_cmap_torsion_grids(omm_pert)
177-
178-
assert len(pert_torsion_grids) == len(ref_torsion_grids), (
179-
f"Wrong number of CMAP torsions at lambda={lam}: "
180-
f"{len(pert_torsion_grids)} != {len(ref_torsion_grids)}"
181-
)
182-
183-
pert_unique = unique_grids(pert_torsion_grids)
184-
assert pert_unique == ref_unique, (
185-
f"Set of unique CMAP grids differs from reference at lambda={lam}: "
186-
f"{len(pert_unique)} unique grids vs {len(ref_unique)} in reference"
187-
)
139+
omm_pert.set_lambda(0.0)
140+
grids_lam0 = get_cmap_torsion_grids(omm_pert)
141+
142+
omm_pert.set_lambda(1.0)
143+
grids_lam1 = get_cmap_torsion_grids(omm_pert)
144+
145+
assert len(grids_lam0) > 0, "No CMAP torsions at lambda=0"
146+
assert len(grids_lam1) == len(
147+
grids_lam0
148+
), f"Torsion count differs between end states: {len(grids_lam0)} vs {len(grids_lam1)}"
149+
150+
differing = sum(
151+
1
152+
for (s0, g0), (s1, g1) in zip(grids_lam0, grids_lam1)
153+
if s0 != s1 or any(round(a, 3) != round(b, 3) for a, b in zip(g0, g1))
154+
)
155+
156+
assert differing > 0, (
157+
"Expected at least one torsion grid to differ between lambda=0 and lambda=1 "
158+
"for a genuine single-residue mutation"
159+
)
160+
assert differing < len(
161+
grids_lam0
162+
), "Expected most torsion grids to be unchanged for a single-residue mutation"
163+
164+
# Verify that changed_cmaps() correctly identifies the same set of
165+
# differing torsions as the direct grid comparison above.
166+
p_omm = mols_pert.molecule(0).perturbation().to_openmm(map=omm_map)
167+
changed = p_omm.changed_cmaps()
168+
assert (
169+
len(changed) == differing
170+
), f"changed_cmaps() returned {len(changed)} torsions but expected {differing}"

0 commit comments

Comments
 (0)