Coverage for /builds/ase/ase/ase/md/langevin.py: 92.11%

76 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2025-08-02 00:12 +0000

1"""Langevin dynamics class.""" 

2 

3import warnings 

4from typing import Optional 

5 

6import numpy as np 

7 

8from ase import Atoms, units 

9from ase.md.md import MolecularDynamics 

10 

11 

12class Langevin(MolecularDynamics): 

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

14 

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

16 _lgv_version = 5 

17 

18 def __init__( 

19 self, 

20 atoms: Atoms, 

21 timestep: float, 

22 temperature: Optional[float] = None, 

23 friction: Optional[float] = None, 

24 fixcm: bool = True, 

25 *, 

26 temperature_K: Optional[float] = None, 

27 rng=None, 

28 **kwargs, 

29 ): 

30 """ 

31 Parameters 

32 ---------- 

33 atoms: Atoms object 

34 The list of atoms. 

35 

36 timestep: float 

37 The time step in ASE time units. 

38 

39 temperature: float (deprecated) 

40 The desired temperature, in electron volt. 

41 

42 temperature_K: float 

43 The desired temperature, in Kelvin. 

44 

45 friction: float 

46 A friction coefficient in inverse ASE time units. 

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

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

49 

50 fixcm: bool (optional) 

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

52 kept unperturbed. Default: True. 

53 

54 rng: RNG object (optional) 

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

56 standard_normal method matching the signature of 

57 numpy.random.standard_normal. 

58 

59 **kwargs : dict, optional 

60 Additional arguments passed to :class:~ase.md.md.MolecularDynamics 

61 base class. 

62 

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

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

65 

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

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

68 

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

70 of the above reference. That reference also contains another 

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

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

73 """ 

74 if 'communicator' in kwargs: 

75 msg = ( 

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

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

78 ) 

79 warnings.warn(msg, FutureWarning) 

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

81 

82 if friction is None: 

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

84 self.fr = friction 

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

86 temperature, temperature_K, 'eV' 

87 ) 

88 self.fix_com = fixcm 

89 

90 if rng is None: 

91 self.rng = np.random 

92 else: 

93 self.rng = rng 

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

95 self.updatevars() 

96 

97 def todict(self): 

98 d = MolecularDynamics.todict(self) 

99 d.update( 

100 { 

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

102 'friction': self.fr, 

103 'fixcm': self.fix_com, 

104 } 

105 ) 

106 return d 

107 

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

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

110 temperature, temperature_K, 'eV' 

111 ) 

112 self.updatevars() 

113 

114 def set_friction(self, friction): 

115 self.fr = friction 

116 self.updatevars() 

117 

118 def set_timestep(self, timestep): 

119 self.dt = timestep 

120 self.updatevars() 

121 

122 def updatevars(self): 

123 dt = self.dt 

124 T = self.temp 

125 fr = self.fr 

126 masses = self.masses 

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

128 

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

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

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

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

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

134 

135 def step(self, forces=None): 

136 atoms = self.atoms 

137 natoms = len(atoms) 

138 

139 if forces is None: 

140 forces = atoms.get_forces(md=True) 

141 

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

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

144 # when atoms migrate between processors. 

145 self.v = atoms.get_velocities() 

146 

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

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

149 

150 # When holonomic constraints for rigid linear triatomic molecules are 

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

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

153 # correct target temperature. 

154 for constraint in self.atoms.constraints: 

155 if hasattr(constraint, 'redistribute_forces_md'): 

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

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

158 

159 self.comm.broadcast(xi, 0) 

160 self.comm.broadcast(eta, 0) 

161 

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

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

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

165 # temperature slightly, and we have to correct. 

166 self.rnd_pos = self.c5 * eta 

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

168 if self.fix_com: 

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

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

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

172 self.masses * natoms 

173 ) 

174 self.rnd_pos *= factor 

175 self.rnd_vel *= factor 

176 

177 # First halfstep in the velocity. 

178 self.v += ( 

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

180 ) 

181 

182 # Full step in positions 

183 x = atoms.get_positions() 

184 

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

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

187 

188 # recalc velocities after RATTLE constraints are applied 

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

190 forces = atoms.get_forces(md=True) 

191 

192 # Update the velocities 

193 self.v += ( 

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

195 ) 

196 

197 # Second part of RATTLE taken care of here 

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

199 

200 return forces