Coverage for /builds/kinetik161/ase/ase/md/andersen.py: 96.77%
62 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-12-10 11:04 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-12-10 11:04 +0000
1"""Andersen dynamics class."""
2from typing import IO, Optional, Union
4from numpy import cos, log, ones, pi, random, repeat
6from ase import Atoms, units
7from ase.md.md import MolecularDynamics
8from ase.parallel import DummyMPI, world
11class Andersen(MolecularDynamics):
12 """Andersen (constant N, V, T) molecular dynamics."""
14 def __init__(
15 self,
16 atoms: Atoms,
17 timestep: float,
18 temperature_K: float,
19 andersen_prob: float,
20 fixcm: bool = True,
21 trajectory: Optional[str] = None,
22 logfile: Optional[Union[IO, str]] = None,
23 loginterval: int = 1,
24 communicator=world,
25 rng=random,
26 append_trajectory: bool = False,
27 ):
28 """"
29 Parameters:
31 atoms: Atoms object
32 The list of atoms.
34 timestep: float
35 The time step in ASE time units.
37 temperature_K: float
38 The desired temperature, in Kelvin.
40 andersen_prob: float
41 A random collision probability, typically 1e-4 to 1e-1.
42 With this probability atoms get assigned random velocity components.
44 fixcm: bool (optional)
45 If True, the position and momentum of the center of mass is
46 kept unperturbed. Default: True.
48 rng: RNG object (optional)
49 Random number generator, by default numpy.random. Must have a
50 random_sample method matching the signature of
51 numpy.random.random_sample.
53 logfile: file object or str (optional)
54 If *logfile* is a string, a file with that name will be opened.
55 Use '-' for stdout.
57 trajectory: Trajectory object or str (optional)
58 Attach trajectory object. If *trajectory* is a string a
59 Trajectory will be constructed. Use *None* (the default) for no
60 trajectory.
62 communicator: MPI communicator (optional)
63 Communicator used to distribute random numbers to all tasks.
64 Default: ase.parallel.world. Set to None to disable communication.
66 append_trajectory: bool (optional)
67 Defaults to False, which causes the trajectory file to be
68 overwritten each time the dynamics is restarted from scratch.
69 If True, the new structures are appended to the trajectory
70 file instead.
72 The temperature is imposed by stochastic collisions with a heat bath
73 that acts on velocity components of randomly chosen particles.
74 The algorithm randomly decorrelates velocities, so dynamical properties
75 like diffusion or viscosity cannot be properly measured.
77 H. C. Andersen, J. Chem. Phys. 72 (4), 2384–2393 (1980)
78 """
79 self.temp = units.kB * temperature_K
80 self.andersen_prob = andersen_prob
81 self.fix_com = fixcm
82 self.rng = rng
83 if communicator is None:
84 communicator = DummyMPI()
85 self.communicator = communicator
86 MolecularDynamics.__init__(self, atoms, timestep, trajectory,
87 logfile, loginterval,
88 append_trajectory=append_trajectory)
90 def set_temperature(self, temperature_K):
91 self.temp = units.kB * temperature_K
93 def set_andersen_prob(self, andersen_prob):
94 self.andersen_prob = andersen_prob
96 def set_timestep(self, timestep):
97 self.dt = timestep
99 def boltzmann_random(self, width, size):
100 x = self.rng.random_sample(size=size)
101 y = self.rng.random_sample(size=size)
102 z = width * cos(2 * pi * x) * (-2 * log(1 - y))**0.5
103 return z
105 def get_maxwell_boltzmann_velocities(self):
106 natoms = len(self.atoms)
107 masses = repeat(self.masses, 3).reshape(natoms, 3)
108 width = (self.temp / masses)**0.5
109 velos = self.boltzmann_random(width, size=(natoms, 3))
110 return velos # [[x, y, z],] components for each atom
112 def step(self, forces=None):
113 atoms = self.atoms
115 if forces is None:
116 forces = atoms.get_forces(md=True)
118 self.v = atoms.get_velocities()
120 # Random atom-wise variables are stored as attributes and broadcasted:
121 # - self.random_com_velocity # added to all atoms if self.fix_com
122 # - self.random_velocity # added to some atoms if the per-atom
123 # - self.andersen_chance # andersen_chance <= andersen_prob
124 # a dummy communicator will be used for serial runs
126 if self.fix_com:
127 # add random velocity to center of mass to prepare Andersen
128 width = (self.temp / sum(self.masses))**0.5
129 self.random_com_velocity = (ones(self.v.shape)
130 * self.boltzmann_random(width, (3)))
131 self.communicator.broadcast(self.random_com_velocity, 0)
132 self.v += self.random_com_velocity
134 self.v += 0.5 * forces / self.masses * self.dt
136 # apply Andersen thermostat
137 self.random_velocity = self.get_maxwell_boltzmann_velocities()
138 self.andersen_chance = self.rng.random_sample(size=self.v.shape)
139 self.communicator.broadcast(self.random_velocity, 0)
140 self.communicator.broadcast(self.andersen_chance, 0)
141 self.v[self.andersen_chance <= self.andersen_prob] \
142 = self.random_velocity[self.andersen_chance <= self.andersen_prob]
144 x = atoms.get_positions()
145 if self.fix_com:
146 old_com = atoms.get_center_of_mass()
147 self.v -= self._get_com_velocity(self.v)
148 # Step: x^n -> x^(n+1) - this applies constraints if any
149 atoms.set_positions(x + self.v * self.dt)
150 if self.fix_com:
151 atoms.set_center_of_mass(old_com)
153 # recalc velocities after RATTLE constraints are applied
154 self.v = (atoms.get_positions() - x) / self.dt
155 forces = atoms.get_forces(md=True)
157 # Update the velocities
158 self.v += 0.5 * forces / self.masses * self.dt
160 if self.fix_com:
161 self.v -= self._get_com_velocity(self.v)
163 # Second part of RATTLE taken care of here
164 atoms.set_momenta(self.v * self.masses)
166 return forces