Coverage for /builds/ase/ase/ase/md/nose_hoover_chain.py: 99.36%
311 statements
« prev ^ index » next coverage.py v7.5.3, created at 2025-08-02 00:12 +0000
« prev ^ index » next coverage.py v7.5.3, created at 2025-08-02 00:12 +0000
1# fmt: off
3from __future__ import annotations
5import numpy as np
6from scipy.special import exprel
8import ase.units
9from ase import Atoms
10from ase.md.md import MolecularDynamics
12# Coefficients for the fourth-order Suzuki-Yoshida integration scheme
13# Ref: H. Yoshida, Phys. Lett. A 150, 5-7, 262-268 (1990).
14# https://doi.org/10.1016/0375-9601(90)90092-3
15FOURTH_ORDER_COEFFS = [
16 1 / (2 - 2 ** (1 / 3)),
17 -(2 ** (1 / 3)) / (2 - 2 ** (1 / 3)),
18 1 / (2 - 2 ** (1 / 3)),
19]
22class NoseHooverChainNVT(MolecularDynamics):
23 """Isothermal molecular dynamics with Nose-Hoover chain.
25 This implementation is based on the Nose-Hoover chain equations and
26 the Liouville-operator derived integrator for non-Hamiltonian systems [1-3].
28 - [1] G. J. Martyna, M. L. Klein, and M. E. Tuckerman, J. Chem. Phys. 97,
29 2635 (1992). https://doi.org/10.1063/1.463940
30 - [2] M. E. Tuckerman, J. Alejandre, R. López-Rendón, A. L. Jochim,
31 and G. J. Martyna, J. Phys. A: Math. Gen. 39, 5629 (2006).
32 https://doi.org/10.1088/0305-4470/39/19/S18
33 - [3] M. E. Tuckerman, Statistical Mechanics: Theory and Molecular
34 Simulation, Oxford University Press (2010).
36 While the algorithm and notation for the thermostat are largely adapted
37 from Ref. [4], the core equations are detailed in the implementation
38 note available at
39 https://github.com/lan496/lan496.github.io/blob/main/notes/nose_hoover_chain/main.pdf.
41 - [4] M. E. Tuckerman, Statistical Mechanics: Theory and Molecular
42 Simulation, 2nd ed. (Oxford University Press, 2009).
43 """
45 def __init__(
46 self,
47 atoms: Atoms,
48 timestep: float,
49 temperature_K: float,
50 tdamp: float,
51 tchain: int = 3,
52 tloop: int = 1,
53 **kwargs,
54 ):
55 """
56 Parameters
57 ----------
58 atoms: ase.Atoms
59 The atoms object.
60 timestep: float
61 The time step in ASE time units.
62 temperature_K: float
63 The target temperature in K.
64 tdamp: float
65 The characteristic time scale for the thermostat in ASE time units.
66 Typically, it is set to 100 times of `timestep`.
67 tchain: int
68 The number of thermostat variables in the Nose-Hoover chain.
69 tloop: int
70 The number of sub-steps in thermostat integration.
71 **kwargs : dict, optional
72 Additional arguments passed to :class:~ase.md.md.MolecularDynamics
73 base class.
74 """
75 super().__init__(
76 atoms=atoms,
77 timestep=timestep,
78 **kwargs,
79 )
80 assert self.masses.shape == (len(self.atoms), 1)
82 num_atoms = self.atoms.get_global_number_of_atoms()
83 self._thermostat = NoseHooverChainThermostat(
84 num_atoms_global=num_atoms,
85 masses=self.masses,
86 temperature_K=temperature_K,
87 tdamp=tdamp,
88 tchain=tchain,
89 tloop=tloop,
90 )
92 # The following variables are updated during self.step()
93 self._q = self.atoms.get_positions()
94 self._p = self.atoms.get_momenta()
96 def step(self) -> None:
97 dt2 = self.dt / 2
98 self._p = self._thermostat.integrate_nhc(self._p, dt2)
99 self._integrate_p(dt2)
100 self._integrate_q(self.dt)
101 self._integrate_p(dt2)
102 self._p = self._thermostat.integrate_nhc(self._p, dt2)
104 self._update_atoms()
106 def get_conserved_energy(self) -> float:
107 """Return the conserved energy-like quantity.
109 This method is mainly used for testing.
110 """
111 conserved_energy = (
112 self.atoms.get_potential_energy(force_consistent=True)
113 + self.atoms.get_kinetic_energy()
114 + self._thermostat.get_thermostat_energy()
115 )
116 return float(conserved_energy)
118 def _update_atoms(self) -> None:
119 self.atoms.set_positions(self._q)
120 self.atoms.set_momenta(self._p)
122 def _get_forces(self) -> np.ndarray:
123 self._update_atoms()
124 return self.atoms.get_forces(md=True)
126 def _integrate_q(self, delta: float) -> None:
127 """Integrate exp(i * L_1 * delta)"""
128 self._q += delta * self._p / self.masses
130 def _integrate_p(self, delta: float) -> None:
131 """Integrate exp(i * L_2 * delta)"""
132 forces = self._get_forces()
133 self._p += delta * forces
136class NoseHooverChainThermostat:
137 """Nose-Hoover chain style thermostats.
139 See `NoseHooverChainNVT` for the references.
140 """
141 def __init__(
142 self,
143 num_atoms_global: int,
144 masses: np.ndarray,
145 temperature_K: float,
146 tdamp: float,
147 tchain: int = 3,
148 tloop: int = 1,
149 ):
150 """See `NoseHooverChainNVT` for the parameters."""
151 self._num_atoms_global = num_atoms_global
152 self._masses = masses # (len(atoms), 1)
153 self._tdamp = tdamp
154 self._tchain = tchain
155 self._tloop = tloop
157 self._kT = ase.units.kB * temperature_K
159 assert tchain >= 1
160 self._Q = np.zeros(tchain)
161 self._Q[0] = 3 * self._num_atoms_global * self._kT * tdamp**2
162 self._Q[1:] = self._kT * tdamp**2
164 # The following variables are updated during self.step()
165 self._eta = np.zeros(self._tchain)
166 self._p_eta = np.zeros(self._tchain)
168 def get_thermostat_energy(self) -> float:
169 """Return energy-like contribution from the thermostat variables."""
170 energy = (
171 3 * self._num_atoms_global * self._kT * self._eta[0]
172 + self._kT * np.sum(self._eta[1:])
173 + np.sum(0.5 * self._p_eta**2 / self._Q)
174 )
175 return float(energy)
177 def integrate_nhc(self, p: np.ndarray, delta: float) -> np.ndarray:
178 """Integrate exp(i * L_NHC * delta) and update momenta `p`."""
179 for _ in range(self._tloop):
180 for coeff in FOURTH_ORDER_COEFFS:
181 p = self._integrate_nhc_loop(
182 p, coeff * delta / len(FOURTH_ORDER_COEFFS)
183 )
185 return p
187 def _integrate_p_eta_j(self, p: np.ndarray, j: int,
188 delta2: float, delta4: float) -> None:
189 if j < self._tchain - 1:
190 self._p_eta[j] *= np.exp(
191 -delta4 * self._p_eta[j + 1] / self._Q[j + 1]
192 )
194 if j == 0:
195 g_j = np.sum(p**2 / self._masses) \
196 - 3 * self._num_atoms_global * self._kT
197 else:
198 g_j = self._p_eta[j - 1] ** 2 / self._Q[j - 1] - self._kT
199 self._p_eta[j] += delta2 * g_j
201 if j < self._tchain - 1:
202 self._p_eta[j] *= np.exp(
203 -delta4 * self._p_eta[j + 1] / self._Q[j + 1]
204 )
206 def _integrate_eta(self, delta: float) -> None:
207 self._eta += delta * self._p_eta / self._Q
209 def _integrate_nhc_p(self, p: np.ndarray, delta: float) -> None:
210 p *= np.exp(-delta * self._p_eta[0] / self._Q[0])
212 def _integrate_nhc_loop(self, p: np.ndarray, delta: float) -> np.ndarray:
213 delta2 = delta / 2
214 delta4 = delta / 4
216 for j in range(self._tchain):
217 self._integrate_p_eta_j(p, self._tchain - j - 1, delta2, delta4)
218 self._integrate_eta(delta)
219 self._integrate_nhc_p(p, delta)
220 for j in range(self._tchain):
221 self._integrate_p_eta_j(p, j, delta2, delta4)
223 return p
226class IsotropicMTKNPT(MolecularDynamics):
227 """Isothermal-isobaric molecular dynamics with isotropic volume fluctuations
228 by Martyna-Tobias-Klein (MTK) method [1].
230 See also `NoseHooverChainNVT` for the references.
232 - [1] G. J. Martyna, D. J. Tobias, and M. L. Klein, J. Chem. Phys. 101,
233 4177-4189 (1994). https://doi.org/10.1063/1.467468
234 """
235 def __init__(
236 self,
237 atoms: Atoms,
238 timestep: float,
239 temperature_K: float,
240 pressure_au: float,
241 tdamp: float,
242 pdamp: float,
243 tchain: int = 3,
244 pchain: int = 3,
245 tloop: int = 1,
246 ploop: int = 1,
247 **kwargs,
248 ):
249 """
250 Parameters
251 ----------
252 atoms: ase.Atoms
253 The atoms object.
254 timestep: float
255 The time step in ASE time units.
256 temperature_K: float
257 The target temperature in K.
258 pressure_au: float
259 The external pressure in eV/Ang^3.
260 tdamp: float
261 The characteristic time scale for the thermostat in ASE time units.
262 Typically, it is set to 100 times of `timestep`.
263 pdamp: float
264 The characteristic time scale for the barostat in ASE time units.
265 Typically, it is set to 1000 times of `timestep`.
266 tchain: int
267 The number of thermostat variables in the Nose-Hoover thermostat.
268 pchain: int
269 The number of barostat variables in the MTK barostat.
270 tloop: int
271 The number of sub-steps in thermostat integration.
272 ploop: int
273 The number of sub-steps in barostat integration.
274 **kwargs : dict, optional
275 Additional arguments passed to :class:~ase.md.md.MolecularDynamics
276 base class.
277 """
278 super().__init__(
279 atoms=atoms,
280 timestep=timestep,
281 **kwargs,
282 )
283 assert self.masses.shape == (len(self.atoms), 1)
285 if len(atoms.constraints) > 0:
286 raise NotImplementedError(
287 "Current implementation does not support constraints"
288 )
290 self._num_atoms_global = self.atoms.get_global_number_of_atoms()
291 self._thermostat = NoseHooverChainThermostat(
292 num_atoms_global=self._num_atoms_global,
293 masses=self.masses,
294 temperature_K=temperature_K,
295 tdamp=tdamp,
296 tchain=tchain,
297 tloop=tloop,
298 )
299 self._barostat = IsotropicMTKBarostat(
300 num_atoms_global=self._num_atoms_global,
301 temperature_K=temperature_K,
302 pdamp=pdamp,
303 pchain=pchain,
304 ploop=ploop,
305 )
307 self._temperature_K = temperature_K
308 self._pressure_au = pressure_au
310 self._kT = ase.units.kB * self._temperature_K
311 self._volume0 = self.atoms.get_volume()
312 self._cell0 = np.array(self.atoms.get_cell())
314 # The following variables are updated during self.step()
315 self._q = self.atoms.get_positions() # positions
316 self._p = self.atoms.get_momenta() # momenta
317 self._eps = 0.0 # volume
318 self._p_eps = 0.0 # volume momenta
320 def step(self) -> None:
321 dt2 = self.dt / 2
323 self._p_eps = self._barostat.integrate_nhc_baro(self._p_eps, dt2)
324 self._p = self._thermostat.integrate_nhc(self._p, dt2)
325 self._integrate_p_cell(dt2)
326 self._integrate_p(dt2)
327 self._integrate_q(self.dt)
328 self._integrate_q_cell(self.dt)
329 self._integrate_p(dt2)
330 self._integrate_p_cell(dt2)
331 self._p = self._thermostat.integrate_nhc(self._p, dt2)
332 self._p_eps = self._barostat.integrate_nhc_baro(self._p_eps, dt2)
334 self._update_atoms()
336 def get_conserved_energy(self) -> float:
337 """Return the conserved energy-like quantity.
339 This method is mainly used for testing.
340 """
341 conserved_energy = (
342 self.atoms.get_potential_energy(force_consistent=True)
343 + self.atoms.get_kinetic_energy()
344 + self._thermostat.get_thermostat_energy()
345 + self._barostat.get_barostat_energy()
346 + self._p_eps * self._p_eps / (2 * self._barostat.W)
347 + self._pressure_au * self._get_volume()
348 )
349 return float(conserved_energy)
351 def _update_atoms(self) -> None:
352 self.atoms.set_positions(self._q)
353 self.atoms.set_momenta(self._p)
354 cell = self._cell0 * np.exp(self._eps)
355 # Never set scale_atoms=True
356 self.atoms.set_cell(cell, scale_atoms=False)
358 def _get_volume(self) -> float:
359 return self._volume0 * np.exp(3 * self._eps)
361 def _get_forces(self) -> np.ndarray:
362 self._update_atoms()
363 return self.atoms.get_forces(md=True)
365 def _get_pressure(self) -> np.ndarray:
366 self._update_atoms()
367 stress = self.atoms.get_stress(voigt=False, include_ideal_gas=True)
368 pressure = -np.trace(stress) / 3
369 return pressure
371 def _integrate_q(self, delta: float) -> None:
372 """Integrate exp(i * L_1 * delta)"""
373 x = delta * self._p_eps / self._barostat.W
374 self._q = (
375 self._q * np.exp(x)
376 + self._p * delta / self.masses * exprel(x)
377 )
379 def _integrate_p(self, delta: float) -> None:
380 """Integrate exp(i * L_2 * delta)"""
381 x = (1 + 1 / self._num_atoms_global) * self._p_eps * delta \
382 / self._barostat.W
383 forces = self._get_forces()
384 self._p = self._p * np.exp(-x) + delta * forces * exprel(-x)
386 def _integrate_q_cell(self, delta: float) -> None:
387 """Integrate exp(i * L_(epsilon, 1) * delta)"""
388 self._eps += delta * self._p_eps / self._barostat.W
390 def _integrate_p_cell(self, delta: float) -> None:
391 """Integrate exp(i * L_(epsilon, 2) * delta)"""
392 pressure = self._get_pressure()
393 volume = self._get_volume()
394 G = (
395 3 * volume * (pressure - self._pressure_au)
396 + np.sum(self._p**2 / self.masses) / self._num_atoms_global
397 )
398 self._p_eps += delta * G
401class IsotropicMTKBarostat:
402 """MTK barostat for isotropic volume fluctuations.
404 See `IsotropicMTKNPT` for the references.
405 """
406 def __init__(
407 self,
408 num_atoms_global: int,
409 temperature_K: float,
410 pdamp: float,
411 pchain: int = 3,
412 ploop: int = 1,
413 ):
414 self._num_atoms_global = num_atoms_global
415 self._pdamp = pdamp
416 self._pchain = pchain
417 self._ploop = ploop
419 self._kT = ase.units.kB * temperature_K
421 self._W = (3 * self._num_atoms_global + 3) * self._kT * self._pdamp**2
423 assert pchain >= 1
424 self._R = np.zeros(self._pchain)
425 self._R[0] = 9 * self._kT * self._pdamp**2
426 self._R[1:] = self._kT * self._pdamp**2
428 self._xi = np.zeros(self._pchain) # barostat coordinates
429 self._p_xi = np.zeros(self._pchain)
431 @property
432 def W(self) -> float:
433 """Virtual mass for barostat momenta `p_xi`."""
434 return self._W
436 def get_barostat_energy(self) -> float:
437 """Return energy-like contribution from the barostat variables."""
438 energy = (
439 + np.sum(0.5 * self._p_xi**2 / self._R)
440 + self._kT * np.sum(self._xi)
441 )
442 return float(energy)
444 def integrate_nhc_baro(self, p_eps: float, delta: float) -> float:
445 """Integrate exp(i * L_NHC-baro * delta)"""
446 for _ in range(self._ploop):
447 for coeff in FOURTH_ORDER_COEFFS:
448 p_eps = self._integrate_nhc_baro_loop(
449 p_eps, coeff * delta / len(FOURTH_ORDER_COEFFS)
450 )
451 return p_eps
453 def _integrate_nhc_baro_loop(self, p_eps: float, delta: float) -> float:
454 delta2 = delta / 2
455 delta4 = delta / 4
457 for j in range(self._pchain):
458 self._integrate_p_xi_j(p_eps, self._pchain - j - 1, delta2, delta4)
459 self._integrate_xi(delta)
460 p_eps = self._integrate_nhc_p_eps(p_eps, delta)
461 for j in range(self._pchain):
462 self._integrate_p_xi_j(p_eps, j, delta2, delta4)
464 return p_eps
466 def _integrate_p_xi_j(self, p_eps: float, j: int,
467 delta2: float, delta4: float) -> None:
468 if j < self._pchain - 1:
469 self._p_xi[j] *= np.exp(
470 -delta4 * self._p_xi[j + 1] / self._R[j + 1]
471 )
473 if j == 0:
474 g_j = p_eps ** 2 / self._W - self._kT
475 else:
476 g_j = self._p_xi[j - 1] ** 2 / self._R[j - 1] - self._kT
477 self._p_xi[j] += delta2 * g_j
479 if j < self._pchain - 1:
480 self._p_xi[j] *= np.exp(
481 -delta4 * self._p_xi[j + 1] / self._R[j + 1]
482 )
484 def _integrate_xi(self, delta: float) -> None:
485 self._xi += delta * self._p_xi / self._R
487 def _integrate_nhc_p_eps(self, p_eps: float, delta: float) -> float:
488 p_eps_new = p_eps * float(
489 np.exp(-delta * self._p_xi[0] / self._R[0])
490 )
491 return p_eps_new
494class MTKNPT(MolecularDynamics):
495 """Isothermal-isobaric molecular dynamics with volume-and-cell fluctuations
496 by Martyna-Tobias-Klein (MTK) method [1].
498 See also `NoseHooverChainNVT` for the references.
500 - [1] G. J. Martyna, D. J. Tobias, and M. L. Klein, J. Chem. Phys. 101,
501 4177-4189 (1994). https://doi.org/10.1063/1.467468
502 """
503 def __init__(
504 self,
505 atoms: Atoms,
506 timestep: float,
507 temperature_K: float,
508 pressure_au: float,
509 tdamp: float,
510 pdamp: float,
511 tchain: int = 3,
512 pchain: int = 3,
513 tloop: int = 1,
514 ploop: int = 1,
515 **kwargs,
516 ):
517 """
518 Parameters
519 ----------
520 atoms: ase.Atoms
521 The atoms object.
522 timestep: float
523 The time step in ASE time units.
524 temperature_K: float
525 The target temperature in K.
526 pressure_au: float
527 The external pressure in eV/Ang^3.
528 tdamp: float
529 The characteristic time scale for the thermostat in ASE time units.
530 Typically, it is set to 100 times of `timestep`.
531 pdamp: float
532 The characteristic time scale for the barostat in ASE time units.
533 Typically, it is set to 1000 times of `timestep`.
534 tchain: int
535 The number of thermostat variables in the Nose-Hoover thermostat.
536 pchain: int
537 The number of barostat variables in the MTK barostat.
538 tloop: int
539 The number of sub-steps in thermostat integration.
540 ploop: int
541 The number of sub-steps in barostat integration.
542 **kwargs : dict, optional
543 Additional arguments passed to :class:~ase.md.md.MolecularDynamics
544 base class.
545 """
546 super().__init__(
547 atoms=atoms,
548 timestep=timestep,
549 **kwargs,
550 )
551 assert self.masses.shape == (len(self.atoms), 1)
553 if len(atoms.constraints) > 0:
554 raise NotImplementedError(
555 "Current implementation does not support constraints"
556 )
558 self._num_atoms_global = self.atoms.get_global_number_of_atoms()
559 self._thermostat = NoseHooverChainThermostat(
560 num_atoms_global=self._num_atoms_global,
561 masses=self.masses,
562 temperature_K=temperature_K,
563 tdamp=tdamp,
564 tchain=tchain,
565 tloop=tloop,
566 )
567 self._barostat = MTKBarostat(
568 num_atoms_global=self._num_atoms_global,
569 temperature_K=temperature_K,
570 pdamp=pdamp,
571 pchain=pchain,
572 ploop=ploop,
573 )
575 self._temperature_K = temperature_K
576 self._pressure_au = pressure_au
578 self._kT = ase.units.kB * self._temperature_K
580 # The following variables are updated during self.step()
581 self._q = self.atoms.get_positions() # positions
582 self._p = self.atoms.get_momenta() # momenta
583 self._h = np.array(self.atoms.get_cell()) # cell
584 self._p_g = np.zeros((3, 3)) # cell momenta
586 def step(self) -> None:
587 dt2 = self.dt / 2
589 self._p_g = self._barostat.integrate_nhc_baro(self._p_g, dt2)
590 self._p = self._thermostat.integrate_nhc(self._p, dt2)
591 self._integrate_p_cell(dt2)
592 self._integrate_p(dt2)
593 self._integrate_q(self.dt)
594 self._integrate_q_cell(self.dt)
595 self._integrate_p(dt2)
596 self._integrate_p_cell(dt2)
597 self._p = self._thermostat.integrate_nhc(self._p, dt2)
598 self._p_g = self._barostat.integrate_nhc_baro(self._p_g, dt2)
600 self._update_atoms()
602 def get_conserved_energy(self) -> float:
603 conserved_energy = (
604 self.atoms.get_total_energy()
605 + self._thermostat.get_thermostat_energy()
606 + self._barostat.get_barostat_energy()
607 + np.trace(self._p_g.T @ self._p_g) / (2 * self._barostat.W)
608 + self._pressure_au * self._get_volume()
609 )
610 return float(conserved_energy)
612 def _update_atoms(self) -> None:
613 self.atoms.set_positions(self._q)
614 self.atoms.set_momenta(self._p)
615 self.atoms.set_cell(self._h, scale_atoms=False)
617 def _get_volume(self) -> float:
618 return np.abs(np.linalg.det(self._h))
620 def _get_forces(self) -> np.ndarray:
621 self._update_atoms()
622 return self.atoms.get_forces(md=True)
624 def _get_stress(self) -> np.ndarray:
625 self._update_atoms()
626 stress = self.atoms.get_stress(voigt=False, include_ideal_gas=True)
627 return -stress
629 def _integrate_q(self, delta: float) -> None:
630 """Integrate exp(i * L_1 * delta)"""
631 # eigvals: (3-eigvec), U: (3-xyz, 3-eigvec)
632 eigvals, U = np.linalg.eigh(self._p_g)
633 x = self._q @ U # (num_atoms, 3-eigvec)
634 y = self._p @ U # (num_atoms, 3-eigvec)
635 sol = (
636 x * np.exp(eigvals * delta / self._barostat.W)[None, :]
637 + delta * y / self.masses * exprel(
638 eigvals * delta / self._barostat.W
639 )[None, :]
640 ) # (num_atoms, 3-eigvec)
641 self._q = sol @ U.T
643 def _integrate_p(self, delta: float) -> None:
644 """Integrate exp(i * L_2 * delta)"""
645 forces = self._get_forces() # (num_atoms, 3-xyz)
647 # eigvals: (3-eigvec), U: (3-xyz, 3-eigvec)
648 eigvals, U = np.linalg.eigh(self._p_g)
649 kappas = eigvals \
650 + np.trace(self._p_g) / (3 * self._num_atoms_global) # (3-eigvec)
651 y = self._p @ U # (num_atoms, 3-eigvec)
652 sol = (
653 y * np.exp(-kappas * delta / self._barostat.W)[None, :]
654 + delta * (forces @ U) * exprel(
655 -kappas * delta / self._barostat.W
656 )[None, :]
657 ) # (num_atoms, 3-eigvec)
658 self._p = sol @ U.T
660 def _integrate_q_cell(self, delta: float) -> None:
661 """Integrate exp(i * L_(g, 1) * delta)"""
662 # U @ np.diag(eigvals) @ U.T = self._p_g
663 # eigvals: (3-eigvec), U: (3-xyz, 3-eigvec)
664 eigvals, U = np.linalg.eigh(self._p_g)
665 n = self._h @ U # (3-axis, 3-eigvec)
666 sol = n * np.exp(
667 eigvals * delta / self._barostat.W
668 )[None, :] # (3-axis, 3-eigvec)
669 self._h = sol @ U.T
671 def _integrate_p_cell(self, delta: float) -> None:
672 """Integrate exp(i * L_(g, 2) * delta)"""
673 stress = self._get_stress()
674 G = (
675 self._get_volume() * (stress - self._pressure_au * np.eye(3))
676 + np.sum(self._p**2 / self.masses) / (3 * self._num_atoms_global)
677 * np.eye(3)
678 )
679 self._p_g += delta * G
682class MTKBarostat:
683 """MTK barostat for volume-and-cell fluctuations.
685 See `MTKNPT` for the references.
686 """
687 def __init__(
688 self,
689 num_atoms_global: int,
690 temperature_K: float,
691 pdamp: float,
692 pchain: int = 3,
693 ploop: int = 1,
694 ):
695 self._num_atoms_global = num_atoms_global
696 self._pdamp = pdamp
697 self._pchain = pchain
698 self._ploop = ploop
700 self._kT = ase.units.kB * temperature_K
702 self._W = (self._num_atoms_global + 1) * self._kT * self._pdamp**2
704 assert pchain >= 1
705 self._R = np.zeros(self._pchain)
706 cell_dof = 9 # TODO:
707 self._R[0] = cell_dof * self._kT * self._pdamp**2
708 self._R[1:] = self._kT * self._pdamp**2
710 self._xi = np.zeros(self._pchain) # barostat coordinates
711 self._p_xi = np.zeros(self._pchain)
713 @property
714 def W(self) -> float:
715 return self._W
717 def get_barostat_energy(self) -> float:
718 energy = (
719 np.sum(self._p_xi**2 / self._R) / 2
720 + 9 * self._kT * self._xi[0]
721 + self._kT * np.sum(self._xi[1:])
722 )
723 return float(energy)
725 def integrate_nhc_baro(self, p_g: np.ndarray, delta: float) -> np.ndarray:
726 """Integrate exp(i * L_NHC-baro * delta)"""
727 for _ in range(self._ploop):
728 for coeff in FOURTH_ORDER_COEFFS:
729 p_g = self._integrate_nhc_baro_loop(
730 p_g, coeff * delta / len(FOURTH_ORDER_COEFFS)
731 )
732 return p_g
734 def _integrate_nhc_baro_loop(
735 self, p_g: np.ndarray, delta: float
736 ) -> np.ndarray:
737 delta2 = delta / 2
738 delta4 = delta / 4
740 for j in range(self._pchain):
741 self._integrate_p_xi_j(p_g, self._pchain - j - 1, delta2, delta4)
742 self._integrate_xi(delta)
743 self._integrate_nhc_p_eps(p_g, delta)
744 for j in range(self._pchain):
745 self._integrate_p_xi_j(p_g, j, delta2, delta4)
747 return p_g
749 def _integrate_p_xi_j(
750 self, p_g: np.ndarray, j: int, delta2: float, delta4: float
751 ) -> None:
752 if j < self._pchain - 1:
753 self._p_xi[j] *= np.exp(
754 -delta4 * self._p_xi[j + 1] / self._R[j + 1]
755 )
757 if j == 0:
758 # TODO: do we need to substitute 9 with cell_dof?
759 g_j = np.trace(p_g.T @ p_g) / self._W - 9 * self._kT
760 else:
761 g_j = self._p_xi[j - 1] ** 2 / self._R[j - 1] - self._kT
762 self._p_xi[j] += delta2 * g_j
764 if j < self._pchain - 1:
765 self._p_xi[j] *= np.exp(
766 -delta4 * self._p_xi[j + 1] / self._R[j + 1]
767 )
769 def _integrate_xi(self, delta: float) -> None:
770 for j in range(self._pchain):
771 self._xi[j] += delta * self._p_xi[j] / self._R[j]
773 def _integrate_nhc_p_eps(self, p_g: np.ndarray, delta: float) -> None:
774 p_g *= np.exp(-delta * self._p_xi[0] / self._R[0])