From d510731e2a6b503f6adc2eca022c5a13cb9f22a1 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Fri, 4 Sep 2026 14:06:13 +0100 Subject: [PATCH] Backport fix from PR #473. [ci skip] --- src/sire/mol/_dynamics.py | 12 ++++-------- tests/mol/test_dynamics.py | 39 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/src/sire/mol/_dynamics.py b/src/sire/mol/_dynamics.py index 4f3dd2ece..afb856e05 100644 --- a/src/sire/mol/_dynamics.py +++ b/src/sire/mol/_dynamics.py @@ -415,6 +415,10 @@ def _exit_dynamics_block( self._omm_state = self._omm_mols.getState(getEnergy=True) self._omm_state_has_cv = (False, False) + # dynamics has advanced the positions without going through + # setPositions(), so the context's energy cache is stale + self._omm_mols.clear_energy_cache() + current_time = ( self._omm_state.getTime().value_in_unit(openmm.unit.nanosecond) * nanosecond ) @@ -481,10 +485,6 @@ def _exit_dynamics_block( nrg_sim_lambda_value = nrg if lambda_windows is not None: - # Positions have just changed (dynamics completed), so - # invalidate all cached per-group energies before the scan. - self._omm_mols.clear_energy_cache() - # get the index of the simulation lambda value in the # lambda windows list try: @@ -544,10 +544,6 @@ def _exit_dynamics_block( self._nrgs = nrgs self._nrgs_array = nrgs_array - # Repex synchronisation point: a peer replica may push new - # positions into this context, so the cache must be invalidated. - self._omm_mols.clear_energy_cache() - # update the interpolation lambda value if self._is_interpolate: if delta_lambda: diff --git a/tests/mol/test_dynamics.py b/tests/mol/test_dynamics.py index 9d6c43b48..7ba6b4c4f 100644 --- a/tests/mol/test_dynamics.py +++ b/tests/mol/test_dynamics.py @@ -196,3 +196,42 @@ def test_crash_report(merged_ethane_methanol, openmm_platform): finally: # Change back to the old directory. os.chdir(old_dir) + + +@pytest.mark.skipif( + "openmm" not in sr.convert.supported_formats(), + reason="openmm support is not available", +) +def test_energy_cache_cleared_after_dynamics(ala_mols): + """ + The context's energy cache must be invalidated after every dynamics block, + not just one that saved energies. The integrator advances the positions + without going through setPositions(), so nothing else clears it. + """ + import openmm + + mols = ala_mols + + d = mols.dynamics(timestep="1fs", temperature="300K", platform="Reference") + + def direct(): + return ( + d.context() + .getState(getEnergy=True) + .getPotentialEnergy() + .value_in_unit(openmm.unit.kilocalorie_per_mole) + ) + + assert d.current_potential_energy().value() == pytest.approx(direct()) + + # A block that doesn't record an energy. + d.run("50fs") + assert d.current_potential_energy().value() == pytest.approx(direct()) + + # A block that does. + d.run("50fs", energy_frequency="10fs") + assert d.current_potential_energy().value() == pytest.approx(direct()) + + # A block that doesn't, again, now that a trajectory exists. + d.run("50fs") + assert d.current_potential_energy().value() == pytest.approx(direct())