Coverage for ase / md / melchionna.py: 67.49%

283 statements  

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

1# fmt: off 

2 

3'''Constant pressure/stress and temperature dynamics. 

4 

5**This dynamics is not recommended due to stability problems.** 

6 

7Combined Nose-Hoover and Parrinello-Rahman dynamics, creating an NPT 

8(or N,stress,T) ensemble. 

9 

10The method is the one proposed by Melchionna et al. [1] and later 

11modified by Melchionna [2]. The differential equations are integrated 

12using a centered difference method [3]. 

13 

14 1. S. Melchionna, G. Ciccotti and B. L. Holian, "Hoover NPT dynamics 

15 for systems varying in shape and size", Molecular Physics 78, p. 533 

16 (1993). 

17 

18 2. S. Melchionna, "Constrained systems and statistical distribution", 

19 Physical Review E, 61, p. 6165 (2000). 

20 

21 3. B. L. Holian, A. J. De Groot, W. G. Hoover, and C. G. Hoover, 

22 "Time-reversible equilibrium and nonequilibrium isothermal-isobaric 

23 simulations with centered-difference Stoermer algorithms.", Physical 

24 Review A, 41, p. 4552 (1990). 

25''' 

26import sys 

27import weakref 

28from typing import IO 

29 

30import numpy as np 

31 

32from ase import Atoms, units 

33from ase.md.md import MolecularDynamics 

34 

35linalg = np.linalg 

36 

37# Delayed imports: If the trajectory object is reading a special ASAP version 

38# of HooverNPT, that class is imported from Asap.Dynamics.NPTDynamics. 

39 

40 

41class MelchionnaNPT(MolecularDynamics): 

42 

43 classname = "MelchionnaNPT" # Used by the trajectory. 

44 _npt_version = 2 # Version number, used for Asap compatibility. 

45 

46 def __init__( 

47 self, 

48 atoms: Atoms, 

49 timestep: float, 

50 temperature: float | None = None, 

51 externalstress: float | None = None, 

52 ttime: float | None = None, 

53 pfactor: float | None = None, 

54 *, 

55 temperature_K: float | None = None, 

56 mask: tuple[int] | np.ndarray | None = None, 

57 trajectory: str | None = None, 

58 logfile: IO | str | None = None, 

59 loginterval: int = 1, 

60 append_trajectory: bool = False, 

61 ): 

62 '''Constant pressure/stress and temperature dynamics. 

63 

64 Combined Nose-Hoover and Parrinello-Rahman dynamics, creating an 

65 NPT (or N,stress,T) ensemble. 

66 

67 The method is the one proposed by Melchionna et al. [1]_ and later 

68 modified by Melchionna [2]_. The differential equations are integrated 

69 using a centered difference method [3]_. See also NPTdynamics.tex 

70 

71 Parameters 

72 ---------- 

73 atoms: Atoms object 

74 The list of atoms. 

75 

76 timestep: float 

77 The timestep in units matching eV, Å, u. 

78 

79 temperature: float (deprecated) 

80 The desired temperature in eV. 

81 

82 temperature_K: float 

83 The desired temperature in K. 

84 

85 externalstress: float or nparray 

86 The external stress in eV/A^3. Either a symmetric 

87 3x3 tensor, a 6-vector representing the same, or a 

88 scalar representing the pressure. Note that the 

89 stress is positive in tension whereas the pressure is 

90 positive in compression: giving a scalar p is 

91 equivalent to giving the tensor (-p, -p, -p, 0, 0, 0). 

92 

93 ttime: float 

94 Characteristic timescale of the thermostat, in ASE internal units 

95 Set to None to disable the thermostat. 

96 

97 WARNING: Not specifying ttime sets it to None, disabling the 

98 thermostat. 

99 

100 pfactor: float 

101 A constant in the barostat differential equation. If 

102 a characteristic barostat timescale of ptime is 

103 desired, set pfactor to ptime^2 * B 

104 (where ptime is in units matching 

105 eV, Å, u; and B is the Bulk Modulus, given in eV/Å^3). 

106 Set to None to disable the barostat. 

107 Typical metallic bulk moduli are of the order of 

108 100 GPa or 0.6 eV/A^3. 

109 

110 WARNING: Not specifying pfactor sets it to None, disabling the 

111 barostat. 

112 

113 mask: None or 3-tuple or 3x3 nparray (optional) 

114 Optional argument. A tuple of three integers (0 or 1), 

115 indicating if the system can change size along the 

116 three Cartesian axes. Set to (1,1,1) or None to allow 

117 a fully flexible computational box. Set to (1,1,0) 

118 to disallow elongations along the z-axis etc. 

119 mask may also be specified as a symmetric 3x3 array 

120 indicating which strain values may change. 

121 

122 Notes 

123 ----- 

124 * The same timestep can be used as in Verlet dynamics, i.e. 5 fs is fine 

125 for bulk copper. 

126 

127 * The ttime and pfactor are quite critical [4]_, too small values may 

128 cause instabilites and/or wrong fluctuations in T / p. Too 

129 large values cause an oscillation which is slow to die. Good 

130 values for the characteristic times seem to be 25 fs for ttime, 

131 and 75 fs for ptime (used to calculate pfactor), at least for 

132 bulk copper with 15000-200000 atoms. But this is not well 

133 tested, it is IMPORTANT to monitor the temperature and 

134 stress/pressure fluctuations. 

135 

136 References 

137 ---------- 

138 .. [1] S. Melchionna, G. Ciccotti and B. L. Holian, 

139 Mol. Phys. 78, 533–544 (1993). 

140 https://doi.org/10.1080/00268979300100371 

141 

142 .. [2] S. Melchionna, 

143 Phys. Rev. E 61, 6165–6170 (2000). 

144 https://doi.org/10.1103/PhysRevE.61.6165 

145 

146 .. [3] B. L. Holian, A. J. De Groot, W. G. Hoover, and C. G. Hoover, 

147 Phys. Rev. A 41, 4552–4553 (1990). 

148 https://doi.org/10.1103/PhysRevA.41.4552 

149 

150 .. [4] F. D. Di Tolla and M. Ronchetti, 

151 Phys. Rev. E 48, 1726–1737 (1993). 

152 https://doi.org/10.1103/PhysRevE.48.1726 

153 

154 ''' 

