Source code for pydft_qmmm.wrappers.simulation

"""The core object which organizes simulations.
"""
from __future__ import annotations

from typing import TYPE_CHECKING

import numpy as np

from .logger import NullLogger
from pydft_qmmm.calculators import Calculator
from pydft_qmmm.calculators import CompositeCalculator
from pydft_qmmm.calculators import InterfaceCalculator
from pydft_qmmm.common import ResourceManager
from pydft_qmmm.common import ELEMENT_TO_MASS
from pydft_qmmm.hamiltonians import CalculatorHamiltonian
from pydft_qmmm.hamiltonians import CompositeHamiltonian
from pydft_qmmm.hamiltonians import QMMMHamiltonian
from pydft_qmmm.plugins import CalculatorCenter
from pydft_qmmm.plugins import CalculatorWrap
from pydft_qmmm.plugins import CentroidPartition
from pydft_qmmm.plugins import Stationary
from pydft_qmmm.plugins.plugin import PartitionPlugin

if TYPE_CHECKING:
    from typing import Any
    from pydft_qmmm.hamiltonians import Hamiltonian
    from pydft_qmmm.integrators import Integrator
    from pydft_qmmm.plugins.plugin import Plugin
    from .system import System


[docs] class Simulation: """Manages and performs simulations. Args: system: The system to simulate. integrator: The integrator defining how the system evolves. hamiltonian: The Hamiltonian defining how calculations will be performed. calculator: A user-defined calculator, which may be provided in place of a Hamiltonian object. plugins: A list of plugins to apply to the calculator or integrator objects before simulation. logger: A logger to record data generated during the simulation. Attributes: _frame: The current frame of the simulation, defaults to zero upon instantiation. """ _frame: int = 0 def __init__( self, system: System, integrator: Integrator, hamiltonian: Hamiltonian | None = None, calculator: Calculator | None = None, plugins: list[Plugin] | None = None, logger: Any = NullLogger(), ) -> None: # Make initial assignments. self.system = system self.integrator = integrator self.hamiltonian = hamiltonian if isinstance(calculator, Calculator): self.calculator = calculator elif ( isinstance(hamiltonian, CompositeHamiltonian) or isinstance(hamiltonian, CalculatorHamiltonian) ): self.calculator = hamiltonian.build_calculator(self.system) else: raise TypeError if plugins is None: plugins = [] self.plugins = plugins self.logger = logger # Perform additional simulation setup self._register_plugins() self._offset = np.zeros(self.system.positions.shape) if isinstance(self.hamiltonian, CompositeHamiltonian): for hamiltonian in self.hamiltonian.hamiltonians: if ( isinstance(hamiltonian, QMMMHamiltonian) and self.system.box.any() and isinstance(self.calculator, CompositeCalculator) ): query = "not (" for plugin in self.plugins: if isinstance(plugin, PartitionPlugin): query += plugin._query + " or " query = query.strip(" or ") + ")" if query == "not ()": query == "all" self.calculator.register_plugin(CentroidPartition(query)) self.calculator.register_plugin(CalculatorWrap()) self.calculator.register_plugin(CalculatorCenter()) calculators = [] if isinstance(self.calculator, InterfaceCalculator): calculators.append(self.calculator) elif isinstance(self.calculator, CompositeCalculator): for calculator in self.calculator.calculators: if isinstance(calculator, InterfaceCalculator): calculators.append(calculator) if system.masses[system.masses == 0].size > 0: query = "atom" for atom in np.where(system.masses.base == 0)[0]: query += f" {atom}" system.masses[atom] = ELEMENT_TO_MASS.get( system.elements[atom], 0.1, ) self.integrator.register_plugin(Stationary(query)) self._resources = ResourceManager(calculators) self.calculate_energy_forces()
[docs] def run_dynamics(self, steps: int) -> None: """Perform a number of simulation steps. Args: steps: The number of steps to take. """ with self.logger as logger: logger.record(self) for i in range(steps): new_positions, new_velocities = self.integrator.integrate( self.system, ) self.system.positions = new_positions self.system.velocities = new_velocities self.calculate_energy_forces() self._frame += 1 logger.record(self)
[docs] def calculate_energy_forces(self) -> None: """Update total system energy and forces on atoms in the system. """ temp = self.system.positions.base.copy() results = self.calculator.calculate() self.system.forces = results.forces kinetic_energy = self.integrator.compute_kinetic_energy( self.system, ) energy = { "Total Energy": kinetic_energy + results.energy, ".": { "Kinetic Energy": kinetic_energy, "Potential Energy": results.energy, ".": results.components, }, } self.energy = energy
#self._offset += temp - self.system.positions.base
[docs] def set_threads(self, threads: int) -> None: """Set the number of threads that calculators can use. Args: threads: The number of threads to utilize. """ self._resources.update_threads(threads)
[docs] def set_memory(self, memory: str) -> None: """Set the amount of memory that calculators can use. Args: memory: The amount of memory to utilize. """ self._resources.update_memory(memory)
def _register_plugins(self) -> None: """Dynamically load plugins for calculators and integrators. """ for plugin in self.plugins: getattr(self, plugin._key).register_plugin(plugin)