Skip to content

Commit

Permalink
Implement take_snapshot and restore_snapshot APIs
Browse files Browse the repository at this point in the history
Per the discussion in #819, users find it confusing that the
snapshot property access returns copies and does not allow direct
modification. Make these explicit methods so that the intent
of the design is clear as is the performance implications.

Also, prevent the type names from changing when restoring
snapshots. Allowing the names to change would lead to parameters
for one type applying to another.
  • Loading branch information
joaander committed Jul 28, 2021
1 parent 2cd75f7 commit 61badf5
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 49 deletions.
4 changes: 0 additions & 4 deletions hoomd/md/pytest/test_gsd.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,6 @@ def test_write_gsd_dynamic(simulation_factory, create_md_sim, tmp_path):

# test dynamic=['attribute']
if snap.communicator.rank == 0:
snap.particles.types = ['t3', 't4']
snap.particles.typeid[:] = N_particles * [1]
snap.particles.mass[:] = N_particles * [0.8]
snap.particles.diameter[:] = N_particles * [0.2]
Expand All @@ -319,7 +318,6 @@ def test_write_gsd_dynamic(simulation_factory, create_md_sim, tmp_path):
if sim.device.communicator.rank == 0:
with gsd.hoomd.open(name=filename, mode='rb') as traj:
for step in range(5, 10):
assert traj[step].particles.types == ['t3', 't4']
np.testing.assert_allclose(traj[step].particles.mass,
N_particles * [0.8],
rtol=1e-07,
Expand Down Expand Up @@ -347,7 +345,6 @@ def test_write_gsd_dynamic(simulation_factory, create_md_sim, tmp_path):
snap.bonds.N = 3
snap.bonds.typeid[2] = 0
snap.bonds.group[2] = [10, 11]
snap.angles.types = ['a3', 'a4']

sim.state.snapshot = snap

Expand All @@ -361,7 +358,6 @@ def test_write_gsd_dynamic(simulation_factory, create_md_sim, tmp_path):
if sim.device.communicator.rank == 0:
with gsd.hoomd.open(name=filename, mode='rb') as traj:
assert traj[-1].bonds.N == 3
assert traj[-1].angles.types == ['a3', 'a4']


def test_write_gsd_log(create_md_sim, tmp_path):
Expand Down
6 changes: 4 additions & 2 deletions hoomd/snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def box(self, box):


class Snapshot:
"""Standalone copy of the simulation `State`.
"""Self-contained copy of the simulation `State`.
Args:
communicator (Communicator): MPI communicator to be used with the
Expand All @@ -63,7 +63,9 @@ class Snapshot:
.. seealso:
`Simulation.create_state_from_snapshot`
`State.snapshot`
`State.take_snapshot`
`State.restore_snapshot`
.. todo::
Expand Down
124 changes: 81 additions & 43 deletions hoomd/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from hoomd.snapshot import Snapshot
from hoomd.data import LocalSnapshot, LocalSnapshotGPU
import hoomd
import warnings


def _create_domain_decomposition(device, box):
Expand Down Expand Up @@ -38,21 +39,23 @@ def _create_domain_decomposition(device, box):
class State:
"""The state of a `hoomd.Simulation` object.
Provides access (read/write) to a `hoomd.Simulation` object's particle,
bond, angle, etc. data. Data access is facilitated through two complementary
APIs: *global* and *local* snapshots (note that global does not refer to
variable scope here). See `State.snapshot`, `State.cpu_local_snapshot`, and
`State.gpu_local_snapshot` for information about these data access patterns.
In addition, many commonly used smaller quantities such as the number of
particles in a simulation are available directly through `State` object
`State` stores a `hoomd.Simulation` object's particle, bond, angle, etc.
data that describe the microstate of the system. Data access is facilitated
through two complementary APIs: *local* snapshots that access data directly
available on the local MPI rank and *global* snapshots that collect the
entire state on rank 0. See `State.cpu_local_snapshot`,
`State.gpu_local_snapshot`, `State.take_snapshot`, and
`State.restore_snapshot` for information about these data access patterns.
In addition, many commonly used quantities such as the number of particles
and types in a simulation are available directly through `State` object
properties.
Note:
This object should never be directly instantiated by users. There is no
way to set a state created outside of a `hoomd.Simulation` object to a
simulation. Use `hoomd.Simulation.create_state_from_gsd` and
`hoomd.Simulation.create_state_from_snapshot` to instantiate a
`State` object.
`State` object as part of a simulation.
"""

def __init__(self, simulation, snapshot):
Expand Down Expand Up @@ -81,56 +84,91 @@ def __init__(self, simulation, snapshot):

@property
def snapshot(self):
r"""hoomd.Snapshot: All data of a simulation's current microstate.
`State.snapshot` should be used when all of a simulation's state
information is desired in a single object. When accessed, data across
all MPI ranks and from GPUs is gathered on the root MPI rank's memory.
When accessing data in MPI simulations, it is recommended to use a
``if snapshot.communicator.rank == 0:`` conditional to prevent
attempting to access data on a non-root rank.
This property can be set to replace the system state with the given
`hoomd.Snapshot` object. Example use cases in which a simulation's
state may be reset from a snapshot include Monte Carlo schemes
implemented at the Python script level, where the current snapshot is
passed to the Monte Carlo simulation before being passed back after
running some Monte Carlo steps.
"""Simulation snapshot.
Warning:
Using `State.snapshot` multiple times will gather data across MPI
ranks and GPUs every time. If the snapshot is needed for more than
one use, it is recommended to store it in a variable.
.. deprecated:: 3.0.0-beta.8
Use `take_snapshot` and `restore_snapshot` instead.
"""
warnings.warn("Deprecated, use state.take_snapshot()",
DeprecationWarning)
return self.take_snapshot()

@snapshot.setter
def snapshot(self, snapshot):
warnings.warn("Deprecated, use state.restore_snapshot()",
DeprecationWarning)
self.restore_snapshot(snapshot)

def take_snapshot(self):
"""Make a copy of the simulation current microstate.
`State.take_snapshot` makes a copy of the simulation microstate and
makes it available in a single object. `State.restore_snapshot` resets
the internal microstate to that in the given snapshot. Use these methods
to implement techniques like hybrid MD/MC or umbrella sampling where
entire system configurations need to be reset to a previous one after a
rejected move.
Note:
Data across all MPI ranks and from GPUs is gathered on the root MPI
rank's memory. When accessing data in MPI simulations, use a ``if
snapshot.communicator.rank == 0:`` conditional to access data arrays
only on the root rank.
Note:
Setting or getting a snapshot is an order :math:`O(N_{particles}
+ N_{bonds} + \ldots)` operation.
`State.take_snapshot` is an order :math:`O(N_{particles} + N_{bonds}
+ \\ldots)` operation.
See Also:
`restore_snapshot`
Returns:
hoomd.Snapshot: The current simulation microstate
"""
cpp_snapshot = self._cpp_sys_def.takeSnapshot_double()
return Snapshot._from_cpp_snapshot(cpp_snapshot,
self._simulation.device.communicator)

@snapshot.setter
def snapshot(self, snapshot):
def restore_snapshot(self, snapshot):
"""Restore the microstate of the simulation from a snapshot.
Args:
snapshot (hoomd.Snapshot): Snapshot of the system from
`take_snapshot`
Warning:
`restore_snapshot` can only make limited changes to the simulation
state. While it can change the number of particles/bonds/etc... or
their properties, it cannot change the number or names of the
particle/bond/etc.. types.
Note:
`State.restore_snapshot` is an order :math:`O(N_{particles} +
N_{bonds} + \\ldots)` operation and is very expensive when the
simulation device is a GPU.
See Also:
`take_snapshot`
"""
if self._in_context_manager:
raise RuntimeError(
"Cannot set state to new snapshot inside local snapshot.")
if self._simulation.device.communicator.rank == 0:
if len(snapshot.particles.types) != len(self.particle_types):
if snapshot.particles.types != self.particle_types:
raise RuntimeError(
"Number of particle types must remain the same")
if len(snapshot.bonds.types) != len(self.bond_types):
raise RuntimeError("Number of bond types must remain the same")
if len(snapshot.angles.types) != len(self.angle_types):
raise RuntimeError("Number of angle types must remain the same")
if len(snapshot.dihedrals.types) != len(self.dihedral_types):
"Particle types must remain the same")
if snapshot.bonds.types != self.bond_types:
raise RuntimeError("Bond types must remain the same")
if snapshot.angles.types != self.angle_types:
raise RuntimeError("Angle types must remain the same")
if snapshot.dihedrals.types != self.dihedral_types:
raise RuntimeError(
"Number of dihedral types must remain the same")
if len(snapshot.impropers.types) != len(self.improper_types):
"Dihedral types must remain the same")
if snapshot.impropers.types != self.improper_types:
raise RuntimeError(
"Number of dihedral types must remain the same")
if len(snapshot.pairs.types) != len(self.special_pair_types):
raise RuntimeError("Number of pair types must remain the same")
"Improper types must remain the same")
if snapshot.pairs.types != self.special_pair_types:
raise RuntimeError("Pair types must remain the same")

self._cpp_sys_def.initializeFromSnapshot(snapshot._cpp_obj)

Expand Down

0 comments on commit 61badf5

Please sign in to comment.