155 

156 MolecularDynamics.__init__(self, atoms, timestep, trajectory, 

157 logfile, loginterval, 

158 append_trajectory=append_trajectory) 

159 # self.atoms = atoms 

160 # self.timestep = timestep 

161 if externalstress is None and pfactor is not None: 

162 raise TypeError("Missing 'externalstress' argument.") 

163 self.zero_center_of_mass_momentum(verbose=1) 

164 self.temperature = units.kB * self._process_temperature( 

165 temperature, temperature_K, 'eV') 

166 if externalstress is not None: 

167 self.set_stress(externalstress) 

168 self.set_mask(mask) 

169 self.eta = np.zeros((3, 3), float) 

170 self.zeta = 0.0 

171 self.zeta_integrated = 0.0 

172 self.initialized = 0 

173 self.ttime = ttime 

174 self.pfactor_given = pfactor 

175 self._calculateconstants() 

176 self.timeelapsed = 0.0 

177 self.frac_traceless = 1 

178 

179 def set_temperature(self, temperature=None, *, temperature_K=None): 

180 """Set the temperature. 

181 

182 Parameters 

183 ---------- 

184 

185 temperature: float (deprecated) 

186 The new temperature in eV. Deprecated, use ``temperature_K``. 

187 

188 temperature_K: float (keyword-only argument) 

189 The new temperature, in K. 

190 """ 

191 self.temperature = units.kB * self._process_temperature( 

192 temperature, temperature_K, 'eV') 

193 self._calculateconstants() 

194 

195 def set_stress(self, stress): 

196 """Set the applied stress. 

197 

198 Must be a symmetric 3x3 tensor, a 6-vector representing a symmetric 

199 3x3 tensor, or a number representing the pressure. 

200 

201 Use with care, it is better to set the correct stress when creating 

202 the object. 

203 """ 

204 

205 if np.isscalar(stress): 

206 stress = np.array([-stress, -stress, -stress, 0.0, 0.0, 0.0]) 

207 else: 

208 stress = np.array(stress) 

209 if stress.shape == (3, 3): 

210 if not self._issymmetric(stress): 

211 raise ValueError( 

212 "The external stress must be a symmetric tensor.") 

213 stress = np.array((stress[0, 0], stress[1, 1], 

214 stress[2, 2], stress[1, 2], 

215 stress[0, 2], stress[0, 1])) 

216 elif stress.shape != (6,): 

