Coverage for ase / md / langevin.py: 83.33%

78 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-30 08:22 +0000

1"""Langevin dynamics class.""" 

2 

3import warnings 

4 

5import numpy as np 

6 

7from ase import Atoms, units 

8from ase.md.md import MolecularDynamics 

9 

10 

11class Langevin(MolecularDynamics): 

12 """Langevin (constant N, V, T) molecular dynamics.""" 

13 

14 # Helps Asap doing the right thing. Increment when changing stuff: 

15 _lgv_version = 5 

16 

17 def __init__( 

18 self, 

19 atoms: Atoms, 

20 timestep: float, 

21 temperature: float | None = None, 

22 friction: float | None = None, 

23 fixcm: bool = True, 

24 *, 

25 temperature_K: float | None = None, 

26 rng=None, 

27 **kwargs, 

28 ): 

29 """ 

30 Parameters 

31 ---------- 

32 atoms: Atoms object 

33 The list of atoms. 

34 

35 timestep: float 

36 The time step in ASE time units. 

37 

38 temperature: float (deprecated) 

39 The desired temperature, in electron volt. 

40 

41 temperature_K: float 

42 The desired temperature, in Kelvin. 

43 

44 friction: float 

45 A friction coefficient in inverse ASE time units. 

46 For example, set ``0.01 / ase.units.fs`` to provide 

47 0.01 fs\\ :sup:`−1` (10 ps\\ :sup:`−1`). 

48 

49 fixcm: bool (optional) 

50 If True, the position and momentum of the center of mass is 

51 kept unperturbed. Default: True. 

52 

53 .. deprecated:: 3.28.0 

54 

55 The implementation of ``fixcm=True`` does not strictly sample 

56 the correct NVT distributions for positions and momenta. 

57 The deviations are typically small for large systems but can be 

58 more pronounced for small systems. 

59 

60 Use :class:`~ase.constraints.FixCom` instead. 

61 ``fixcm`` will be removed from ASE in the near future. 

62 

63 rng: RNG object (optional) 

64 Random number generator, by default numpy.random. Must have a 

65 standard_normal method matching the signature of 

66 numpy.random.standard_normal. 

67 

68 **kwargs : dict, optional 

69 Additional arguments passed to 

70 :class:`~ase.md.md.MolecularDynamics` base class. 

71 

72 Notes 

73 ----- 

74 The temperature and friction are normally scalars, but in principle one 

75 quantity per atom could be specified by giving an array. 

76 

77 RATTLE constraints can be used with these propagators, see: 

78 E. V.-Eijnden, and G. Ciccotti, Chem. Phys. Lett. 429, 310 (2006) 

79 

80 The propagator is Equation 23 (Eq. 39 if RATTLE constraints are used) 

81 of the above reference. That reference also contains another 

82 propagator in Eq. 21/34; but that propagator is not quasi-symplectic 

83 and gives a systematic offset in the temperature at large time steps. 

84 """ 

85 if 'communicator' in kwargs: 

86 msg = ( 

87 '`communicator` has been deprecated since ASE 3.25.0 ' 

88 'and will be removed in ASE 3.26.0. Use `comm` instead.' 

89 ) 

90 warnings.warn(msg, FutureWarning) 

91 kwargs['comm'] = kwargs.pop('communicator') 

92 

93 if friction is None: 

94 raise TypeError("Missing 'friction' argument.") 

95 self.fr = friction 

96 self.temp = units.kB * self._process_temperature( 

97 temperature, temperature_K, 'eV' 

98 ) 

99 

100 if fixcm: 

101 msg = ( 

102 'The implementation of `fixcm=True` in `Langevin` does not ' 

103 'strictly sample the correct NVT distributions. ' 

104 'The deviations are typically small for large systems but can ' 

105 'be more pronounced for small systems. ' 

106 'Use `fixcm=False` together with `ase.constraints.FixCom`. ' 

107 '`fixcm` is deprecated since ASE 3.28.0 and will be removed in ' 

108 'a future release.' 

109 ) 

110 warnings.warn(msg, FutureWarning) 

111 self.fix_com = fixcm 

112 

113 if rng is None: 

114 self.rng = np.random 

115 else: 

116 self.rng = rng 

117 MolecularDynamics.__init__(self, atoms, timestep, **kwargs) 

118 self.updatevars() 

119 

120 def todict(self): 

121 d = MolecularDynamics.todict(self) 

