Beispiel #1
0
class Simulation(metaclass=Loggable):
    """Define a simulation.

    Args:
        device (`hoomd.device.Device`): Device to execute the simulation.
        seed (int): Random number seed.

    `Simulation` is the central class in HOOMD-blue that defines a simulation,
    including the `state` of the system, the `operations` that apply to the
    state during a simulation `run`, and the `device` to use when executing
    the simulation.

    `seed` sets the seed for the random number generator used by all operations
    added to this `Simulation`.
    """
    def __init__(self, device, seed=None):
        self._device = device
        self._state = None
        self._operations = Operations()
        self._operations._simulation = self
        self._timestep = None
        self._seed = seed

    @property
    def device(self):
        """hoomd.device.Device: Device used to execute the simulation."""
        return self._device

    @device.setter
    def device(self, value):
        raise ValueError("Device cannot be removed or replaced once in "
                         "Simulation object.")

    @log
    def timestep(self):
        """int: Current time step of the simulation.

        Note:
            Functions like `create_state_from_gsd` will set the initial timestep
            from the input. Set `timestep` before creating the simulation state
            to override values from ``create_`` methods::

                sim.timestep = 5000
                sim.create_state_from_gsd('gsd_at_step_10000000.gsd')
                assert sim.timestep == 5000
        """
        if not hasattr(self, '_cpp_sys'):
            return self._timestep
        else:
            return self._cpp_sys.getCurrentTimeStep()

    @timestep.setter
    def timestep(self, step):
        if int(step) < 0 or int(step) > TIMESTEP_MAX:
            raise ValueError(f"steps must be in the range [0, {TIMESTEP_MAX}]")
        elif self._state is None:
            self._timestep = step
        else:
            raise RuntimeError("State must not be set to change timestep.")

    @log
    def seed(self):
        """int: Random number seed.

        Seeds are in the range [0, 65535]. When set, `seed` will take only the
        lowest 16 bits of the given value.

        HOOMD-blue uses a deterministic counter based pseudorandom number
        generator. Any time a random value is needed, HOOMD-blue computes it as
        a function of the user provided seed `seed` (16 bits), the current
        `timestep` (lower 40 bits), particle identifiers, MPI ranks, and other
        unique identifying values as needed to sample uncorrelated values:
        ``random_value = f(seed, timestep, ...)``
        """
        if self.state is None or self._seed is None:
            return self._seed
        else:
            return self._state._cpp_sys_def.getSeed()

    @seed.setter
    def seed(self, v):
        v_int = int(v)
        if v_int < 0 or v_int > SEED_MAX:
            v_int = v_int & SEED_MAX
            self.device._cpp_msg.warning(
                f"Provided seed {v} is larger than {SEED_MAX}. "
                f"Truncating to {v_int}.\n")

        self._seed = v_int

        if self._state is not None:
            self._state._cpp_sys_def.setSeed(v_int)

    def _init_system(self, step):
        """Initialize the system State.

        Perform additional initialization operations not in the State
        constructor.
        """
        self._cpp_sys = _hoomd.System(self.state._cpp_sys_def, step)

        if self._seed is not None:
            self._state._cpp_sys_def.setSeed(self._seed)

        self._init_communicator()

    def _init_communicator(self):
        """Initialize the Communicator."""
        # initialize communicator
        if hoomd.version.mpi_enabled:
            pdata = self.state._cpp_sys_def.getParticleData()
            decomposition = pdata.getDomainDecomposition()
            if decomposition is not None:
                # create the c++ Communicator
                if isinstance(self.device, hoomd.device.CPU):
                    cpp_communicator = _hoomd.Communicator(
                        self.state._cpp_sys_def, decomposition)
                else:
                    cpp_communicator = _hoomd.CommunicatorGPU(
                        self.state._cpp_sys_def, decomposition)

                # set Communicator in C++ System
                self._cpp_sys.setCommunicator(cpp_communicator)
                self._system_communicator = cpp_communicator
            else:
                self._system_communicator = None
        else:
            self._system_communicator = None

    def _warn_if_seed_unset(self):
        if self.seed is None:
            self.device._cpp_msg.warning(
                "Simulation.seed is not set, using default seed=0\n")

    def create_state_from_gsd(self, filename, frame=-1):
        """Create the simulation state from a GSD file.

        Args:
            filename (str): GSD file to read

            frame (int): Index of the frame to read from the file. Negative
                values index back from the last frame in the file.
        """
        if self.state is not None:
            raise RuntimeError("Cannot initialize more than once\n")
        filename = _hoomd.mpi_bcast_str(filename, self.device._cpp_exec_conf)
        # Grab snapshot and timestep
        reader = _hoomd.GSDReader(self.device._cpp_exec_conf, filename,
                                  abs(frame), frame < 0)
        snapshot = Snapshot._from_cpp_snapshot(reader.getSnapshot(),
                                               self.device.communicator)

        step = reader.getTimeStep() if self.timestep is None else self.timestep
        self._state = State(self, snapshot)

        reader.clearSnapshot()

        self._init_system(step)

    def create_state_from_snapshot(self, snapshot):
        """Create the simulations state from a `Snapshot`.

        Args:
            snapshot (Snapshot or gsd.hoomd.Snapshot): Snapshot to initialize
                the state from. A `gsd.hoomd.Snapshot` will first be
                converted to a `hoomd.Snapshot`.


        When `timestep` is `None` before calling, `create_state_from_snapshot`
        sets `timestep` to 0.
        """
        if self.state is not None:
            raise RuntimeError("Cannot initialize more than once\n")

        if isinstance(snapshot, Snapshot):
            # snapshot is hoomd.Snapshot
            self._state = State(self, snapshot)
        elif _match_class_path(snapshot, 'gsd.hoomd.Snapshot'):
            # snapshot is gsd.hoomd.Snapshot
            snapshot = Snapshot.from_gsd_snapshot(snapshot,
                                                  self._device.communicator)
            self._state = State(self, snapshot)
        else:
            raise TypeError(
                "Snapshot must be a hoomd.Snapshot or gsd.hoomd.Snapshot.")

        step = 0
        if self.timestep is not None:
            step = self.timestep

        self._init_system(step)

    @property
    def state(self):
        """hoomd.State: The current simulation state."""
        return self._state

    @property
    def operations(self):
        """hoomd.Operations: The operations that apply to the state."""
        return self._operations

    @operations.setter
    def operations(self, operations):
        # This condition is necessary to allow for += and -= operators to work
        # correctly with simulation.operations (+=/-=).
        if operations is self._operations:
            return
        else:
            # Handle error cases first
            if operations._scheduled or operations._simulation is not None:
                raise RuntimeError(
                    "Cannot add `hoomd.Operations` object that belongs to "
                    "another `hoomd.Simulation` object.")
            # Switch out `hoomd.Operations` objects.
            reschedule = False
            if self._operations._scheduled:
                self._operations._unschedule()
                reschedule = True

            self._operations._simulation = None
            operations._simulation = self
            self._operations = operations

            if reschedule:
                self._operations._schedule()

    @log
    def tps(self):
        """float: The average number of time steps per second.

        `tps` is the number of steps executed divided by the elapsed
        walltime in seconds. It is updated during the `run` loop and remains
        fixed after `run` completes.

        Note:
            The start time and step are reset at the beginning of each call to
            `run`.
        """
        if self.state is None:
            return None
        else:
            return self._cpp_sys.getLastTPS()

    @log
    def walltime(self):
        """float: The walltime spent during the last call to `run`.

        `walltime` is the number seconds that the last call to `run` took to
        complete. It is updated during the `run` loop and remains fixed after
        `run` completes.

        Note:
            `walltime` resets to 0 at the beginning of each call to `run`.
        """
        if self.state is None:
            return 0
        else:
            return self._cpp_sys.walltime

    @log
    def final_timestep(self):
        """float: `run` will end at this timestep.

        `final_timestep` is the timestep on which the currently executing `run`
        will complete.
        """
        if self.state is None:
            return self.timestep
        else:
            return self._cpp_sys.final_timestep

    @property
    def always_compute_pressure(self):
        """bool: Always compute the virial and pressure (defaults to ``False``).

        By default, HOOMD only computes the virial and pressure on timesteps
        where it is needed (when :py:class:`hoomd.write.GSD` writes
        log data to a file or when using an NPT integrator). Set
        `always_compute_pressure` to True to make the per particle virial,
        net virial, and system pressure available to query any time by property
        or through the :py:class:`hoomd.logging.Logger` interface.

        Note:
            Enabling this flag will result in a moderate performance penalty
            when using MD pair potentials.
        """
        if not hasattr(self, '_cpp_sys'):
            return False
        else:
            return self._cpp_sys.getPressureFlag()

    @always_compute_pressure.setter
    def always_compute_pressure(self, value):
        if not hasattr(self, '_cpp_sys'):
            # TODO make this work when not attached by automatically setting
            # flag when state object is instantiated.
            raise RuntimeError('Cannot set flag without state')
        else:
            self._cpp_sys.setPressureFlag(value)

            # if the flag is true, also set it in the particle data
            if value:
                self._state._cpp_sys_def.getParticleData().setPressureFlag()

    def run(self, steps, write_at_start=False):
        """Advance the simulation a number of steps.

        Args:
            steps (int): Number of steps to advance the simulation.

            write_at_start (bool): When `True`, writers
               with triggers that evaluate `True` for the initial step will be
               exected before the time step loop.

        Note:
            Initialize the simulation's state before calling `run`.

        During each step `run`, `Simulation` applies its `operations` to the
        state in the order: Tuners, Updaters, Integrator, then Writers following
        the logic in this pseudocode::

            if write_at_start:
                for writer in operations.writers:
                    if writer.trigger(timestep):
                        writer.write(timestep)

            end_step = timestep + steps
            while timestep < end_step:
                for tuner in operations.tuners:
                    if tuner.trigger(timestep):
                        tuner.tune(timestep)

                for updater in operations.updaters:
                    if updater.trigger(timestep):
                        updater.update(timestep)

                if operations.integrator is not None:
                    operations.integrator(timestep)

                timestep += 1

                for writer in operations.writers:
                    if writer.trigger(timestep):
                        writer.write(timestep)

        This order of operations ensures that writers (such as `hoomd.dump.GSD`)
        capture the final output of the last step of the run loop. For example,
        a writer with a trigger ``hoomd.trigger.Periodic(period=100, phase=0)``
        active during a ``run(500)`` would write on steps 100, 200, 300, 400,
        and 500. Set ``write_at_start=True`` on the first
        call to `run` to also obtain output at step 0.

        Warning:
            Using ``write_at_start=True`` in subsequent
            calls to `run` will result in duplicate output frames.
        """
        # check if initialization has occurred
        if not hasattr(self, '_cpp_sys'):
            raise RuntimeError('Cannot run before state is set.')
        if self._state._in_context_manager:
            raise RuntimeError(
                "Cannot call run inside of a local snapshot context manager.")
        if not self.operations._scheduled:
            self.operations._schedule()

        steps_int = int(steps)
        if steps_int < 0 or steps_int > TIMESTEP_MAX - 1:
            raise ValueError(f"steps must be in the range [0, "
                             f"{TIMESTEP_MAX-1}]")

        self._cpp_sys.run(steps_int, write_at_start)

    def write_debug_data(self, filename):
        """Write debug data to a JSON file.

        Args:
            filename (str): Name of file to write.

        The debug data file contains useful information for others to help you
        troubleshoot issues.

        Note:
            The file format and particular data written to this file may change
            from version to version.

        Warning:
            The specified file name will be overwritten.
        """
        debug_data = {}
        debug_data['hoomd_module'] = str(hoomd)
        debug_data['version'] = dict(
            compile_date=hoomd.version.compile_date,
            compile_flags=hoomd.version.compile_flags,
            cxx_compiler=hoomd.version.cxx_compiler,
            git_branch=hoomd.version.git_branch,
            git_sha1=hoomd.version.git_sha1,
            gpu_api_version=hoomd.version.gpu_api_version,
            gpu_enabled=hoomd.version.gpu_enabled,
            gpu_platform=hoomd.version.gpu_platform,
            install_dir=hoomd.version.install_dir,
            mpi_enabled=hoomd.version.mpi_enabled,
            source_dir=hoomd.version.source_dir,
            tbb_enabled=hoomd.version.tbb_enabled,
        )

        reasons = hoomd.device.GPU.get_unavailable_device_reasons()

        debug_data['device'] = dict(
            msg_file=self.device.msg_file,
            notice_level=self.device.notice_level,
            devices=self.device.devices,
            num_cpu_threads=self.device.num_cpu_threads,
            gpu_available_devices=hoomd.device.GPU.get_available_devices(),
            gpu_unavailable_device_reasons=reasons)

        debug_data['communicator'] = dict(
            num_ranks=self.device.communicator.num_ranks,
            partition=self.device.communicator.partition)

        # TODO: Domain decomposition

        if self.state is not None:
            debug_data['state'] = dict(
                types=self.state.types,
                N_particles=self.state.N_particles,
                N_bonds=self.state.N_bonds,
                N_angles=self.state.N_angles,
                N_impropers=self.state.N_impropers,
                N_special_pairs=self.state.N_special_pairs,
                N_dihedrals=self.state.N_dihedrals,
                box=repr(self.state.box))

        # save all loggable quantities from operations and their child computes
        logger = hoomd.logging.Logger(only_default=False)
        logger += self

        for op in self.operations:
            logger.add(op)

            for child in op._children:
                logger.add(child)

        log = logger.log()
        log_values = hoomd.util.dict_map(log, lambda v: v[0])
        debug_data['operations'] = log_values

        if self.device.communicator.rank == 0:
            with open(filename, 'w') as f:
                json.dump(debug_data, f, default=lambda v: str(v), indent=4)