217 raise ValueError("The external stress has the wrong shape.") 

218 self.externalstress = stress 

219 

220 def set_mask(self, mask): 

221 """Set the mask indicating dynamic elements of the computational box. 

222 

223 If set to None, all elements may change. If set to a 3-vector 

224 of ones and zeros, elements which are zero specify directions 

225 along which the size of the computational box cannot change. 

226 For example, if mask = (1,1,0) the length of the system along 

227 the z-axis cannot change, although xz and yz shear is still 

228 possible. May also be specified as a symmetric 3x3 array indicating 

229 which strain values may change. 

230 

231 Use with care, as you may "freeze in" a fluctuation in the strain rate. 

232 """ 

233 if mask is None: 

234 mask = np.ones((3,)) 

235 if not hasattr(mask, "shape"): 

236 mask = np.array(mask) 

237 if mask.shape != (3,) and mask.shape != (3, 3): 

238 raise RuntimeError('The mask has the wrong shape ' + 

239 '(must be a 3-vector or 3x3 matrix)') 

240 else: 

241 mask = np.not_equal(mask, 0) # Make sure it is 0/1 

242 

243 if mask.shape == (3,): 

244 self.mask = np.outer(mask, mask) 

245 else: 

246 self.mask = mask 

247 

248 def set_fraction_traceless(self, fracTraceless): 

249 """set what fraction of the traceless part of the force 

250 on eta is kept. 

251 

252 By setting this to zero, the volume may change but the shape may not. 

253 """ 

254 self.frac_traceless = fracTraceless 

255 

256 def get_strain_rate(self): 

257 """Get the strain rate as a triangular 3x3 matrix. 

258 

259 This includes the fluctuations in the shape of the computational box. 

260 

261 """ 

262 return np.array(self.eta, copy=1) 

263 

264 def set_strain_rate(self, rate): 

265 """Set the strain rate. Must be a triangular 3x3 matrix. 

266 

267 If you set a strain rate along a direction that is "masked out" 

268 (see ``set_mask``), the strain rate along that direction will be 

269 maintained constantly. 

270 """ 

271 if not (rate.shape == (3, 3) and self._triangular(rate)): 

272 raise ValueError("Strain rate must be a triangular matrix.") 

273 self.eta = rate 

274 if self.initialized: 

275 # Recalculate h_past and eta_past so they match the current value. 

276 self._initialize_eta_h() 

277 

278 def get_time(self): 

279 "Get the elapsed time." 

280 return self.timeelapsed 

281 

282 def irun(self, steps): 

283 """Run dynamics algorithm as generator. 

284 

285 Parameters 

286 ---------- 

287 steps : int 

288 Number of dynamics steps to be run. 

289 

290 Yields 

291 ------ 

292 complete : bool 

293 True if the maximum number of steps are reached. 

294 """ 

295 

296 if not self.initialized: 

297 self.initialize() 

298 else: 

299 if self.have_the_atoms_been_changed(): 

300 raise NotImplementedError( 

301 "You have modified the atoms since the last timestep.") 

302 

303 yield from super().irun(steps) 

304 

305 def run(self, steps): 

306 """Perform a number of time steps. 

307 

308 Parameters 

309 ---------- 

310 steps : int 

311 Number of dynamics steps to be run. 

312 

313 Yields 

314 ------ 

315 complete : bool 

316 True if the maximum number of steps are reached. 

317 """ 

318 

319 for complete in self.irun(steps): 

320 pass 

321 return complete 

322 

323 def have_the_atoms_been_changed(self): 

324 "Checks if the user has modified the positions or momenta of the atoms" 

325 limit = 1e-10 

326 h = self._getbox() 

327 if max(abs((h - self.h).ravel())) > limit: 

328 self._warning("The computational box has been modified.") 

329 return 1 

330 expected_r = np.dot(self.q + 0.5, h) 

331 err = max(abs((expected_r - self.atoms.get_positions()).ravel())) 

332 if err > limit: 

333 self._warning("The atomic positions have been modified: " + 

334 str(err)) 

335 return 1 

336 return 0 

337 

338 def step(self): 

339 """Perform a single time step. 

340 

341 Assumes that the forces and stresses are up to date, and that 

342 the positions and momenta have not been changed since last 

343 timestep. 

344 """ 

345 

346 # Assumes the following variables are OK 

347 # q_past, q, q_future, p, eta, eta_past, zeta, zeta_past, h, h_past 

