Coverage for /builds/ase/ase/ase/md/npt.py: 67.49%

283 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2025-08-02 00:12 +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, Optional, Tuple, Union 

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 NPT(MolecularDynamics): 

42 

43 classname = "NPT" # 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: Optional[float] = None, 

51 externalstress: Optional[float] = None, 

52 ttime: Optional[float] = None, 

53 pfactor: Optional[float] = None, 

54 *, 

55 temperature_K: Optional[float] = None, 

56 mask: Optional[Union[Tuple[int], np.ndarray]] = None, 

57 trajectory: Optional[str] = None, 

58 logfile: Optional[Union[IO, str]] = 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 The dynamics object is called with the following 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 Useful parameter values: 

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 

137 References: 

138 

139 1) S. Melchionna, G. Ciccotti and B. L. Holian, Molecular 

140 Physics 78, p. 533 (1993). 

141 

142 2) S. Melchionna, Physical 

143 Review E 61, p. 6165 (2000). 

144 

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

146 Physical Review A 41, p. 4552 (1990). 

147 

148 4) F. D. Di Tolla and M. Ronchetti, Physical 

149 Review E 48, p. 1726 (1993). 

150 

151 ''' 

152 

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

154 logfile, loginterval, 

155 append_trajectory=append_trajectory) 

156 # self.atoms = atoms 

157 # self.timestep = timestep 

158 if externalstress is None and pfactor is not None: 

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

160 self.zero_center_of_mass_momentum(verbose=1) 

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

162 temperature, temperature_K, 'eV') 

163 if externalstress is not None: 

164 self.set_stress(externalstress) 

165 self.set_mask(mask) 

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

167 self.zeta = 0.0 

168 self.zeta_integrated = 0.0 

169 self.initialized = 0 

170 self.ttime = ttime 

171 self.pfactor_given = pfactor 

172 self._calculateconstants() 

173 self.timeelapsed = 0.0 

174 self.frac_traceless = 1 

175 

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

177 """Set the temperature. 

178 

179 Parameters: 

180 

181 temperature: float (deprecated) 

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

183 

184 temperature_K: float (keyword-only argument) 

185 The new temperature, in K. 

186 """ 

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

188 temperature, temperature_K, 'eV') 

189 self._calculateconstants() 

190 

191 def set_stress(self, stress): 

192 """Set the applied stress. 

193 

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

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

196 

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

198 the object. 

199 """ 

200 

201 if np.isscalar(stress): 

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

203 else: 

204 stress = np.array(stress) 

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

206 if not self._issymmetric(stress): 

207 raise ValueError( 

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

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

210 stress[2, 2], stress[1, 2], 

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

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

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

214 self.externalstress = stress 

215 

216 def set_mask(self, mask): 

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

218 

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

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

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

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

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

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

225 which strain values may change. 

226 

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

228 """ 

229 if mask is None: 

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

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

232 mask = np.array(mask) 

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

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

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

236 else: 

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

238 

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

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

241 else: 

242 self.mask = mask 

243 

244 def set_fraction_traceless(self, fracTraceless): 

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

246 on eta is kept. 

247 

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

249 """ 

250 self.frac_traceless = fracTraceless 

251 

252 def get_strain_rate(self): 

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

254 

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

256 

257 """ 

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

259 

260 def set_strain_rate(self, rate): 

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

262 

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

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

265 maintained constantly. 

266 """ 

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

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

269 self.eta = rate 

270 if self.initialized: 

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

272 self._initialize_eta_h() 

273 

274 def get_time(self): 

275 "Get the elapsed time." 

276 return self.timeelapsed 

277 

278 def irun(self, steps): 

279 """Run dynamics algorithm as generator. 

280 

281 Parameters 

282 ---------- 

283 steps : int 

284 Number of dynamics steps to be run. 

285 

286 Yields 

287 ------ 

288 complete : bool 

289 True if the maximum number of steps are reached. 

290 """ 

291 

292 if not self.initialized: 

293 self.initialize() 

294 else: 

295 if self.have_the_atoms_been_changed(): 

296 raise NotImplementedError( 

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

298 

299 yield from super().irun(steps) 

300 

301 def run(self, steps): 

302 """Perform a number of time steps. 

303 

304 Parameters 

305 ---------- 

306 steps : int 

307 Number of dynamics steps to be run. 

308 

309 Yields 

310 ------ 

311 complete : bool 

312 True if the maximum number of steps are reached. 

313 """ 

314 

315 for complete in self.irun(steps): 

316 pass 

317 return complete 

318 

319 def have_the_atoms_been_changed(self): 

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

321 limit = 1e-10 

322 h = self._getbox() 

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

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

325 return 1 

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

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

328 if err > limit: 

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

330 str(err)) 

331 return 1 

332 return 0 

333 

334 def step(self): 

335 """Perform a single time step. 

336 

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

338 the positions and momenta have not been changed since last 

339 timestep. 

340 """ 

341 

342 # Assumes the following variables are OK 

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

344 # 

345 # q corresponds to the current positions 

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

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

348 # 

349 # print "Making a timestep" 

350 dt = self.dt 

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

352 if self.pfactor_given is None: 

353 deltaeta = np.zeros(6, float) 

354 else: 

355 stress = self.stresscalculator() 

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

357 (stress - self.externalstress)) 

358 

359 if self.frac_traceless == 1: 

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

361 self._maketriangular(deltaeta) 

362 else: 

363 trace_part, traceless_part = self._separatetrace( 

364 self._maketriangular(deltaeta)) 

365 eta_future = (self.eta_past + trace_part + 

366 self.frac_traceless * traceless_part) 

367 

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

369 self.desiredEkin) 

370 zeta_future = self.zeta_past + deltazeta 

371 # Advance time 

372 self.timeelapsed += dt 

373 self.h_past = self.h 

374 self.h = h_future 

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

376 self.q_past = self.q 

377 self.q = self.q_future 

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

379 self.eta_past = self.eta 

380 self.eta = eta_future 

381 self.zeta_past = self.zeta 

382 self.zeta = zeta_future 

383 self._synchronize() # for parallel simulations. 

384 self.zeta_integrated += dt * self.zeta 

385 force = self.forcecalculator() 

386 self._calculate_q_future(force) 

387 self.atoms.set_momenta( 

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

389 self._getmasses()) 

390 # self.stresscalculator() 

391 

392 def forcecalculator(self): 

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

394 

395 def stresscalculator(self): 

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

397 

398 def initialize(self): 

399 """Initialize the dynamics. 

400 

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

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

403 method performs a 'backwards' timestep to generate a 

404 configuration before the current. 

405 

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

407 """ 

408 # print "Initializing the NPT dynamics." 

409 dt = self.dt 

410 atoms = self.atoms 

411 self.h = self._getbox() 

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

413 raise NotImplementedError( 

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

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

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

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

418 # self._make_special_q_arrays() 

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

420 # zeta and eta were set in __init__ 

421 self._initialize_eta_h() 

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

423 self.desiredEkin) 

424 self.zeta_past = self.zeta - deltazeta 

425 self._calculate_q_past_and_future() 

426 self.initialized = 1 

427 

428 def get_gibbs_free_energy(self): 

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

430 

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

432 

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

434 first timestep, Initialize will be called. 

435 """ 

436 if not self.initialized: 

437 self.initialize() 

438 n = self._getnatoms() 

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

440 # self.eta))) 

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

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

443 self.atoms.get_kinetic_energy() 

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

445 if self.ttime is not None: 

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

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

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

449 else: 

450 assert self.zeta == 0.0 

451 if self.pfactor_given is not None: 

452 gibbs += 0.5 / self.pfact * contractedeta 

453 else: 

454 assert contractedeta == 0.0 

455 return gibbs 

456 

457 def get_center_of_mass_momentum(self): 

458 "Get the center of mass momentum." 

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

460 

461 def zero_center_of_mass_momentum(self, verbose=0): 

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

463 cm = self.get_center_of_mass_momentum() 

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

465 if verbose and abscm > 1e-4: 

466 self._warning( 

467 self.classname + 

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

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

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

471 cm / self._getnatoms()) 

472 

473 def attach_atoms(self, atoms): 

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

475 

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

477 dynamics object has been read from a trajectory. 

478 """ 

479 try: 

480 self.atoms 

481 except AttributeError: 

482 pass 

483 else: 

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

485 "which already has atoms.") 

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

487 limit = 1e-6 

488 h = self._getbox() 

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

490 raise RuntimeError( 

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

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

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

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

495 self._calculate_q_past_and_future() 

496 self.initialized = 1 

497 

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

499 """Attach callback function or trajectory. 

500 

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

502 *args* and keyword arguments *kwargs*. 

503 

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

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

506 trajectory supporting set_extra_data(), said method is first 

507 used to instruct the trajectory to also save internal 

508 data from the NPT dynamics object. 

509 """ 

510 if hasattr(function, "set_extra_data"): 

511 # We are attaching a BundleTrajectory or similar 

512 function.set_extra_data("npt_init", 

513 WeakMethodWrapper(self, "get_init_data"), 

514 once=True) 

515 function.set_extra_data("npt_dynamics", 

516 WeakMethodWrapper(self, "get_data")) 

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

518 

519 def get_init_data(self): 

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

521 return {'dt': self.dt, 

522 'temperature': self.temperature, 

523 'desiredEkin': self.desiredEkin, 

524 'externalstress': self.externalstress, 

525 'mask': self.mask, 

526 'ttime': self.ttime, 

527 'tfact': self.tfact, 

528 'pfactor_given': self.pfactor_given, 

529 'pfact': self.pfact, 

530 'frac_traceless': self.frac_traceless} 

531 

532 def get_data(self): 

533 "Return data needed to restore the state." 

534 return {'eta': self.eta, 

535 'eta_past': self.eta_past, 

536 'zeta': self.zeta, 

537 'zeta_past': self.zeta_past, 

538 'zeta_integrated': self.zeta_integrated, 

539 'h': self.h, 

540 'h_past': self.h_past, 

541 'timeelapsed': self.timeelapsed} 

542 

543 @classmethod 

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

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

546 

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

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

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

550 

551 Arguments: 

552 

553 trajectory 

554 The filename or an open BundleTrajectory object. 

555 

556 frame (optional) 

557 Which frame to read. Default: the last. 

558 

559 atoms (optional, internal use only) 

560 Pre-read atoms. Do not use. 

561 """ 

562 if isinstance(trajectory, str): 

563 if trajectory.endswith('/'): 

564 trajectory = trajectory[:-1] 

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

566 from ase.io.bundletrajectory import BundleTrajectory 

567 trajectory = BundleTrajectory(trajectory) 

568 else: 

569 raise ValueError( 

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

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

572 if atoms is None: 

573 atoms = trajectory[frame] 

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

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

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

577 temperature=init_data['temperature'], 

578 externalstress=init_data['externalstress'], 

579 ttime=init_data['ttime'], 

580 pfactor=init_data['pfactor_given'], 

581 mask=init_data['mask']) 

582 dyn.desiredEkin = init_data['desiredEkin'] 

583 dyn.tfact = init_data['tfact'] 

584 dyn.pfact = init_data['pfact'] 

585 dyn.frac_traceless = init_data['frac_traceless'] 

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

587 setattr(dyn, k, v) 

588 return (dyn, atoms) 

589 

590 def _getbox(self): 

591 "Get the computational box." 

592 return self.atoms.get_cell() 

593 

594 def _getmasses(self): 

595 "Get the masses as an Nx1 array." 

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

597 

598 def _separatetrace(self, mat): 

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

600 the other traceless, which sum to the given matrix 

601 """ 

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

603 return tracePart, mat - tracePart 

604 

605 # A number of convenient helper methods 

606 def _warning(self, text): 

607 "Emit a warning." 

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

609 sys.stderr.flush() 

610 

611 def _calculate_q_future(self, force): 

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

613 dt = self.dt 

614 id3 = np.identity(3) 

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

616 self.inv_h) 

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

618 self.inv_h)) 

619 inv_b = linalg.inv(beta + id3) 

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

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

622 inv_b) 

623 

624 def _calculate_q_past_and_future(self): 

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

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

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

628 p0 = self.atoms.get_momenta() 

629 m = self._getmasses() 

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

631 dt = self.dt 

632 for i in range(2): 

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

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

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

636 e = ekin(p) 

637 if e < 1e-5: 

638 # The kinetic energy and momenta are virtually zero 

639 return 

640 p = (p0 - p) + p0 

641 

642 def _initialize_eta_h(self): 

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

644 if self.pfactor_given is None: 

645 deltaeta = np.zeros(6, float) 

646 else: 

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

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

649 if self.frac_traceless == 1: 

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

651 self._maketriangular(deltaeta) 

652 else: 

653 trace_part, traceless_part = self._separatetrace( 

654 self._maketriangular(deltaeta)) 

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

656 self.frac_traceless * traceless_part) 

657 

658 @staticmethod 

659 def _isuppertriangular(m) -> bool: 

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

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

662 

663 @classmethod 

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

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

666 return cls._isuppertriangular(m.T) 

667 

668 @classmethod 

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

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

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

672 

673 def _maketriangular(self, sixvector): 

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

675 if self._isuppertriangular(self.h): 

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

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

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

679 

680 if self._islowertriangular(self.h): 

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

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

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

684 

685 def _calculateconstants(self): 

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

687 ttime or temperature have been changed.""" 

688 

689 n = self._getnatoms() 

690 if self.ttime is None: 

691 self.tfact = 0.0 

692 else: 

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

694 self.ttime * self.ttime) 

695 if self.pfactor_given is None: 

696 self.pfact = 0.0 

697 else: 

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

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

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

701 

702 def _setbox_and_positions(self, h, q): 

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

704 self.atoms.set_cell(h) 

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

706 self.atoms.set_positions(r) 

707 

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

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

710 def _synchronize(self): 

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

712 

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

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

715 causing them to diverge. 

716 

717 In a serial simulation, do nothing. 

718 """ 

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

720 

721 def _getnatoms(self): 

722 """Get the number of atoms. 

723 

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

725 processors. 

726 """ 

727 return len(self.atoms) 

728 

729 def _make_special_q_arrays(self): 

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

731 

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

733 serial simulation they are ordinary Numeric arrays. 

734 """ 

735 natoms = len(self.atoms) 

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

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

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

739 

740 

741class WeakMethodWrapper: 

742 """A weak reference to a method. 

743 

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

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

746 

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

748 as the bound method object would go away immediately. 

749 """ 

750 

751 def __init__(self, obj, method): 

752 self.obj = weakref.proxy(obj) 

753 self.method = method 

754 

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

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

757 return m(*args, **kwargs)