Source code for ase.md.bussi
# fmt: off
"""Bussi NVT dynamics class."""
import math
import numpy as np
from ase import units
from ase.md.verlet import VelocityVerlet
[docs]
class Bussi(VelocityVerlet):
"""Bussi stochastic velocity rescaling (NVT) molecular dynamics.
Based on the paper from Bussi et al., J. Chem. Phys. 126, 014101 (2007)
(also available from https://arxiv.org/abs/0803.4060).
"""
def __init__(
self,
atoms,
timestep,
temperature_K,
taut,
rng=None,
**kwargs,
):
"""
Parameters
----------
atoms : Atoms
The atoms object.
timestep : float
The time step in ASE time units.
temperature_K : float
The desired temperature, in Kelvin.
taut : float
Time constant for Bussi temperature coupling in ASE time units.
rng : RNG object, optional
Random number generator, by default numpy.random.
**kwargs : dict, optional
Additional arguments are passed to
:class:~ase.md.md.MolecularDynamics base class.
"""
super().__init__(atoms, timestep, **kwargs)
self.temp = temperature_K * units.kB
self.taut = taut
if rng is None:
self.rng = np.random
else:
self.rng = rng
self.ndof = self.atoms.get_number_of_degrees_of_freedom()
self.target_kinetic_energy = 0.5 * self.temp * self.ndof
if np.isclose(
self.atoms.get_kinetic_energy(), 0.0, rtol=0, atol=1e-12
):
raise ValueError(
"Initial kinetic energy is zero. "
"Please set the initial velocities before running Bussi NVT."
)
self._exp_term = math.exp(-self.dt / self.taut)
self._masses = self.atoms.get_masses()[:, np.newaxis]
self.transferred_energy = 0.0
def scale_velocities(self):
"""Do the NVT Bussi stochastic velocity scaling."""
kinetic_energy = self.atoms.get_kinetic_energy()
alpha = self.calculate_alpha(kinetic_energy)
momenta = self.atoms.get_momenta()
self.atoms.set_momenta(alpha * momenta)
self.transferred_energy += (alpha**2 - 1.0) * kinetic_energy
def calculate_alpha(self, kinetic_energy):
"""Calculate the scaling factor alpha using equation (A7)
from the Bussi paper."""
energy_scaling_term = (
(1 - self._exp_term)
* self.target_kinetic_energy
/ kinetic_energy
/ self.ndof
)
# R1 in Eq. (A7)
noisearray = self.rng.standard_normal(size=(1,))
# ASE mpi interfaces can only broadcast arrays, not scalars
self.comm.broadcast(noisearray, 0)
normal_noise = noisearray[0]
# \sum_{i=2}^{Nf} R_i^2 in Eq. (A7)
# 2 * standard_gamma(n / 2) is equal to chisquare(n)
sum_of_noises = 2.0 * self.rng.standard_gamma(0.5 * (self.ndof - 1))
return math.sqrt(
self._exp_term
+ energy_scaling_term * (sum_of_noises + normal_noise**2)
+ 2
* normal_noise
* math.sqrt(self._exp_term * energy_scaling_term)
)
def step(self, forces=None):
"""Move one timestep forward using Bussi NVT molecular dynamics."""
self.scale_velocities()
return super().step(forces)