348 # 

349 # q corresponds to the current positions 

350 # p must be equal to self.atoms.GetCartesianMomenta() 

351 # h must be equal to self.atoms.GetUnitCell() 

352 # 

353 # print "Making a timestep" 

354 dt = self.dt 

355 h_future = self.h_past + 2 * dt * np.dot(self.h, self.eta) 

356 if self.pfactor_given is None: 

357 deltaeta = np.zeros(6, float) 

358 else: 

359 stress = self.stresscalculator() 

360 deltaeta = -2 * dt * (self.pfact * linalg.det(self.h) * 

361 (stress - self.externalstress)) 

362 

363 if self.frac_traceless == 1: 

364 eta_future = self.eta_past + self.mask * \ 

365 self._maketriangular(deltaeta) 

366 else: 

367 trace_part, traceless_part = self._separatetrace( 

368 self._maketriangular(deltaeta)) 

369 eta_future = (self.eta_past + trace_part + 

370 self.frac_traceless * traceless_part) 

371 

372 deltazeta = 2 * dt * self.tfact * (self.atoms.get_kinetic_energy() - 

373 self.desiredEkin) 

374 zeta_future = self.zeta_past + deltazeta 

375 # Advance time 

376 self.timeelapsed += dt 

377 self.h_past = self.h 

378 self.h = h_future 

379 self.inv_h = linalg.inv(self.h) 

380 self.q_past = self.q 

381 self.q = self.q_future 

382 self._setbox_and_positions(self.h, self.q) 

383 self.eta_past = self.eta 

384 self.eta = eta_future 

385 self.zeta_past = self.zeta 

386 self.zeta = zeta_future 

387 self._synchronize() # for parallel simulations. 

388 self.zeta_integrated += dt * self.zeta 

389 force = self.forcecalculator() 

390 self._calculate_q_future(force) 

391 self.atoms.set_momenta( 

392 np.dot(self.q_future - self.q_past, self.h / (2 * dt)) * 

393 self._getmasses()) 

394 # self.stresscalculator() 

395 

396 def forcecalculator(self): 

397 return self.atoms.get_forces(md=True) 

398 

399 def stresscalculator(self): 

400 return self.atoms.get_stress(include_ideal_gas=True) 

401 

402 def initialize(self): 

403 """Initialize the dynamics. 

404 

405 The dynamics requires positions etc for the two last times to 

406 do a timestep, so the algorithm is not self-starting. This 

407 method performs a 'backwards' timestep to generate a 

408 configuration before the current. 

409 

410 This is called automatically the first time ``run()`` is called. 

411 """ 

412 # print "Initializing the NPT dynamics." 

413 dt = self.dt 

414 atoms = self.atoms 

415 self.h = self._getbox() 

416 if not self._istriangular(self.h): 

417 raise NotImplementedError( 

418 f"Can (so far) only operate on lists of atoms where the " 

419 f"computational box is a triangular matrix. {self.h}") 

420 self.inv_h = linalg.inv(self.h) 

421 # The contents of the q arrays should migrate in parallel simulations. 

422 # self._make_special_q_arrays() 

423 self.q = np.dot(self.atoms.get_positions(), self.inv_h) - 0.5 

424 # zeta and eta were set in __init__ 

425 self._initialize_eta_h() 

426 deltazeta = dt * self.tfact * (atoms.get_kinetic_energy() - 

427 self.desiredEkin) 

428 self.zeta_past = self.zeta - deltazeta 

429 self._calculate_q_past_and_future() 

430 self.initialized = 1 

431 

432 def get_gibbs_free_energy(self): 

433 """Return the Gibb's free energy, which is supposed to be conserved. 

434 

435 Requires that the energies of the atoms are up to date. 

436 

437 This is mainly intended as a diagnostic tool. If called before the 

438 first timestep, Initialize will be called. 

439 """ 

440 if not self.initialized: 

441 self.initialize() 

442 n = self._getnatoms() 

443 # tretaTeta = sum(diagonal(matrixmultiply(transpose(self.eta), 

444 # self.eta))) 

445 contractedeta = np.sum((self.eta * self.eta).ravel()) 

446 gibbs = (self.atoms.get_potential_energy() + 

447 self.atoms.get_kinetic_energy() 

448 - np.sum(self.externalstress[0:3]) * linalg.det(self.h) / 3.0) 

