diff --git a/src/somd2/runner/_repex.py b/src/somd2/runner/_repex.py index 523e7d7..92f999b 100644 --- a/src/somd2/runner/_repex.py +++ b/src/somd2/runner/_repex.py @@ -1242,6 +1242,22 @@ def _run_block( # Get the dynamics object (and GCMC sampler). dynamics, gcmc_sampler = self._dynamics_cache.get(index) + # Perform the GCMC move before dynamics so that the energies + # computed during dynamics are consistent with the state used + # for replica exchange mixing. + if gcmc_sampler is not None and is_gcmc: + gcmc_sampler.push() + try: + _logger.info(f"Performing GCMC move at {_lam_sym} = {lam:.5f}") + gcmc_sampler.move(dynamics.context()) + finally: + gcmc_sampler.pop() + + # Write ghost residues immediately after the GCMC move so the + # ghost state and frame (saved during dynamics) are consistent. + if write_gcmc_ghosts: + gcmc_sampler.write_ghost_residues() + _logger.info(f"Running dynamics at {_lam_sym} = {lam:.5f}") # Draw new velocities from the Maxwell-Boltzmann distribution. @@ -1272,27 +1288,10 @@ def _run_block( ) if gcmc_sampler is not None: - # Write ghost residues before the GCMC move so the ghost state - # is consistent with the saved frame (which is also captured - # before the GCMC move). - if write_gcmc_ghosts: - gcmc_sampler.write_ghost_residues() - - if is_gcmc: - # Push the PyCUDA context on top of the stack. - gcmc_sampler.push() - try: - # Perform the GCMC move. - _logger.info(f"Performing GCMC move at {_lam_sym} = {lam:.5f}") - gcmc_sampler.move(dynamics.context()) - finally: - # Remove the PyCUDA context from the stack. - gcmc_sampler.pop() - # Save the GCMC state. self._dynamics_cache.save_gcmc_state(index) - # Save the OpenMM state after any GCMC move so the context is consistent. + # Save the OpenMM state. self._dynamics_cache.save_openmm_state(index) # Get the energy at each lambda value. diff --git a/src/somd2/runner/_runner.py b/src/somd2/runner/_runner.py index 5fa0fc8..32905b7 100644 --- a/src/somd2/runner/_runner.py +++ b/src/somd2/runner/_runner.py @@ -722,6 +722,29 @@ def generate_lam_vals(lambda_base, increment=0.001): # Loop until we reach the runtime. while runtime < checkpoint_frequency: + # Perform a GCMC move before dynamics so the ghost + # state is consistent with the energies computed + # during dynamics. + _logger.info( + f"Performing GCMC move at {_lam_sym} = {lambda_value:.5f}" + ) + gcmc_sampler.push() + try: + gcmc_sampler.move(dynamics.context()) + finally: + gcmc_sampler.pop() + + # Write ghost residues immediately after the GCMC + # move if a frame will be saved in the upcoming + # dynamics block. + if ( + save_frames + and runtime + self._config.energy_frequency + >= next_frame + ): + gcmc_sampler.write_ghost_residues() + next_frame += self._config.frame_frequency + # Run the dynamics in blocks of the GCMC frequency. dynamics.run( self._config.gcmc_frequency, @@ -748,23 +771,6 @@ def generate_lam_vals(lambda_base, increment=0.001): # Update the runtime. runtime += self._config.energy_frequency - # If a frame is saved, write the ghost residue indices - # before the GCMC move so the ghost state is consistent - # with the saved frame. - if save_frames and runtime >= next_frame: - gcmc_sampler.write_ghost_residues() - next_frame += self._config.frame_frequency - - # Perform a GCMC move. - _logger.info( - f"Performing GCMC move at {_lam_sym} = {lambda_value:.5f}" - ) - gcmc_sampler.push() - try: - gcmc_sampler.move(dynamics.context()) - finally: - gcmc_sampler.pop() - else: dynamics.run( checkpoint_frequency, @@ -948,7 +954,29 @@ def generate_lam_vals(lambda_base, increment=0.001): next_frame = self._config.frame_frequency # Loop until we reach the runtime. - while runtime <= time: + while runtime < time: + # Perform a GCMC move before dynamics so the ghost + # state is consistent with the energies computed + # during dynamics. + _logger.info( + f"Performing GCMC move at {_lam_sym} = {lambda_value:.5f}" + ) + gcmc_sampler.push() + try: + gcmc_sampler.move(dynamics.context()) + finally: + gcmc_sampler.pop() + + # Write ghost residues immediately after the GCMC + # move if a frame will be saved in the upcoming + # dynamics block. + if ( + save_frames + and runtime + self._config.energy_frequency >= next_frame + ): + gcmc_sampler.write_ghost_residues() + next_frame += self._config.frame_frequency + # Run the dynamics in blocks of the GCMC frequency. dynamics.run( self._config.gcmc_frequency, @@ -963,24 +991,8 @@ def generate_lam_vals(lambda_base, increment=0.001): save_crash_report=self._config.save_crash_report, ) - # Perform a GCMC move. - _logger.info( - f"Performing GCMC move at {_lam_sym} = {lambda_value:.5f}" - ) - gcmc_sampler.push() - try: - gcmc_sampler.move(dynamics.context()) - finally: - gcmc_sampler.pop() - # Update the runtime. runtime += self._config.energy_frequency - - # If a frame is saved, then we need to save current indices - # of the ghost water residues. - if save_frames and runtime >= next_frame: - gcmc_sampler.write_ghost_residues() - next_frame += self._config.frame_frequency else: dynamics.run( time,