Source code for ase.md.andersen
"""Andersen dynamics class."""
from numpy import cos, log, ones, pi, random, repeat
from ase import Atoms, units
from ase.md.md import MolecularDynamics
[docs]
class Andersen(MolecularDynamics):
"""Andersen (constant N, V, T) molecular dynamics.
Andersen, J. Chem. Phys. 72, 2384–2393 (1980).
https://doi.org/10.1063/1.439486
"""
def __init__(
self,
atoms: Atoms,
timestep: float,
temperature_K: float,
andersen_prob: float,
fixcm: bool = True,
rng=random,
**kwargs,
):
"""
Parameters
----------
atoms: Atoms object
The list of atoms.
timestep: float
The time step in ASE time units.
temperature_K: float
The desired temperature, in Kelvin.
andersen_prob: float
A random collision probability, typically 1e-4 to 1e-1.
With this probability atoms get assigned random velocity components.
fixcm: bool (optional)
If True, the position and momentum of the center of mass is
kept unperturbed. Default: True.
rng: RNG object, default: ``numpy.random``
Random number generator. This must have the ``random`` method
with the same signature as ``numpy.random.random``.
**kwargs : dict, optional
Additional arguments passed to
:class:`~ase.md.md.MolecularDynamics`
base class.
Notes
-----
The temperature is imposed by stochastic collisions with a heat bath
that acts on velocity components of randomly chosen particles.
The algorithm randomly decorrelates velocities, so dynamical properties
like diffusion or viscosity cannot be properly measured.
"""
self.temp = units.kB * temperature_K
self.andersen_prob = andersen_prob
self.fix_com = fixcm
self.rng = rng
MolecularDynamics.__init__(self, atoms, timestep, **kwargs)
def set_temperature(self, temperature_K):
self.temp = units.kB * temperature_K
def set_andersen_prob(self, andersen_prob):
self.andersen_prob = andersen_prob
def set_timestep(self, timestep):
self.dt = timestep
def boltzmann_random(self, width, size):
x = self.rng.random(size=size)
y = self.rng.random(size=size)
z = width * cos(2 * pi * x) * (-2 * log(1 - y)) ** 0.5
return z
def get_maxwell_boltzmann_velocities(self):
natoms = len(self.atoms)
masses = repeat(self.masses, 3).reshape(natoms, 3)
width = (self.temp / masses) ** 0.5
velos = self.boltzmann_random(width, size=(natoms, 3))
return velos # [[x, y, z],] components for each atom
def step(self, forces=None):
atoms = self.atoms
if forces is None:
forces = atoms.get_forces(md=True)
self.v = atoms.get_velocities()
# Random atom-wise variables are stored as attributes and broadcasted:
# - self.random_com_velocity # added to all atoms if self.fix_com
# - self.random_velocity # added to some atoms if the per-atom
# - self.andersen_chance # andersen_chance <= andersen_prob
# a dummy communicator will be used for serial runs
if self.fix_com:
# add random velocity to center of mass to prepare Andersen
width = (self.temp / sum(self.masses)) ** 0.5
self.random_com_velocity = ones(
self.v.shape
) * self.boltzmann_random(width, (3))
self.comm.broadcast(self.random_com_velocity, 0)
self.v += self.random_com_velocity
self.v += 0.5 * forces / self.masses * self.dt
# apply Andersen thermostat
self.random_velocity = self.get_maxwell_boltzmann_velocities()
self.andersen_chance = self.rng.random(size=self.v.shape)
self.comm.broadcast(self.random_velocity, 0)
self.comm.broadcast(self.andersen_chance, 0)
self.v[self.andersen_chance <= self.andersen_prob] = (
self.random_velocity[self.andersen_chance <= self.andersen_prob]
)
x = atoms.get_positions()
if self.fix_com:
old_com = atoms.get_center_of_mass()
self.v -= self._get_com_velocity(self.v)
# Step: x^n -> x^(n+1) - this applies constraints if any
atoms.set_positions(x + self.v * self.dt)
if self.fix_com:
atoms.set_center_of_mass(old_com)
# recalc velocities after RATTLE constraints are applied
self.v = (atoms.get_positions() - x) / self.dt
forces = atoms.get_forces(md=True)
# Update the velocities
self.v += 0.5 * forces / self.masses * self.dt
if self.fix_com:
self.v -= self._get_com_velocity(self.v)
# Second part of RATTLE taken care of here
atoms.set_momenta(self.v * self.masses)
return forces