449 if self.ttime is not None: 

450 gibbs += (1.5 * n * self.temperature * 

451 (self.ttime * self.zeta)**2 + 

452 3 * self.temperature * (n - 1) * self.zeta_integrated) 

453 else: 

454 assert self.zeta == 0.0 

455 if self.pfactor_given is not None: 

456 gibbs += 0.5 / self.pfact * contractedeta 

457 else: 

458 assert contractedeta == 0.0 

459 return gibbs 

460 

461 def get_center_of_mass_momentum(self): 

462 "Get the center of mass momentum." 

463 return self.atoms.get_momenta().sum(0) 

464 

465 def zero_center_of_mass_momentum(self, verbose=0): 

466 "Set the center of mass momentum to zero." 

467 cm = self.get_center_of_mass_momentum() 

468 abscm = np.sqrt(np.sum(cm * cm)) 

469 if verbose and abscm > 1e-4: 

470 self._warning( 

471 self.classname + 

472 ": Setting the center-of-mass momentum to zero " 

473 "(was %.6g %.6g %.6g)" % tuple(cm)) 

474 self.atoms.set_momenta(self.atoms.get_momenta() - 

475 cm / self._getnatoms()) 

476 

477 def attach_atoms(self, atoms): 

478 """Assign atoms to a restored dynamics object. 

479 

480 This function must be called to set the atoms immediately after the 

481 dynamics object has been read from a trajectory. 

482 """ 

483 try: 

484 self.atoms 

485 except AttributeError: 

486 pass 

487 else: 

488 raise RuntimeError("Cannot call attach_atoms on a dynamics " 

489 "which already has atoms.") 

490 MolecularDynamics.__init__(self, atoms, self.dt) 

491 limit = 1e-6 

492 h = self._getbox() 

493 if max(abs((h - self.h).ravel())) > limit: 

494 raise RuntimeError( 

495 "The unit cell of the atoms does not match " 

496 "the unit cell stored in the file.") 

497 self.inv_h = linalg.inv(self.h) 

498 self.q = np.dot(self.atoms.get_positions(), self.inv_h) - 0.5 

499 self._calculate_q_past_and_future() 

500 self.initialized = 1 

501 

502 def attach(self, function, interval=1, *args, **kwargs): 

503 """Attach callback function or trajectory. 

504 

505 At every *interval* steps, call *function* with arguments 

506 *args* and keyword arguments *kwargs*. 

507 

508 If *function* is a trajectory object, its write() method is 

509 attached, but if *function* is a BundleTrajectory (or another 

510 trajectory supporting set_extra_data(), said method is first 

511 used to instruct the trajectory to also save internal 

512 data from the NPT dynamics object. 

513 """ 

514 if hasattr(function, "set_extra_data"): 

515 # We are attaching a BundleTrajectory or similar 

516 function.set_extra_data("npt_init", 

517 WeakMethodWrapper(self, "get_init_data"), 

518 once=True) 

519 function.set_extra_data("npt_dynamics", 

520 WeakMethodWrapper(self, "get_data")) 

521 MolecularDynamics.attach(self, function, interval, *args, **kwargs) 

522 

523 def get_init_data(self): 

524 "Return the data needed to initialize a new NPT dynamics." 

525 return {'dt': self.dt, 

526 'temperature': self.temperature, 

527 'desiredEkin': self.desiredEkin, 

528 'externalstress': self.externalstress, 

529 'mask': self.mask, 

530 'ttime': self.ttime, 

531 'tfact': self.tfact, 

532 'pfactor_given': self.pfactor_given, 

533 'pfact': self.pfact, 

534 'frac_traceless': self.frac_traceless} 

535 

536 def get_data(self): 

537 "Return data needed to restore the state." 

538 return {'eta': self.eta, 

539 'eta_past': self.eta_past, 

540 'zeta': self.zeta, 

541 'zeta_past': self.zeta_past, 

542 'zeta_integrated': self.zeta_integrated, 

543 'h': self.h, 

544 'h_past': self.h_past, 

545 'timeelapsed': self.timeelapsed} 

546 

547 @classmethod 

548 def read_from_trajectory(cls, trajectory, frame=-1, atoms=None): 