class Simulation(metaclass=Loggable):
    """Define a simulation.

    Args:
        device (`hoomd.device.Device`): Device to execute the simulation.
        seed (int): Random number seed.

    `Simulation` is the central class in HOOMD-blue that defines a simulation,
    including the `state` of the system, the `operations` that apply to the
    state during a simulation `run`, and the `device` to use when executing
    the simulation.

    `seed` sets the seed for the random number generator used by all operations
    added to this `Simulation`.
    """

    def __init__(self, device, seed=None):
        self._device = device
        self._state = None
        self._operations = Operations()
        self._operations._simulation = self
        self._timestep = None
        self._seed = seed

    @property
    def device(self):
        """hoomd.device.Device: Device used to execute the simulation."""
        return self._device

    @device.setter
    def device(self, value):
        raise ValueError("Device cannot be removed or replaced once in "
                         "Simulation object.")

    @log
    def timestep(self):
        """int: Current time step of the simulation.

        Note:
            Functions like `create_state_from_gsd` will set the initial timestep
            from the input. Set `timestep` before creating the simulation state
            to override values from ``create_`` methods::

                sim.timestep = 5000
                sim.create_state_from_gsd('gsd_at_step_10000000.gsd')
                assert sim.timestep == 5000
        """
        if not hasattr(self, '_cpp_sys'):
            return self._timestep
        else:
            return self._cpp_sys.getCurrentTimeStep()

    @timestep.setter
    def timestep(self, step):
        if int(step) < 0 or int(step) > TIMESTEP_MAX:
            raise ValueError(f"steps must be in the range [0, {TIMESTEP_MAX}]")
        elif self._state is None:
            self._timestep = step
        else:
            raise RuntimeError("State must not be set to change timestep.")

    @log
    def seed(self):
        """int: Random number seed.

        Seeds are in the range [0, 65535]. When set, `seed` will take only the
        lowest 16 bits of the given value.

        HOOMD-blue uses a deterministic counter based pseudorandom number
        generator. Any time a random value is needed, HOOMD-blue computes it as
        a function of the user provided seed `seed` (16 bits), the current
        `timestep` (lower 40 bits), particle identifiers, MPI ranks, and other
        unique identifying values as needed to sample uncorrelated values:
        ``random_value = f(seed, timestep, ...)``
        """
        if self._state is None or self._seed is None:
            return self._seed
        else:
            return self._state._cpp_sys_def.getSeed()

    @seed.setter
    def seed(self, v):
        v_int = int(v)
        if v_int < 0 or v_int > SEED_MAX:
            v_int = v_int & SEED_MAX
            self.device._cpp_msg.warning(
                f"Provided seed {v} is larger than {SEED_MAX}. "
                f"Truncating to {v_int}.\n")

        self._seed = v_int

        if self._state is not None:
            self._state._cpp_sys_def.setSeed(v_int)

    def _init_system(self, step):
        """Initialize the system State.

        Perform additional initialization operations not in the State
        constructor.
        """
        self._cpp_sys = _hoomd.System(self.state._cpp_sys_def, step)

        if self._seed is not None:
            self._state._cpp_sys_def.setSeed(self._seed)

        self._init_communicator()

    def _init_communicator(self):
        """Initialize the Communicator."""
        # initialize communicator
        if hoomd.version.mpi_enabled:
            pdata = self.state._cpp_sys_def.getParticleData()
            decomposition = pdata.getDomainDecomposition()
            if decomposition is not None:
                # create the c++ Communicator
                if isinstance(self.device, hoomd.device.CPU):
                    cpp_communicator = _hoomd.Communicator(
                        self.state._cpp_sys_def, decomposition)
                else:
                    cpp_communicator = _hoomd.CommunicatorGPU(
                        self.state._cpp_sys_def, decomposition)

                # set Communicator in C++ System and SystemDefinition
                self._cpp_sys.setCommunicator(cpp_communicator)
                self.state._cpp_sys_def.setCommunicator(cpp_communicator)
                self._system_communicator = cpp_communicator
            else:
                self._system_communicator = None
        else:
            self._system_communicator = None

    def _warn_if_seed_unset(self):
        if self.seed is None:
            self.device._cpp_msg.warning(
                "Simulation.seed is not set, using default seed=0\n")

    def create_state_from_gsd(self,
                              filename,
                              frame=-1,
                              domain_decomposition=(None, None, None)):
        """Create the simulation state from a GSD file.

        Args:
            filename (str): GSD file to read

            frame (int): Index of the frame to read from the file. Negative
                values index back from the last frame in the file.

            domain_decomposition (tuple): Choose how to distribute the state
                across MPI ranks with domain decomposition. Provide a tuple
                of 3 integers indicating the number of evenly spaced domains in
                the x, y, and z directions (e.g. ``(8,4,2)``). Provide a tuple
                of 3 lists of floats to set the fraction of the simulation box
                to include in each domain. The sum of each list of floats must
                be 1.0 (e.g. ``([0.25, 0.75], [0.2, 0.8], [1.0])``).

        Note:
            Set any or all of the ``domain_decomposition`` tuple elements to
            `None` and `create_state_from_gsd` will select a value that
            minimizes the surface area between the domains (e.g.
            ``(2,None,None)``). The domains are spaced evenly along each
            automatically selected direction. The default value of ``(None,
            None, None)`` will automatically select the number of domains in all
            directions.
        """
        if self._state is not None:
            raise RuntimeError("Cannot initialize more than once\n")
        filename = _hoomd.mpi_bcast_str(filename, self.device._cpp_exec_conf)
        # Grab snapshot and timestep
        reader = _hoomd.GSDReader(self.device._cpp_exec_conf, filename,
                                  abs(frame), frame < 0)
        snapshot = Snapshot._from_cpp_snapshot(reader.getSnapshot(),
                                               self.device.communicator)

        step = reader.getTimeStep() if self.timestep is None else self.timestep
        self._state = State(self, snapshot, domain_decomposition)

        reader.clearSnapshot()

        self._init_system(step)

    def create_state_from_snapshot(self,
                                   snapshot,
                                   domain_decomposition=(None, None, None)):
        """Create the simulation state from a `Snapshot`.

        Args:
            snapshot (Snapshot or gsd.hoomd.Snapshot): Snapshot to initialize
                the state from. A `gsd.hoomd.Snapshot` will first be
                converted to a `hoomd.Snapshot`.

            domain_decomposition (tuple): Choose how to distribute the state
                across MPI ranks with domain decomposition. Provide a tuple
                of 3 integers indicating the number of evenly spaced domains in
                the x, y, and z directions (e.g. ``(8,4,2)``). Provide a tuple
                of 3 lists of floats to set the fraction of the simulation box
                to include in each domain. The sum of each list of floats must
                be 1.0 (e.g. ``([0.25, 0.75], [0.2, 0.8], [1.0])``).

        When `timestep` is `None` before calling, `create_state_from_snapshot`
        sets `timestep` to 0.

        Note:
            Set any or all of the ``domain_decomposition`` tuple elements to
            `None` and `create_state_from_gsd` will select a value that
            minimizes the surface area between the domains (e.g.
            ``(2,None,None)``). The domains are spaced evenly along each
            automatically selected direction. The default value of ``(None,
            None, None)`` will automatically select the number of domains in all
            directions.

        See Also:
            `State.get_snapshot`

            `State.set_snapshot`
        """
        if self._state is not None:
            raise RuntimeError("Cannot initialize more than once\n")

        if isinstance(snapshot, Snapshot):
            # snapshot is hoomd.Snapshot
            self._state = State(self, snapshot, domain_decomposition)
        elif _match_class_path(snapshot, 'gsd.hoomd.Snapshot'):
            # snapshot is gsd.hoomd.Snapshot
            snapshot = Snapshot.from_gsd_snapshot(snapshot,
                                                  self._device.communicator)
            self._state = State(self, snapshot, domain_decomposition)
        else:
            raise TypeError(
                "Snapshot must be a hoomd.Snapshot or gsd.hoomd.Snapshot.")

        step = 0
        if self.timestep is not None:
            step = self.timestep

        self._init_system(step)

    @property
    def state(self):
        """hoomd.State: The current simulation state."""
        return self._state

    @property
    def operations(self):
        """hoomd.Operations: The operations that apply to the state."""
        return self._operations

    @operations.setter
    def operations(self, operations):
        # This condition is necessary to allow for += and -= operators to work
        # correctly with simulation.operations (+=/-=).
        if operations is self._operations:
            return
        else:
            # Handle error cases first
            if operations._scheduled or operations._simulation is not None:
                raise RuntimeError(
                    "Cannot add `hoomd.Operations` object that belongs to "
                    "another `hoomd.Simulation` object.")
            # Switch out `hoomd.Operations` objects.
            reschedule = False
            if self._operations._scheduled:
                self._operations._unschedule()
                reschedule = True

            self._operations._simulation = None
            operations._simulation = self
            self._operations = operations

            if reschedule:
                self._operations._schedule()

    @log
    def tps(self):
        """float: The average number of time steps per second.

        `tps` is the number of steps executed divided by the elapsed
        walltime in seconds. It is updated during the `run` loop and remains
        fixed after `run` completes.

        Note:
            The start time and step are reset at the beginning of each call to
            `run`.
        """
        if self._state is None:
            return None
        else:
            return self._cpp_sys.getLastTPS()

    @log
    def walltime(self):
        """float: The walltime spent during the last call to `run`.

        `walltime` is the number seconds that the last call to `run` took to
        complete. It is updated during the `run` loop and remains fixed after
        `run` completes.

        Note:
            `walltime` resets to 0 at the beginning of each call to `run`.
        """
        if self._state is None:
            return 0
        else:
            return self._cpp_sys.walltime

    @log
    def final_timestep(self):
        """float: `run` will end at this timestep.

        `final_timestep` is the timestep on which the currently executing `run`
        will complete.
        """
        if self._state is None:
            return self.timestep
        else:
            return self._cpp_sys.final_timestep

    @property
    def always_compute_pressure(self):
        """bool: Always compute the virial and pressure (defaults to ``False``).

        By default, HOOMD only computes the virial and pressure on timesteps
        where it is needed (when :py:class:`hoomd.write.GSD` writes
        log data to a file or when using an NPT integrator). Set
        `always_compute_pressure` to True to make the per particle virial,
        net virial, and system pressure available to query any time by property
        or through the :py:class:`hoomd.logging.Logger` interface.

        Note:
            Enabling this flag will result in a moderate performance penalty
            when using MD pair potentials.
        """
        if not hasattr(self, '_cpp_sys'):
            return False
        else:
            return self._cpp_sys.getPressureFlag()

    @always_compute_pressure.setter
    def always_compute_pressure(self, value):
        if not hasattr(self, '_cpp_sys'):
            # TODO make this work when not attached by automatically setting
            # flag when state object is instantiated.
            raise RuntimeError('Cannot set flag without state')
        else:
            self._cpp_sys.setPressureFlag(value)

            # if the flag is true, also set it in the particle data
            if value:
                self._state._cpp_sys_def.getParticleData().setPressureFlag()

    def run(self, steps, write_at_start=False):
        """Advance the simulation a number of steps.

        Args:
            steps (int): Number of steps to advance the simulation.

            write_at_start (bool): When `True`, writers
               with triggers that evaluate `True` for the initial step will be
               executed before the time step loop.

        Note:
            Initialize the simulation's state before calling `run`.

        During each step `run`, `Simulation` applies its `operations` to the
        state in the order: Tuners, Updaters, Integrator, then Writers following
        the logic in this pseudocode::

            if write_at_start:
                for writer in operations.writers:
                    if writer.trigger(timestep):
                        writer.write(timestep)

            end_step = timestep + steps
            while timestep < end_step:
                for tuner in operations.tuners:
                    if tuner.trigger(timestep):
                        tuner.tune(timestep)

                for updater in operations.updaters:
                    if updater.trigger(timestep):
                        updater.update(timestep)

                if operations.integrator is not None:
                    operations.integrator(timestep)

                timestep += 1

                for writer in operations.writers:
                    if writer.trigger(timestep):
                        writer.write(timestep)

        This order of operations ensures that writers (such as
        `hoomd.write.GSD`) capture the final output of the last step of the run
        loop. For example, a writer with a trigger
        ``hoomd.trigger.Periodic(period=100, phase=0)`` active during a
        ``run(500)`` would write on steps 100, 200, 300, 400, and 500. Set
        ``write_at_start=True`` on the first call to `run` to also obtain output
        at step 0.

        Warning:
            Using ``write_at_start=True`` in subsequent
            calls to `run` will result in duplicate output frames.
        """
        # check if initialization has occurred
        if not hasattr(self, '_cpp_sys'):
            raise RuntimeError('Cannot run before state is set.')
        if self._state._in_context_manager:
            raise RuntimeError(
                "Cannot call run inside of a local snapshot context manager.")
        if not self.operations._scheduled:
            self.operations._schedule()

        steps_int = int(steps)
        if steps_int < 0 or steps_int > TIMESTEP_MAX - 1:
            raise ValueError(f"steps must be in the range [0, "
                             f"{TIMESTEP_MAX-1}]")

        self._cpp_sys.run(steps_int, write_at_start)