122 d.update( 

123 { 

124 'temperature_K': self.temp / units.kB, 

125 'friction': self.fr, 

126 'fixcm': self.fix_com, 

127 } 

128 ) 

129 return d 

130 

131 def set_temperature(self, temperature=None, temperature_K=None): 

132 self.temp = units.kB * self._process_temperature( 

133 temperature, temperature_K, 'eV' 

134 ) 

135 self.updatevars() 

136 

137 def set_friction(self, friction): 

138 self.fr = friction 

139 self.updatevars() 

140 

141 def set_timestep(self, timestep): 

142 self.dt = timestep 

143 self.updatevars() 

144 

145 def updatevars(self): 

146 dt = self.dt 

147 T = self.temp 

148 fr = self.fr 

149 masses = self.masses 

150 sigma = np.sqrt(2 * T * fr / masses) 

151 

152 self.c1 = dt / 2.0 - dt * dt * fr / 8.0 

153 self.c2 = dt * fr / 2 - dt * dt * fr * fr / 8.0 

154 self.c3 = np.sqrt(dt) * sigma / 2.0 - dt**1.5 * fr * sigma / 8.0 

155 self.c5 = dt**1.5 * sigma / (2 * np.sqrt(3)) 

156 self.c4 = fr / 2.0 * self.c5 

157 

158 def step(self, forces=None): 

159 atoms = self.atoms 

160 natoms = len(atoms) 

161 

162 if forces is None: 

163 forces = atoms.get_forces(md=True) 

164 

165 # This velocity as well as rnd_pos, rnd_mom and a few other 

166 # variables are stored as attributes, so Asap can do its magic 

167 # when atoms migrate between processors. 

168 self.v = atoms.get_velocities() 

169 

170 xi = self.rng.standard_normal(size=(natoms, 3)) 

171 eta = self.rng.standard_normal(size=(natoms, 3)) 

172 

173 # When holonomic constraints for rigid linear triatomic molecules are 

174 # present, ask the constraints to redistribute xi and eta within each 

175 # triple defined in the constraints. This is needed to achieve the 

176 # correct target temperature. 

177 for constraint in self.atoms.constraints: 

178 if hasattr(constraint, 'redistribute_forces_md'): 

179 constraint.redistribute_forces_md(atoms, xi, rand=True) 

180 constraint.redistribute_forces_md(atoms, eta, rand=True) 

181 

182 self.comm.broadcast(xi, 0) 

183 self.comm.broadcast(eta, 0) 

184 

185 # To keep the center of mass stationary, we have to calculate 

186 # the random perturbations to the positions and the momenta, 

187 # and make sure that they sum to zero. This perturbs the 

188 # temperature slightly, and we have to correct. 

189 self.rnd_pos = self.c5 * eta 

190 self.rnd_vel = self.c3 * xi - self.c4 * eta 

191 # https://gitlab.com/ase/ase/-/merge_requests/3986 

192 # The implementation of `fixcm=True` does not strictly sample the 

193 # correct NVT distributions for positions and momenta. 

194 # It is deprecated and will be removed in a future ASE release. 

195 if self.fix_com: 

196 factor = np.sqrt(natoms / (natoms - 1.0)) 

197 self.rnd_pos -= self.rnd_pos.sum(axis=0) / natoms 

198 self.rnd_vel -= (self.rnd_vel * self.masses).sum(axis=0) / ( 

199 self.masses * natoms 

200 ) 

201 self.rnd_pos *= factor 

202 self.rnd_vel *= factor 

203 

204 # First halfstep in the velocity. 

205 self.v += ( 

206 self.c1 * forces / self.masses - self.c2 * self.v + self.rnd_vel 

207 ) 

208 

209 # Full step in positions 

210 x = atoms.get_positions() 

211 

212 # Step: x^n -> x^(n+1) - this applies constraints if any. 

213 atoms.set_positions(x + self.dt * self.v + self.rnd_pos) 

214 

215 # recalc velocities after RATTLE constraints are applied 

216 self.v = (self.atoms.get_positions() - x - self.rnd_pos) / self.dt 

217 forces = atoms.get_forces(md=True) 

218 

219 # Update the velocities 

220 self.v += ( 

221 self.c1 * forces / self.masses - self.c2 * self.v + self.rnd_vel 

222 ) 

223 

224 # Second part of RATTLE taken care of here 

225 atoms.set_momenta(self.v * self.masses) 

226 

227 return forces