549 """Read dynamics and atoms from trajectory (Class method). 

550 

551 Simultaneously reads the atoms and the dynamics from a BundleTrajectory, 

552 including the internal data of the NPT dynamics object (automatically 

553 saved when attaching a BundleTrajectory to an NPT object). 

554 

555 Arguments: 

556 

557 trajectory 

558 The filename or an open BundleTrajectory object. 

559 

560 frame (optional) 

561 Which frame to read. Default: the last. 

562 

563 atoms (optional, internal use only) 

564 Pre-read atoms. Do not use. 

565 """ 

566 if isinstance(trajectory, str): 

567 if trajectory.endswith('/'): 

568 trajectory = trajectory[:-1] 

569 if trajectory.endswith('.bundle'): 

570 from ase.io.bundletrajectory import BundleTrajectory 

571 trajectory = BundleTrajectory(trajectory) 

572 else: 

573 raise ValueError( 

574 f"Cannot open '{trajectory}': unsupported file format") 

575 # trajectory is now a BundleTrajectory object (or compatible) 

576 if atoms is None: 

577 atoms = trajectory[frame] 

578 init_data = trajectory.read_extra_data('npt_init', 0) 

579 frame_data = trajectory.read_extra_data('npt_dynamics', frame) 

580 dyn = cls(atoms, timestep=init_data['dt'], 

581 temperature=init_data['temperature'], 

582 externalstress=init_data['externalstress'], 

583 ttime=init_data['ttime'], 

584 pfactor=init_data['pfactor_given'], 

585 mask=init_data['mask']) 

586 dyn.desiredEkin = init_data['desiredEkin'] 

587 dyn.tfact = init_data['tfact'] 

588 dyn.pfact = init_data['pfact'] 

589 dyn.frac_traceless = init_data['frac_traceless'] 

590 for k, v in frame_data.items(): 

591 setattr(dyn, k, v) 

592 return (dyn, atoms) 

593 

594 def _getbox(self): 

595 "Get the computational box." 

596 return self.atoms.get_cell() 

597 

598 def _getmasses(self): 

599 "Get the masses as an Nx1 array." 

600 return np.reshape(self.atoms.get_masses(), (-1, 1)) 

601 

602 def _separatetrace(self, mat): 

603 """return two matrices, one proportional to the identity 

604 the other traceless, which sum to the given matrix 

605 """ 

606 tracePart = ((mat[0][0] + mat[1][1] + mat[2][2]) / 3.) * np.identity(3) 

607 return tracePart, mat - tracePart 

608 

609 # A number of convenient helper methods 

610 def _warning(self, text): 

611 "Emit a warning." 

612 sys.stderr.write("WARNING: " + text + "\n") 

613 sys.stderr.flush() 

614 

615 def _calculate_q_future(self, force): 

616 "Calculate future q. Needed in Timestep and Initialization." 

617 dt = self.dt 

618 id3 = np.identity(3) 

619 alpha = (dt * dt) * np.dot(force / self._getmasses(), 

620 self.inv_h) 

621 beta = dt * np.dot(self.h, np.dot(self.eta + 0.5 * self.zeta * id3, 

622 self.inv_h)) 

623 inv_b = linalg.inv(beta + id3) 

624 self.q_future = np.dot(2 * self.q + 

625 np.dot(self.q_past, beta - id3) + alpha, 

626 inv_b) 

627 

628 def _calculate_q_past_and_future(self): 

629 def ekin(p, m=self.atoms.get_masses()): 

630 p2 = np.sum(p * p, -1) 

631 return 0.5 * np.sum(p2 / m) / len(m) 

632 p0 = self.atoms.get_momenta() 

633 m = self._getmasses() 

634 p = np.array(p0, copy=1) 

635 dt = self.dt 

636 for i in range(2): 

637 self.q_past = self.q - dt * np.dot(p / m, self.inv_h) 

638 self._calculate_q_future(self.atoms.get_forces(md=True)) 

639 p = np.dot(self.q_future - self.q_past, self.h / (2 * dt)) * m 

640 e = ekin(p) 

641 if e < 1e-5: 

642 # The kinetic energy and momenta are virtually zero 

643 return 

644 p = (p0 - p) + p0 

645 

646 def _initialize_eta_h(self): 

647 self.h_past = self.h - self.dt * np.dot(self.h, self.eta) 

648 if self.pfactor_given is None: 

649 deltaeta = np.zeros(6, float) 

650 else: 

651 deltaeta = (-self.dt * self.pfact * linalg.det(self.h) 

652 * (self.stresscalculator() - self.externalstress)) 

