Coverage for /builds/kinetik161/ase/ase/md/velocitydistribution.py: 82.46%

114 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-12-10 11:04 +0000

1# VelocityDistributions.py -- set up a velocity distribution 

2 

3"""Module for setting up velocity distributions such as Maxwell–Boltzmann. 

4 

5Currently, only a few functions are defined, such as 

6MaxwellBoltzmannDistribution, which sets the momenta of a list of 

7atoms according to a Maxwell-Boltzmann distribution at a given 

8temperature. 

9""" 

10from typing import Optional 

11 

12import numpy as np 

13 

14from ase import Atoms, units 

15from ase.md.md import process_temperature 

16from ase.parallel import world 

17 

18# define a ``zero'' temperature to avoid divisions by zero 

19eps_temp = 1e-12 

20 

21 

22class UnitError(Exception): 

23 """Exception raised when wrong units are specified""" 

24 

25 

26def force_temperature(atoms: Atoms, temperature: float, unit: str = "K"): 

27 """ force (nucl.) temperature to have a precise value 

28 

29 Parameters: 

30 atoms: ase.Atoms 

31 the structure 

32 temperature: float 

33 nuclear temperature to set 

34 unit: str 

35 'K' or 'eV' as unit for the temperature 

36 """ 

37 

38 if unit == "K": 

39 E_temp = temperature * units.kB 

40 elif unit == "eV": 

41 E_temp = temperature 

42 else: 

43 raise UnitError(f"'{unit}' is not supported, use 'K' or 'eV'.") 

44 

45 if temperature > eps_temp: 

46 E_kin0 = atoms.get_kinetic_energy() / len(atoms) / 1.5 

47 gamma = E_temp / E_kin0 

48 else: 

49 gamma = 0.0 

50 atoms.set_momenta(atoms.get_momenta() * np.sqrt(gamma)) 

51 

52 

53def _maxwellboltzmanndistribution(masses, temp, communicator=None, rng=None): 

54 """Return a Maxwell-Boltzmann distribution with a given temperature. 

55 

56 Paremeters: 

57 

58 masses: float 

59 The atomic masses. 

60 

61 temp: float 

62 The temperature in electron volt. 

63 

64 communicator: MPI communicator (optional) 

65 Communicator used to distribute an identical distribution to 

66 all tasks. Set to 'serial' to disable communication (setting to None 

67 gives the default). Default: ase.parallel.world 

68 

69 rng: numpy RNG (optional) 

70 The random number generator. Default: np.random 

71 

72 Returns: 

73 

74 A numpy array with Maxwell-Boltzmann distributed momenta. 

75 """ 

76 if rng is None: 

77 rng = np.random 

78 if communicator is None: 

79 communicator = world 

80 xi = rng.standard_normal((len(masses), 3)) 

81 if communicator != 'serial': 

82 communicator.broadcast(xi, 0) 

83 momenta = xi * np.sqrt(masses * temp)[:, np.newaxis] 

84 return momenta 

85 

86 

87def MaxwellBoltzmannDistribution( 

88 atoms: Atoms, 

89 temp: Optional[float] = None, 

90 *, 

91 temperature_K: Optional[float] = None, 

92 communicator=None, 

93 force_temp: bool = False, 

94 rng=None, 

95): 

96 """Sets the momenta to a Maxwell-Boltzmann distribution. 

97 

98 Parameters: 

99 

100 atoms: Atoms object 

101 The atoms. Their momenta will be modified. 

102 

103 temp: float (deprecated) 

104 The temperature in eV. Deprecated, used temperature_K instead. 

105 

106 temperature_K: float 

107 The temperature in Kelvin. 

108 

109 communicator: MPI communicator (optional) 

110 Communicator used to distribute an identical distribution to 

111 all tasks. Set to 'serial' to disable communication. Leave as None to 

112 get the default: ase.parallel.world 

113 

114 force_temp: bool (optinal, default: False) 

115 If True, random the momenta are rescaled so the kinetic energy is 

116 exactly 3/2 N k T. This is a slight deviation from the correct 

117 Maxwell-Boltzmann distribution. 

118 

119 rng: Numpy RNG (optional) 

120 Random number generator. Default: numpy.random 

121 """ 

