Coverage for ase / md / velocitydistribution.py: 78.33%

120 statements  

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

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

2 

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

4MaxwellBoltzmannDistribution, which sets the momenta of a list of 

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

6temperature. 

7""" 

8 

9import warnings 

10from typing import Literal 

11 

12import numpy as np 

13 

14from ase import Atoms, units 

15from ase.md.md import process_temperature 

16from ase.parallel import DummyMPI, 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( 

27 atoms: Atoms, 

28 temperature: float, 

29 unit: Literal['K', 'eV'] = 'K', 

30): 

31 """ 

32 Force the temperature of the atomic system to a precise value. 

33 

34 This function adjusts the momenta of the atoms to achieve the 

35 exact specified temperature, overriding the Maxwell-Boltzmann 

36 distribution. The temperature can be specified in either Kelvin 

37 (K) or electron volts (eV). 

38 

39 Parameters 

40 ---------- 

41 atoms 

42 The atomic system represented as an ASE Atoms object. 

43 temperature 

44 The exact temperature to force for the atomic system. 

45 unit 

46 The unit of the specified temperature. 

47 Can be either 'K' (Kelvin) or 'eV' (electron volts). Default is 'K'. 

48 """ 

49 

50 if unit == 'K': 

51 target_temp = temperature * units.kB 

52 elif unit == 'eV': 

53 target_temp = temperature 

54 else: 

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

56 

57 if temperature > eps_temp: 

58 ndof = atoms.get_number_of_degrees_of_freedom() 

59 current_temp = 2 * atoms.get_kinetic_energy() / ndof 

60 scale = target_temp / current_temp 

61 else: 

62 scale = 0.0 

63 

64 atoms.set_momenta(atoms.get_momenta() * np.sqrt(scale)) 

65 

66 

67def _maxwellboltzmanndistribution(masses, temp, comm=world, rng=None): 

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

69 

70 Parameters 

71 ---------- 

72 masses: float 

73 The atomic masses. 

74 

75 temp: float 

76 The temperature in electron volt. 

77 

78 comm: MPI communicator (optional, default: ase.parallel.world) 

79 Communicator used to distribute an identical distribution to all tasks. 

80 

81 rng: numpy RNG (optional) 

82 The random number generator. Default: np.random 

83 

84 Returns 

85 ------- 

86 np.ndarray 

87 Maxwell-Boltzmann distributed momenta. 

88 """ 

89 if rng is None: 

90 rng = np.random 

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

92 comm.broadcast(xi, 0) 

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

94 return momenta 

95 

96 

97def MaxwellBoltzmannDistribution( 

98 atoms: Atoms, 

99 temp: float | None = None, 

100 *, 

101 temperature_K: float | None = None, 

102 comm=world, 

103 communicator=None, 

104 force_temp: bool = False, 

105 rng=None, 

106): 

107 """Set the atomic momenta to a Maxwell-Boltzmann distribution. 

108 

109 Parameters 

110 ---------- 

111 atoms: Atoms object 

112 The atoms. Their momenta will be modified. 

113 

114 temp: float (deprecated) 

115 The temperature in eV. Deprecated, use ``temperature_K`` instead. 

116 

117 temperature_K: float 

118 The temperature in Kelvin. 

119 

120 comm: MPI communicator, default: :data:`ase.parallel.world` 

121 Communicator used to distribute an identical distribution to all tasks. 

122 

123 .. versionadded:: 3.26.0 

124 

125 communicator 

126 

127 .. deprecated:: 3.26.0 

128 

129 To be removed in ASE 3.27.0 in favor of ``comm``. 

130 

131 force_temp: bool (optional, default: False) 

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

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

134 Maxwell-Boltzmann distribution. 

135 

136 rng: Numpy RNG (optional) 

137 Random number generator. Default: numpy.random 

138 If you would like to always get the identical distribution, you can 

139 supply a random seed like ``rng=numpy.random.RandomState(seed)``, where 

140 seed is an integer. 

141 """ 

