From 05ca87c48942fb19a575b048e5e9db06a1cd4963 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Wed, 4 Mar 2026 13:45:58 +0000 Subject: [PATCH 1/5] Add method to return current energy trajectory records. --- doc/source/changelog.rst | 2 ++ src/sire/mol/_dynamics.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index 6f8d469d7..e3842d002 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -21,6 +21,8 @@ organisation on `GitHub `__. incorrectly for systems containing more than one molecule with CMAP terms (e.g. multi-chain glycoproteins). +* Add convenience function to ``sire.mol.dynamics`` to get current energy trajectory records. + `2025.4.0 `__ - February 2026 --------------------------------------------------------------------------------------------- diff --git a/src/sire/mol/_dynamics.py b/src/sire/mol/_dynamics.py index 3e6e2dfb0..c28175c5f 100644 --- a/src/sire/mol/_dynamics.py +++ b/src/sire/mol/_dynamics.py @@ -525,6 +525,9 @@ def _exit_dynamics_block( self._current_time, nrgs, {"lambda": str(sim_lambda_value)} ) + # Store the current energies. + self._nrgs = nrgs + # update the interpolation lambda value if self._is_interpolate: if delta_lambda: @@ -861,6 +864,12 @@ def current_kinetic_energy(self): def energy_trajectory(self): return self._energy_trajectory.clone() + def current_energies(self): + try: + return self._nrgs + except Exception: + return {} + def step(self, num_steps: int = 1): """ Just perform 'num_steps' steps of dynamics, without saving @@ -2195,6 +2204,12 @@ def energy_trajectory(self, to_pandas: bool = False, to_alchemlyb: bool = False) else: return t + def current_energies(self): + """ + Return a dictionary of the most recent energy trajectory entry. + """ + return self._d.current_energies() + def to_xml(self, f=None): """ Save the current state of the dynamics to XML. From de8bbf8455b5d1de4476713d8a35a6435c8bfc66 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Wed, 4 Mar 2026 19:08:15 +0000 Subject: [PATCH 2/5] Sort energies by lambda. --- src/sire/mol/_dynamics.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/sire/mol/_dynamics.py b/src/sire/mol/_dynamics.py index c28175c5f..dd12da030 100644 --- a/src/sire/mol/_dynamics.py +++ b/src/sire/mol/_dynamics.py @@ -475,7 +475,7 @@ def _exit_dynamics_block( nrg += self._pressure * volume if excess_chemical_potential is not None: nrg += excess_chemical_potential * num_waters - nrgs[str(sim_lambda_value)] = nrg * kcal_per_mol + nrgs[f"{sim_lambda_value:.5f}"] = nrg * kcal_per_mol if lambda_windows is not None: # get the index of the simulation lambda value in the @@ -494,6 +494,7 @@ def _exit_dynamics_block( not has_lambda_index or abs(lambda_index - i) <= num_energy_neighbours ): + key = f"{lambda_value:.5f}" self._omm_mols.set_lambda( lambda_value, rest2_scale=rest2_scale, @@ -506,9 +507,9 @@ def _exit_dynamics_block( nrg += self._pressure * volume if excess_chemical_potential is not None: nrg += excess_chemical_potential * num_waters - nrgs[str(lambda_value)] = nrg * kcal_per_mol + nrgs[key] = nrg * kcal_per_mol else: - nrgs[str(lambda_value)] = null_energy * kcal_per_mol + nrgs[key] = null_energy * kcal_per_mol self._omm_mols.set_lambda( sim_lambda_value, @@ -866,7 +867,10 @@ def energy_trajectory(self): def current_energies(self): try: - return self._nrgs + # Sort the energies by key to ensure consistent ordering. + nrgs = self._nrgs.copy() + nrgs = dict(sorted(nrgs.items(), key=lambda item: item[0])) + return nrgs except Exception: return {} From 6b378bcd769a5a3c4b262a91047c66e76921a173 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Thu, 5 Mar 2026 09:19:00 +0000 Subject: [PATCH 3/5] Store energies in lambda order to begin with. --- src/sire/mol/_dynamics.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/sire/mol/_dynamics.py b/src/sire/mol/_dynamics.py index dd12da030..2ac606a2d 100644 --- a/src/sire/mol/_dynamics.py +++ b/src/sire/mol/_dynamics.py @@ -1,4 +1,4 @@ -__all__ = ["Dynamics"] +__all__ = ["Dynamics": class DynamicsData: @@ -475,7 +475,8 @@ def _exit_dynamics_block( nrg += self._pressure * volume if excess_chemical_potential is not None: nrg += excess_chemical_potential * num_waters - nrgs[f"{sim_lambda_value:.5f}"] = nrg * kcal_per_mol + # Store the potential energy for the current lambda value. + nrg_sim_lambda_value = nrg if lambda_windows is not None: # get the index of the simulation lambda value in the @@ -510,6 +511,10 @@ def _exit_dynamics_block( nrgs[key] = nrg * kcal_per_mol else: nrgs[key] = null_energy * kcal_per_mol + else: + nrgs[f"{sim_lambda_value:.5f}"] = ( + nrg_sim_lambda_value * kcal_per_mol + ) self._omm_mols.set_lambda( sim_lambda_value, @@ -865,12 +870,15 @@ def current_kinetic_energy(self): def energy_trajectory(self): return self._energy_trajectory.clone() - def current_energies(self): + def current_energies(self, sort: bool = False): try: - # Sort the energies by key to ensure consistent ordering. - nrgs = self._nrgs.copy() - nrgs = dict(sorted(nrgs.items(), key=lambda item: item[0])) - return nrgs + if sort: + nrgs = self._nrgs.copy() + sorted_items = sorted(list(nrgs.items())[2:], key=lambda x: x[0]) + nrgs = dict(list(nrgs.items())[:2] + sorted_items) + return nrgs + else: + return self._nrgs except Exception: return {} @@ -2208,11 +2216,12 @@ def energy_trajectory(self, to_pandas: bool = False, to_alchemlyb: bool = False) else: return t - def current_energies(self): + def current_energies(self, sort: bool = False): """ Return a dictionary of the most recent energy trajectory entry. + If 'sort' is True, then the dictionary will be sorted by key. """ - return self._d.current_energies() + return self._d.current_energies(sort=sort) def to_xml(self, f=None): """ From 8098fee38325081e35115854c3e0b332f03bd688 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Thu, 5 Mar 2026 09:39:44 +0000 Subject: [PATCH 4/5] Fix accidental character deletion. --- src/sire/mol/_dynamics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sire/mol/_dynamics.py b/src/sire/mol/_dynamics.py index 2ac606a2d..180fe26d7 100644 --- a/src/sire/mol/_dynamics.py +++ b/src/sire/mol/_dynamics.py @@ -1,4 +1,4 @@ -__all__ = ["Dynamics": +__all__ = ["Dynamics"] class DynamicsData: From 2049dec4217dbf5a8835882fe05645ddab10620c Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Thu, 5 Mar 2026 16:12:02 +0000 Subject: [PATCH 5/5] Just return a raw NumPy array of floats. --- src/sire/mol/_dynamics.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/sire/mol/_dynamics.py b/src/sire/mol/_dynamics.py index 180fe26d7..7f9ece5c5 100644 --- a/src/sire/mol/_dynamics.py +++ b/src/sire/mol/_dynamics.py @@ -434,6 +434,7 @@ def _exit_dynamics_block( if save_energy: # should save energy here nrgs = {} + nrgs_array = [] nrgs["kinetic"] = ( self._omm_state.getKineticEnergy().value_in_unit( @@ -509,12 +510,15 @@ def _exit_dynamics_block( if excess_chemical_potential is not None: nrg += excess_chemical_potential * num_waters nrgs[key] = nrg * kcal_per_mol + nrgs_array.append(nrg) else: + nrgs_array.append(null_energy) nrgs[key] = null_energy * kcal_per_mol else: nrgs[f"{sim_lambda_value:.5f}"] = ( nrg_sim_lambda_value * kcal_per_mol ) + nrgs_array.append(nrg_sim_lambda_value) self._omm_mols.set_lambda( sim_lambda_value, @@ -533,6 +537,7 @@ def _exit_dynamics_block( # Store the current energies. self._nrgs = nrgs + self._nrgs_array = nrgs_array # update the interpolation lambda value if self._is_interpolate: @@ -870,17 +875,13 @@ def current_kinetic_energy(self): def energy_trajectory(self): return self._energy_trajectory.clone() - def current_energies(self, sort: bool = False): + def _current_energy_array(self): try: - if sort: - nrgs = self._nrgs.copy() - sorted_items = sorted(list(nrgs.items())[2:], key=lambda x: x[0]) - nrgs = dict(list(nrgs.items())[:2] + sorted_items) - return nrgs - else: - return self._nrgs + import numpy as np + + return np.array(self._nrgs_array) except Exception: - return {} + return None def step(self, num_steps: int = 1): """ @@ -2216,12 +2217,12 @@ def energy_trajectory(self, to_pandas: bool = False, to_alchemlyb: bool = False) else: return t - def current_energies(self, sort: bool = False): + def _current_energy_array(self): """ - Return a dictionary of the most recent energy trajectory entry. - If 'sort' is True, then the dictionary will be sorted by key. + Return the current energies as a numpy array, in the same order + as the energy trajectory columns. """ - return self._d.current_energies(sort=sort) + return self._d._current_energy_array() def to_xml(self, f=None): """