122 temp = units.kB * process_temperature(temp, temperature_K, 'eV') 

123 masses = atoms.get_masses() 

124 momenta = _maxwellboltzmanndistribution(masses, temp, communicator, rng) 

125 atoms.set_momenta(momenta) 

126 if force_temp: 

127 force_temperature(atoms, temperature=temp, unit="eV") 

128 

129 

130def Stationary(atoms: Atoms, preserve_temperature: bool = True): 

131 "Sets the center-of-mass momentum to zero." 

132 

133 # Save initial temperature 

134 temp0 = atoms.get_temperature() 

135 

136 p = atoms.get_momenta() 

137 p0 = np.sum(p, 0) 

138 # We should add a constant velocity, not momentum, to the atoms 

139 m = atoms.get_masses() 

140 mtot = np.sum(m) 

141 v0 = p0 / mtot 

142 p -= v0 * m[:, np.newaxis] 

143 atoms.set_momenta(p) 

144 

145 if preserve_temperature: 

146 force_temperature(atoms, temp0) 

147 

148 

149def ZeroRotation(atoms: Atoms, preserve_temperature: float = True): 

150 "Sets the total angular momentum to zero by counteracting rigid rotations." 

151 

152 # Save initial temperature 

153 temp0 = atoms.get_temperature() 

154 

155 # Find the principal moments of inertia and principal axes basis vectors 

156 Ip, basis = atoms.get_moments_of_inertia(vectors=True) 

157 # Calculate the total angular momentum and transform to principal basis 

158 Lp = np.dot(basis, atoms.get_angular_momentum()) 

159 # Calculate the rotation velocity vector in the principal basis, avoiding 

160 # zero division, and transform it back to the cartesian coordinate system 

161 omega = np.dot(np.linalg.inv(basis), np.select([Ip > 0], [Lp / Ip])) 

162 # We subtract a rigid rotation corresponding to this rotation vector 

163 com = atoms.get_center_of_mass() 

164 positions = atoms.get_positions() 

165 positions -= com # translate center of mass to origin 

166 velocities = atoms.get_velocities() 

167 atoms.set_velocities(velocities - np.cross(omega, positions)) 

168 

169 if preserve_temperature: 

170 force_temperature(atoms, temp0) 

171 

172 

173def n_BE(temp, omega): 

174 """Bose-Einstein distribution function. 

175 

176 Args: 

177 temp: temperature converted to eV (*units.kB) 

178 omega: sequence of frequencies converted to eV 

179 

180 Returns: 

181 Value of Bose-Einstein distribution function for each energy 

182 

183 """ 

184 

185 omega = np.asarray(omega) 

186 

187 # 0K limit 

188 if temp < eps_temp: 

189 n = np.zeros_like(omega) 

190 else: 

191 n = 1 / (np.exp(omega / (temp)) - 1) 

192 return n 

193 

194 

195def phonon_harmonics( 

196 force_constants, 

197 masses, 

198 temp=None, 

199 *, 

200 temperature_K=None, 

201 rng=np.random.rand, 

202 quantum=False, 

203 plus_minus=False, 

204 return_eigensolution=False, 

205 failfast=True, 

206): 