142 if communicator is not None: 

143 msg = ( 

144 '`communicator` has been deprecated since ASE 3.26.0 ' 

145 'and will be removed in ASE 3.27.0. Use `comm` instead.' 

146 ) 

147 warnings.warn(msg, FutureWarning) 

148 comm = communicator 

149 if comm == 'serial': 

150 msg = ( 

151 '`comm=="serial"` has been deprecated since ASE 3.26.0 ' 

152 'and will be removed in ASE 3.27.0. Use `comm=DummyMPI()` instead.' 

153 ) 

154 warnings.warn(msg, FutureWarning) 

155 comm = DummyMPI() 

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

157 masses = atoms.get_masses() 

158 momenta = _maxwellboltzmanndistribution(masses, temp, comm=comm, rng=rng) 

159 atoms.set_momenta(momenta) 

160 if force_temp: 

161 force_temperature(atoms, temperature=temp, unit='eV') 

162 

163 

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

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

166 

167 # Save initial temperature 

168 temp0 = atoms.get_temperature() 

169 

170 p = atoms.get_momenta() 

171 p0 = np.sum(p, 0) 

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

173 m = atoms.get_masses() 

174 mtot = np.sum(m) 

175 v0 = p0 / mtot 

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

177 atoms.set_momenta(p) 

178 

179 if preserve_temperature: 

180 force_temperature(atoms, temp0) 

181 

182 

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

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

185 

186 # Save initial temperature 

187 temp0 = atoms.get_temperature() 

188 

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

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

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

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

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

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

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

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

197 com = atoms.get_center_of_mass() 

198 positions = atoms.get_positions() 

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

200 velocities = atoms.get_velocities() 

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

202 

203 if preserve_temperature: 

204 force_temperature(atoms, temp0) 

205 

206 

207def n_BE(temp, omega): 

208 """Bose-Einstein distribution function. 

209 

210 Args: 

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

212 omega: sequence of frequencies converted to eV 

213 

214 Returns 

215 ------- 

216 Value of Bose-Einstein distribution function for each energy 

217 

218 """ 

219 

220 omega = np.asarray(omega) 

221 

222 # 0K limit 

223 if temp < eps_temp: 

224 n = np.zeros_like(omega) 

225 else: 

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

227 return n 

228 

229 

230def phonon_harmonics( 

231 force_constants, 

232 masses, 

233 temp=None, 

234 *, 

235 temperature_K=None, 

236 rng=np.random.rand, 

237 quantum=False, 

238 plus_minus=False, 

239 return_eigensolution=False, 

240 failfast=True, 

241): 

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

243 

244 Parameters 

245 ---------- 

246 force_constants: array of size 3N x 3N 

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

248 

249 masses: array of length N 

250 masses of the structure in amu 

251 

252 temp: float (deprecated) 

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

254 ``temperature_K``. 

255 

256 temperature_K: float 

257 Temperature in Kelvin. 

258 

259 rng: function 

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

261 

262 quantum: bool 

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

264 (classical limit) 

265 

266 plus_minus: bool 

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

268 

269 return_eigensolution: bool 

270 return eigenvalues and eigenvectors of the dynamical matrix 

271 

272 failfast: bool 

273 True for sanity checking the phonon spectrum for negative 

274 frequencies at Gamma 

275 

276 Returns 

277 ------- 

278 Displacements, velocities generated from the eigenmodes, 

279 (optional: eigenvalues, eigenvectors of dynamical matrix) 

280 

281 Notes 

282 ----- 

283 Excite phonon modes to specified temperature. 

284 

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

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

287 energy and kinetic energy will be consistent with the phononic 

288 vibrations characteristic of the specified temperature. 

289 

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

291 that temperature. 

292 

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

294 as computed by the ase.phonons module. 

295 

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

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

