Source code for qmmm_pme.wrappers.simulation
#! /usr/bin/env python3
"""A module for defining the :class:`Simulation` class.
"""
from __future__ import annotations
from dataclasses import dataclass
from dataclasses import field
from typing import Any
from typing import Dict
from typing import TYPE_CHECKING
import numpy as np
from .logger import NullLogger
if TYPE_CHECKING:
from qmmm_pme.hamiltonians.hamiltonian import Hamiltonian
from qmmm_pme.dynamics.dynamics import Dynamics
from qmmm_pme.plugins.plugin import Plugin
from .system import System
EnergyDict = Dict[str, float]
[docs]@dataclass
class Simulation:
"""An object which manages and performs simulations.
:param system: |system| to perform calculations on.
:param hamiltonian: |hamiltonian| to perform calculations with.
:param integrator: |integrator| to perform calculations with.
:param logger: |logger| to record data generated during the
simulation
:param num_threads: The number of threads to run calculations on.
:param memory: The amount of memory to allocate to calculations.
:param plugins: Any :class:`Plugin` objects to apply to the
simulation.
"""
system: System
hamiltonian: Hamiltonian
dynamics: Dynamics
logger: Any = NullLogger
num_threads: int = 1
memory: str = "1 GB"
plugins: list[Plugin] = field(default_factory=list)
frame: int = 0
energy: EnergyDict = field(default_factory=dict)
def __post_init__(self) -> None:
self.calculator = self.hamiltonian.build_calculator(self.system)
self.integrator = self.dynamics.build_integrator(self.system)
self._register_plugins()
if not self.system.state.velocities().size:
self.system.state.velocities.update(
self.integrator.compute_velocities(),
)
self.calculate_energy_forces()
[docs] def run_dynamics(self, steps: int) -> None:
"""Run simulation using the :class:`System`,
:class:`Calculator`, and :class:`Integrator`.
:param steps: The number of steps to take.
"""
with self.logger as logger:
for i in range(steps):
new_positions, new_velocities = self.integrator.integrate()
self.system.state.positions.update(new_positions)
self.system.state.velocities.update(new_velocities)
self.wrap_positions()
logger.record(self)
self.calculate_energy_forces()
self.frame += 1
[docs] def calculate_energy_forces(self) -> None:
"""Update the :class:`System` forces and :class:`Simulation`
energy using calculations from the :class:`Calculator`.
"""
(
potential_energy, forces, components,
) = self.calculator.calculate()
self.system.state.forces.update(forces)
kinetic_energy = self.integrator.compute_kinetic_energy()
energy = {
"Total Energy": kinetic_energy + potential_energy,
".": {
"Kinetic Energy": kinetic_energy,
"Potential Energy": potential_energy,
".": components,
},
}
self.energy = energy
[docs] def calculate_forces(self) -> None:
"""Update the :class:`State` forces using calculations from the
:class:`Calculator`.
"""
(
potential_energy, forces, components,
) = self.calculator.calculate()
self.system.state.forces.update(forces)
[docs] def wrap_positions(self) -> None:
"""Atoms are wrapped to stay inside of the periodic box. This
function ensures molecules are not broken up by a periodic
boundary, since OpenMM electrostatics will be incorrect if atoms
in a molecule are not on the same side of the periodic box.
This method currently assumes an isotropic box.
"""
box = self.system.state.box()
inverse_box = np.linalg.inv(box)
positions = self.system.state.positions()
new_positions = np.zeros_like(positions)
for residue in self.system.topology.atoms():
residue_positions = positions[residue, :]
residue_centroid = np.average(residue_positions, axis=0)
inverse_centroid = residue_centroid @ inverse_box
mask = np.floor(inverse_centroid)
diff = (-mask @ box).reshape((-1, 3))
temp_pos = residue_positions + diff[:, np.newaxis, :]
new_positions[residue] = temp_pos.reshape((len(residue), 3))
self.system.state.positions.update(new_positions)
def _register_plugins(self) -> None:
"""Dynamically load :class:`Plugin` objects.
"""
for plugin in self.plugins:
getattr(self, plugin._key).register_plugin(plugin)