207 r"""Return displacements and velocities that produce a given temperature. 

208 

209 Parameters: 

210 

211 force_constants: array of size 3N x 3N 

212 force constants (Hessian) of the system in eV/Ų 

213 

214 masses: array of length N 

215 masses of the structure in amu 

216 

217 temp: float (deprecated) 

218 Temperature converted to eV (T * units.kB). Deprecated, use 

219 ``temperature_K``. 

220 

221 temperature_K: float 

222 Temperature in Kelvin. 

223 

224 rng: function 

225 Random number generator function, e.g., np.random.rand 

226 

227 quantum: bool 

228 True for Bose-Einstein distribution, False for Maxwell-Boltzmann 

229 (classical limit) 

230 

231 plus_minus: bool 

232 Displace atoms with +/- the amplitude accoding to PRB 94, 075125 

233 

234 return_eigensolution: bool 

235 return eigenvalues and eigenvectors of the dynamical matrix 

236 

237 failfast: bool 

238 True for sanity checking the phonon spectrum for negative 

239 frequencies at Gamma 

240 

241 Returns: 

242 

243 Displacements, velocities generated from the eigenmodes, 

244 (optional: eigenvalues, eigenvectors of dynamical matrix) 

245 

246 Purpose: 

247 

248 Excite phonon modes to specified temperature. 

249 

250 This excites all phonon modes randomly so that each contributes, 

251 on average, equally to the given temperature. Both potential 

252 energy and kinetic energy will be consistent with the phononic 

253 vibrations characteristic of the specified temperature. 

254 

255 In other words the system will be equilibrated for an MD run at 

256 that temperature. 

257 

258 force_constants should be the matrix as force constants, e.g., 

259 as computed by the ase.phonons module. 

260 

261 Let X_ai be the phonon modes indexed by atom and mode, w_i the 

262 phonon frequencies, and let 0 < Q_i <= 1 and 0 <= R_i < 1 be 

263 uniformly random numbers. Then 

264 

265 .. code-block:: none 

266 

267 

268 1/2 

269 _ / k T \ --- 1 _ 1/2 

270 R += | --- | > --- X (-2 ln Q ) cos (2 pi R ) 

271 a \ m / --- w ai i i 

272 a i i 

273 

274 

275 1/2 

276 _ / k T \ --- _ 1/2 

277 v = | --- | > X (-2 ln Q ) sin (2 pi R ) 

278 a \ m / --- ai i i 

279 a i 

280 

281 Reference: [West, Estreicher; PRL 96, 22 (2006)] 

282 """ 

283 

284 # Handle the temperature units 

285 temp = units.kB * process_temperature(temp, temperature_K, 'eV') 

286 

287 # Build dynamical matrix 

288 rminv = (masses ** -0.5).repeat(3) 

289 dynamical_matrix = force_constants * rminv[:, None] * rminv[None, :] 

290 

291 # Solve eigenvalue problem to compute phonon spectrum and eigenvectors 

292 w2_s, X_is = np.linalg.eigh(dynamical_matrix) 

293 

294 # Check for soft modes 

295 if failfast: 

296 zeros = w2_s[:3] 

297 worst_zero = np.abs(zeros).max() 

298 if worst_zero > 1e-3: 

299 msg = "Translational deviate from 0 significantly: " 

300 raise ValueError(msg + f"{w2_s[:3]}") 

301 

302 w2min = w2_s[3:].min() 

303 if w2min < 0: 

304 msg = "Dynamical matrix has negative eigenvalues such as " 

305 raise ValueError(msg + f"{w2min}") 

306 

307 # First three modes are translational so ignore: 

308 nw = len(w2_s) - 3 

309 n_atoms = len(masses) 

310 w_s = np.sqrt(w2_s[3:]) 

311 X_acs = X_is[:, 3:].reshape(n_atoms, 3, nw) 

312 

313 # Assign the amplitudes according to Bose-Einstein distribution 

314 # or high temperature (== classical) limit 

315 if quantum: 

316 hbar = units._hbar * units.J * units.s 

317 A_s = np.sqrt(hbar * (2 * n_BE(temp, hbar * w_s) + 1) / (2 * w_s)) 

318 else: 

319 A_s = np.sqrt(temp) / w_s 

320 

321 if plus_minus: 

322 # create samples by multiplying the amplitude with +/- 

323 # according to Eq. 5 in PRB 94, 075125 

324 

325 spread = (-1) ** np.arange(nw) 

326 

327 # gauge eigenvectors: largest value always positive 