298 uniformly random numbers. Then 

299 

300 .. code-block:: none 

301 

302 

303 1/2 

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

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

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

307 a i i 

308 

309 

310 1/2 

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

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

313 a \ m / --- ai i i 

314 a i 

315 

316 References 

317 ---------- 

318 D. West and S. K. Estreicher, Phys. Rev. Lett. 96, 115504 (2006). 

319 https://doi.org/10.1103/PhysRevLett.96.115504 

320 

321 """ 

322 

323 # Handle the temperature units 

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

325 

326 # Build dynamical matrix 

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

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

329 

330 # Solve eigenvalue problem to compute phonon spectrum and eigenvectors 

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

332 

333 # Check for soft modes 

334 if failfast: 

335 zeros = w2_s[:3] 

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

337 if worst_zero > 1e-3: 

338 msg = 'Translational deviate from 0 significantly: ' 

339 raise ValueError(msg + f'{w2_s[:3]}') 

340 

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

342 if w2min < 0: 

343 msg = 'Dynamical matrix has negative eigenvalues such as ' 

344 raise ValueError(msg + f'{w2min}') 

345 

346 # First three modes are translational so ignore: 

347 nw = len(w2_s) - 3 

348 n_atoms = len(masses) 

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

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

351 

352 # Assign the amplitudes according to Bose-Einstein distribution 

353 # or high temperature (== classical) limit 

354 if quantum: 

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

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

357 else: 

358 A_s = np.sqrt(temp) / w_s 

359 

360 if plus_minus: 

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

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

363 

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

365 

366 # gauge eigenvectors: largest value always positive 

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

368 vec = X_acs[:, :, ii] 

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

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

371 

372 # Create velocities und displacements from the amplitudes and 

373 # eigenvectors 

374 A_s *= spread 

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

376 

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

378 # amplitude for displacement 

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

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

381 

382 # Assign displacements 

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

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

385 

386 else: 

387 # compute the gaussian distribution for the amplitudes 

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

389 # to avoid (highly improbable) NaN. 

390 

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

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

393 

394 # assign amplitudes and phases 

395 A_s *= spread 

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

397 

398 # Assign velocities and displacements 

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

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

401 

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

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

404 

405 if return_eigensolution: 

406 return d_ac, v_ac, w2_s, X_is 

407 # else 

408 return d_ac, v_ac 

409 

410 

411def PhononHarmonics( 

412 atoms, 

413 force_constants, 

414 temp=None, 

415 *, 

416 temperature_K=None, 

417 rng=np.random, 

418 quantum=False, 

419 plus_minus=False, 

420 return_eigensolution=False, 

421 failfast=True, 

422): 

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

424 

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

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

427 temperature. 

428 

429 Parameters 

430 ---------- 

431 atoms: :class:`~ase.Atoms` 

432 Positions and momenta of this object are perturbed. 

433 

434 force_constants: ndarray of size 3N x 3N 

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

436 

437 temp: float (deprecated). 

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

439 

440 temperature_K: float 

441 Temperature in Kelvin. 

442 

443 rng: Random number generator 

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

445 

446 quantum: bool 

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

448 (classical limit) 

449 

450 failfast: bool 

451 True for sanity checking the phonon spectrum for negative frequencies 

452 at Gamma. 

453 

454 """ 

455 

456 # Receive displacements and velocities from phonon_harmonics() 

457 d_ac, v_ac = phonon_harmonics( 

458 force_constants=force_constants, 

459 masses=atoms.get_masses(), 

460 temp=temp, 

461 temperature_K=temperature_K, 

462 rng=rng.rand, 

463 plus_minus=plus_minus, 

464 quantum=quantum, 

465 failfast=failfast, 

466 return_eigensolution=False, 

467 ) 

468 

469 # Assign new positions (with displacements) and velocities 

470 atoms.positions += d_ac 

471 atoms.set_velocities(v_ac)