Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions news/md_outputs.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
**Added:**

* <news item>

**Changed:**

* Plain MD simulations now write out PDBs with periodicity shifts consistent
with the simulation XTC file (PR #1302).
* The simulation log file written during plain MD simulations now starts from
zero rather than including the steps from the equilibration.

**Deprecated:**

* <news item>

**Removed:**

* <news item>

**Fixed:**

* <news item>

**Security:**

* <news item>
114 changes: 75 additions & 39 deletions openfe/protocols/openmm_md/plain_md_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import time
import mdtraj
from mdtraj.reporters import XTCReporter
import numpy.typing as npt
from openfe.utils import without_oechem_backend, log_system_probe
from gufe import (
settings,
Expand Down Expand Up @@ -267,6 +268,40 @@ def __init__(
generation=generation
)

@staticmethod
def _save_simulation_pdb(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this method belong in some base class we have in utils file for our openmm based protocols?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eventually I think, but we'll leave that for the "big refactor".

simulation: openmm.app.Simulation,
selection_indices: npt.NDArray,
filepath: pathlib.Path
) -> None:
"""
Helper method to write out PDB for the current state of the simulation.

Parameters
----------
simulation : openmm.app.Simulation
The simulation to save the PDB for.
output_indices : str
A string defining which parts of the system to save an output for.
filepath : pathlib.Path
The full path of the file to be written.
"""
state = simulation.context.getState(
getPositions=True,
enforcePeriodicBox=simulation._usesPBC,
)

positions = to_openmm(from_openmm(state.getPositions()))

# Store subset of atoms, specified in input, as PDB file
mdtraj_top = mdtraj.Topology.from_openmm(simulation.topology)
traj = mdtraj.Trajectory(
positions[selection_indices, :],
mdtraj_top.subset(selection_indices),
)

traj.save_pdb(filepath)

@staticmethod
def _run_MD(simulation: openmm.app.Simulation,
positions: omm_unit.Quantity,
Expand Down Expand Up @@ -326,25 +361,19 @@ def _run_MD(simulation: openmm.app.Simulation,
maxIterations=simulation_settings.minimization_steps
)

# Get the sub selection of the system to save coords for
selection_indices = mdtraj.Topology.from_openmm(
simulation.topology).select(output_settings.output_indices)

positions = to_openmm(from_openmm(
simulation.context.getState(getPositions=True,
enforcePeriodicBox=False
).getPositions()))
# Store subset of atoms, specified in input, as PDB file
mdtraj_top = mdtraj.Topology.from_openmm(simulation.topology)
traj = mdtraj.Trajectory(
positions[selection_indices, :],
mdtraj_top.subset(selection_indices),
)
# Get the sub selection of the system for coord saving
# will be using this going forward
mdtop = mdtraj.Topology.from_openmm(simulation.topology)
selection_indices = mdtop.select(output_settings.output_indices)

# Save a PDB of the minimized structure
if output_settings.minimized_structure:
traj.save_pdb(
PlainMDProtocolUnit._save_simulation_pdb(
simulation,
selection_indices,
shared_basepath / output_settings.minimized_structure
)

# equilibrate
# NVT equilibration
if equil_steps_nvt:
Expand All @@ -366,18 +395,11 @@ def _run_MD(simulation: openmm.app.Simulation,
logger.info(
f"Completed NVT equilibration in {t1 - t0} seconds")

# Save last frame NVT equilibration
positions = to_openmm(
from_openmm(simulation.context.getState(
getPositions=True, enforcePeriodicBox=False
).getPositions()))

traj = mdtraj.Trajectory(
positions[selection_indices, :],
mdtraj_top.subset(selection_indices),
)
# Optionally save last frame NVT equilibration
if output_settings.equil_nvt_structure is not None:
traj.save_pdb(
PlainMDProtocolUnit._save_simulation_pdb(
simulation,
selection_indices,
shared_basepath / output_settings.equil_nvt_structure
)

Expand All @@ -400,24 +422,20 @@ def _run_MD(simulation: openmm.app.Simulation,
f"Completed NPT equilibration in {t1 - t0} seconds")

# Save last frame NPT equilibration
positions = to_openmm(
from_openmm(simulation.context.getState(
getPositions=True, enforcePeriodicBox=False
).getPositions()))

traj = mdtraj.Trajectory(
positions[selection_indices, :],
mdtraj_top.subset(selection_indices),
)
if output_settings.equil_npt_structure is not None:
traj.save_pdb(
PlainMDProtocolUnit._save_simulation_pdb(
simulation,
selection_indices,
shared_basepath / output_settings.equil_npt_structure
)

# production
if verbose:
logger.info("running production phase")

# For the production simulation, we reset the step counter to 0
simulation.context.setStepCount(0)

# Setup the reporters
write_interval = settings_validation.divmod_time_and_check(
output_settings.trajectory_write_interval,
Expand All @@ -433,18 +451,23 @@ def _run_MD(simulation: openmm.app.Simulation,
)

if output_settings.production_trajectory_filename:
simulation.reporters.append(XTCReporter(
xtc_reporter = XTCReporter(
file=str(
shared_basepath /
output_settings.production_trajectory_filename),
output_settings.production_trajectory_filename
),
reportInterval=write_interval,
atomSubset=selection_indices))
atomSubset=selection_indices
)
simulation.reporters.append(xtc_reporter)

if output_settings.checkpoint_storage_filename:
simulation.reporters.append(openmm.app.CheckpointReporter(
file=str(
shared_basepath /
output_settings.checkpoint_storage_filename),
reportInterval=checkpoint_interval))

if output_settings.log_output:
simulation.reporters.append(openmm.app.StateDataReporter(
str(shared_basepath / output_settings.log_output),
Expand All @@ -459,9 +482,19 @@ def _run_MD(simulation: openmm.app.Simulation,
density=True,
speed=True,
))

t0 = time.time()
simulation.step(prod_steps)
t1 = time.time()

# Write the final production frame
if output_settings.production_structure is not None:
PlainMDProtocolUnit._save_simulation_pdb(
simulation,
selection_indices,
shared_basepath / output_settings.production_structure
)

if verbose:
logger.info(f"Completed simulation in {t1 - t0} seconds")

Expand Down Expand Up @@ -700,6 +733,9 @@ def run(self, *, dry=False, verbose=True,
if output_settings.equil_npt_structure:
output['npt_equil_pdb'] = shared_basepath / output_settings.equil_npt_structure

if output_settings.production_structure:
output['production_pdb'] = shared_basepath / output_settings.production_structure

return output
else:
return {'debug': {'system': stateA_system}}
Expand Down
21 changes: 14 additions & 7 deletions openfe/protocols/openmm_utils/omm_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -673,17 +673,24 @@ class Config:
Frequency to write the xtc file. Default 5000 * unit.timestep.
"""
preminimized_structure: Optional[str] = 'system.pdb'
"""Path to the pdb file of the full pre-minimized system.
"""Filename for the pdb file of the full pre-minimized system.
Default 'system.pdb'."""
minimized_structure: Optional[str] = 'minimized.pdb'
"""Path to the pdb file of the system after minimization.
Only the specified atom subset is saved. Default 'minimized.pdb'."""
"""Filename for the pdb file of the system after minimization.
Only the specified atom subset defined by ``output_indices`` is saved.
Default 'minimized.pdb'."""
equil_nvt_structure: Optional[str] = 'equil_nvt.pdb'
"""Path to the pdb file of the system after NVT equilibration.
Only the specified atom subset is saved. Default 'equil_nvt.pdb'."""
"""Filename for the pdb file of the system after NVT equilibration.
Only the specified atom subset defined by ``output_indices`` is saved.
Default 'equil_nvt.pdb'."""
equil_npt_structure: Optional[str] = 'equil_npt.pdb'
"""Path to the pdb file of the system after NPT equilibration.
Only the specified atom subset is saved. Default 'equil_npt.pdb'."""
"""Filename for the pdb file of the system after NPT equilibration.
Only the specified atom subset defined by ``output_indices`` is saved.
Default 'equil_npt.pdb'."""
production_structure: Optional[str] = 'production.pdb'
"""Filename of the pdb file of the system after the production simulation.
Only the specified atom subset defined by ``output_indices`` is saved.
Default to 'production.pdb'."""
log_output: Optional[str] = 'simulation.log'
"""
Filename for writing the log of the MD simulation, including timesteps,
Expand Down
23 changes: 23 additions & 0 deletions openfe/tests/protocols/openmm_md/test_plain_md_slow.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# This code is part of OpenFE and is licensed under the MIT license.
# For details, see https://github.com/OpenFreeEnergy/openfe
import MDAnalysis as mda
from numpy.testing import assert_allclose
import pathlib
import pytest
from openff.units import unit
Expand Down Expand Up @@ -94,6 +96,7 @@
settings.simulation_settings.equilibration_length = 50 * unit.picosecond
settings.simulation_settings.production_length = 100 * unit.picosecond
settings.output_settings.checkpoint_interval = 10 * unit.picosecond
settings.output_settings.trajectory_write_interval = 10 * unit.picosecond

Check warning on line 99 in openfe/tests/protocols/openmm_md/test_plain_md_slow.py

View check run for this annotation

Codecov / codecov/patch

openfe/tests/protocols/openmm_md/test_plain_md_slow.py#L99

Added line #L99 was not covered by tests
settings.engine_settings.compute_platform = platform

prot = openmm_md.PlainMDProtocol(settings)
Expand Down Expand Up @@ -127,6 +130,7 @@
"checkpoint.chk",
"equil_nvt.pdb",
"equil_npt.pdb",
"production.pdb",
"minimized.pdb",
"simulation.xtc",
"simulation.log",
Expand All @@ -142,3 +146,22 @@
assert pur.outputs['last_checkpoint'] == unit_shared / "checkpoint.chk"
assert pur.outputs['nvt_equil_pdb'] == unit_shared / "equil_nvt.pdb"
assert pur.outputs['npt_equil_pdb'] == unit_shared / "equil_npt.pdb"
assert pur.outputs['production_pdb'] == unit_shared / "production.pdb"

Check warning on line 149 in openfe/tests/protocols/openmm_md/test_plain_md_slow.py

View check run for this annotation

Codecov / codecov/patch

openfe/tests/protocols/openmm_md/test_plain_md_slow.py#L149

Added line #L149 was not covered by tests

# Check the final trajectory frame
first_frame = mda.Universe(pur.outputs['npt_equil_pdb'])
final_frame = mda.Universe(pur.outputs['production_pdb'])
simulation = mda.Universe(pur.outputs['minimized_pdb'], pur.outputs['nc'])

Check warning on line 154 in openfe/tests/protocols/openmm_md/test_plain_md_slow.py

View check run for this annotation

Codecov / codecov/patch

openfe/tests/protocols/openmm_md/test_plain_md_slow.py#L152-L154

Added lines #L152 - L154 were not covered by tests

# Check we have the right number of frames
assert len(simulation.trajectory) == 10

Check warning on line 157 in openfe/tests/protocols/openmm_md/test_plain_md_slow.py

View check run for this annotation

Codecov / codecov/patch

openfe/tests/protocols/openmm_md/test_plain_md_slow.py#L157

Added line #L157 was not covered by tests

# Check that the final frame matches
simulation.trajectory[-1] # fast-forward
assert_allclose(

Check warning on line 161 in openfe/tests/protocols/openmm_md/test_plain_md_slow.py

View check run for this annotation

Codecov / codecov/patch

openfe/tests/protocols/openmm_md/test_plain_md_slow.py#L160-L161

Added lines #L160 - L161 were not covered by tests
final_frame.atoms.positions,
simulation.atoms.positions,
rtol=0,
atol=1e-2
)

Loading