328 for ii in range(X_acs.shape[-1]): 

329 vec = X_acs[:, :, ii] 

330 max_arg = np.argmax(abs(vec)) 

331 X_acs[:, :, ii] *= np.sign(vec.flat[max_arg]) 

332 

333 # Create velocities und displacements from the amplitudes and 

334 # eigenvectors 

335 A_s *= spread 

336 phi_s = 2.0 * np.pi * rng(nw) 

337 

338 # Assign velocities, sqrt(2) compensates for missing sin(phi) in 

339 # amplitude for displacement 

340 v_ac = (w_s * A_s * np.sqrt(2) * np.cos(phi_s) * X_acs).sum(axis=2) 

341 v_ac /= np.sqrt(masses)[:, None] 

342 

343 # Assign displacements 

344 d_ac = (A_s * X_acs).sum(axis=2) 

345 d_ac /= np.sqrt(masses)[:, None] 

346 

347 else: 

348 # compute the gaussian distribution for the amplitudes 

349 # We need 0 < P <= 1.0 and not 0 0 <= P < 1.0 for the logarithm 

350 # to avoid (highly improbable) NaN. 

351 

352 # Box Muller [en.wikipedia.org/wiki/Box–Muller_transform]: 

353 spread = np.sqrt(-2.0 * np.log(1.0 - rng(nw))) 

354 

355 # assign amplitudes and phases 

356 A_s *= spread 

357 phi_s = 2.0 * np.pi * rng(nw) 

358 

359 # Assign velocities and displacements 

360 v_ac = (w_s * A_s * np.cos(phi_s) * X_acs).sum(axis=2) 

361 v_ac /= np.sqrt(masses)[:, None] 

362 

363 d_ac = (A_s * np.sin(phi_s) * X_acs).sum(axis=2) 

364 d_ac /= np.sqrt(masses)[:, None] 

365 

366 if return_eigensolution: 

367 return d_ac, v_ac, w2_s, X_is 

368 # else 

369 return d_ac, v_ac 

370 

371 

372def PhononHarmonics( 

373 atoms, 

374 force_constants, 

375 temp=None, 

376 *, 

377 temperature_K=None, 

378 rng=np.random, 

379 quantum=False, 

380 plus_minus=False, 

381 return_eigensolution=False, 

382 failfast=True, 

383): 

384 r"""Excite phonon modes to specified temperature. 

385 

386 This will displace atomic positions and set the velocities so as 

387 to produce a random, phononically correct state with the requested 

388 temperature. 

389 

390 Parameters: 

391 

392 atoms: ase.atoms.Atoms() object 

393 Positions and momenta of this object are perturbed. 

394 

395 force_constants: ndarray of size 3N x 3N 

396 Force constants for the the structure represented by atoms in eV/Ų 

397 

398 temp: float (deprecated). 

399 Temperature in eV. Deprecated, use ``temperature_K`` instead. 

400 

401 temperature_K: float 

402 Temperature in Kelvin. 

403 

404 rng: Random number generator 

405 RandomState or other random number generator, e.g., np.random.rand 

406 

407 quantum: bool 

408 True for Bose-Einstein distribution, False for Maxwell-Boltzmann 

409 (classical limit) 

410 

411 failfast: bool 

412 True for sanity checking the phonon spectrum for negative frequencies 

413 at Gamma. 

414 

415 """ 

416 

417 # Receive displacements and velocities from phonon_harmonics() 

418 d_ac, v_ac = phonon_harmonics( 

419 force_constants=force_constants, 

420 masses=atoms.get_masses(), 

421 temp=temp, 

422 temperature_K=temperature_K, 

423 rng=rng.rand, 

424 plus_minus=plus_minus, 

425 quantum=quantum, 

426 failfast=failfast, 

427 return_eigensolution=False, 

428 ) 

429 

430 # Assign new positions (with displacements) and velocities 

431 atoms.positions += d_ac 

432 atoms.set_velocities(v_ac)