653 if self.frac_traceless == 1: 

654 self.eta_past = self.eta - self.mask * \ 

655 self._maketriangular(deltaeta) 

656 else: 

657 trace_part, traceless_part = self._separatetrace( 

658 self._maketriangular(deltaeta)) 

659 self.eta_past = (self.eta - trace_part - 

660 self.frac_traceless * traceless_part) 

661 

662 @staticmethod 

663 def _isuppertriangular(m) -> bool: 

664 "Check that a 3x3 matrix is of upper triangular form." 

665 return m[1, 0] == m[2, 0] == m[2, 1] == 0.0 

666 

667 @classmethod 

668 def _islowertriangular(cls, m) -> bool: 

669 "Check that a 3x3 matrix is of lower triangular form." 

670 return cls._isuppertriangular(m.T) 

671 

672 @classmethod 

673 def _istriangular(cls, m) -> bool: 

674 "Check that a 3x3 matrix is of triangular form." 

675 return cls._isuppertriangular(m) or cls._islowertriangular(m) 

676 

677 def _maketriangular(self, sixvector): 

678 "Make 3x3 triangular matrix from a 6-vector." 

679 if self._isuppertriangular(self.h): 

680 return np.array(((sixvector[0], sixvector[5], sixvector[4]), 

681 (0, sixvector[1], sixvector[3]), 

682 (0, 0, sixvector[2]))) 

683 

684 if self._islowertriangular(self.h): 

685 return np.array(((sixvector[0], 0, 0), 

686 (sixvector[5], sixvector[1], 0), 

687 (sixvector[4], sixvector[3], sixvector[2]))) 

688 

689 def _calculateconstants(self): 

690 """(Re)calculate some constants when pfactor, 

691 ttime or temperature have been changed.""" 

692 

693 n = self._getnatoms() 

694 if self.ttime is None: 

695 self.tfact = 0.0 

696 else: 

697 self.tfact = 2.0 / (3 * n * self.temperature * 

698 self.ttime * self.ttime) 

699 if self.pfactor_given is None: 

700 self.pfact = 0.0 

701 else: 

702 self.pfact = 1.0 / (self.pfactor_given * linalg.det(self._getbox())) 

703 # self.pfact = 1.0/(n * self.temperature * self.ptime * self.ptime) 

704 self.desiredEkin = 1.5 * (n - 1) * self.temperature 

705 

706 def _setbox_and_positions(self, h, q): 

707 """Set the computational box and the positions.""" 

708 self.atoms.set_cell(h) 

709 r = np.dot(q + 0.5, h) 

710 self.atoms.set_positions(r) 

711 

712 # A few helper methods, which have been placed in separate methods 

713 # so they can be replaced in the parallel version. 

714 def _synchronize(self): 

715 """Synchronize eta, h and zeta on all processors. 

716 

717 In a parallel simulation, eta, h and zeta are communicated 

718 from the master to all slaves, to prevent numerical noise from 

719 causing them to diverge. 

720 

721 In a serial simulation, do nothing. 

722 """ 

723 # This is a serial simulation object. Do nothing. 

724 

725 def _getnatoms(self): 

726 """Get the number of atoms. 

727 

728 In a parallel simulation, this is the total number of atoms on all 

729 processors. 

730 """ 

731 return len(self.atoms) 

732 

733 def _make_special_q_arrays(self): 

734 """Make the arrays used to store data about the atoms. 

735 

736 In a parallel simulation, these are migrating arrays. In a 

737 serial simulation they are ordinary Numeric arrays. 

738 """ 

739 natoms = len(self.atoms) 

740 self.q = np.zeros((natoms, 3), float) 

741 self.q_past = np.zeros((natoms, 3), float) 

742 self.q_future = np.zeros((natoms, 3), float) 

743 

744 

745class WeakMethodWrapper: 

746 """A weak reference to a method. 

747 

748 Create an object storing a weak reference to an instance and 

749 the name of the method to call. When called, calls the method. 

750 

751 Just storing a weak reference to a bound method would not work, 

752 as the bound method object would go away immediately. 

753 """ 

754 

755 def __init__(self, obj, method): 

756 self.obj = weakref.proxy(obj) 

757 self.method = method 

758 

759 def __call__(self, *args, **kwargs): 

760 m = getattr(self.obj, self.method) 

761 return m(*args, **kwargs)