Coverage for /builds/debichem-team/python-ase/ase/md/switch_langevin.py: 91.67%
48 statements
« prev ^ index » next coverage.py v7.5.3, created at 2025-03-06 04:00 +0000
« prev ^ index » next coverage.py v7.5.3, created at 2025-03-06 04:00 +0000
1from typing import Any, List, Optional
3import numpy as np
4from scipy.integrate import trapezoid
6from ase import Atoms
7from ase.calculators.mixing import MixedCalculator
8from ase.md.langevin import Langevin
11class SwitchLangevin(Langevin):
12 """
13 MD class for carrying out thermodynamic integration between two
14 hamiltonians
16 Read more at https://en.wikipedia.org/wiki/Thermodynamic_integration
18 The integration path is lambda 0 ---> 1 , with the switching hamiltonian
19 H(lambda) = (1-lambda) * H1 + lambda * H2
21 Attributes
22 ----------
23 path_data : numpy array
24 col 1 (md-step), col 2 (lambda), col 3 (energy H1), col 4 (energy H2)
26 Parameters
27 ----------
28 atoms : ASE Atoms object
29 Atoms object for which MD will be run
30 calc1 : ASE calculator object
31 Calculator corresponding to first Hamiltonian
32 calc2 : ASE calculator object
33 Calculator corresponding to second Hamiltonian
34 dt : float
35 Timestep for MD simulation
36 T : float
37 Temperature in eV (deprecated)
38 friction : float
39 Friction for langevin dynamics
40 n_eq : int
41 Number of equilibration steps
42 n_switch : int
43 Number of switching steps
44 """
46 def __init__(
47 self,
48 atoms: Atoms,
49 calc1,
50 calc2,
51 dt: float,
52 T: Optional[float] = None,
53 friction: Optional[float] = None,
54 n_eq: Optional[int] = None,
55 n_switch: Optional[int] = None,
56 temperature_K: Optional[float] = None,
57 **langevin_kwargs,
58 ):
59 super().__init__(atoms, dt, temperature=T, temperature_K=temperature_K,
60 friction=friction, **langevin_kwargs)
61 if friction is None:
62 raise TypeError("Missing 'friction' argument.")
63 if n_eq is None:
64 raise TypeError("Missing 'n_eq' argument.")
65 if n_switch is None:
66 raise TypeError("Missing 'n_switch' argument.")
67 self.n_eq = n_eq
68 self.n_switch = n_switch
69 self.lam = 0.0
70 calc = MixedCalculator(calc1, calc2, weight1=1.0, weight2=0.0)
71 self.atoms.calc = calc
73 self.path_data: List[Any] = []
75 def run(self):
76 """ Run the MD switching simulation """
77 forces = self.atoms.get_forces(md=True)
79 # run equilibration with calc1
80 for _ in range(self.n_eq):
81 forces = self.step(forces)
82 self.nsteps += 1
83 self.call_observers()
85 # run switch from calc1 to calc2
86 self.path_data.append(
87 [0, self.lam,
88 *self.atoms.calc.get_energy_contributions(self.atoms)])
89 for step in range(1, self.n_switch):
90 # update calculator
91 self.lam = get_lambda(step, self.n_switch)
92 self.atoms.calc.set_weights(1 - self.lam, self.lam)
94 # carry out md step
95 forces = self.step(forces)
96 self.nsteps += 1
98 # collect data
99 self.call_observers()
100 self.path_data.append(
101 [step, self.lam,
102 *self.atoms.calc.get_energy_contributions(self.atoms)])
104 self.path_data = np.array(self.path_data)
106 def get_free_energy_difference(self):
107 """ Return the free energy difference between calc2 and calc1, by
108 integrating dH/dlam along the switching path
110 Returns
111 -------
112 float
113 Free energy difference, F2 - F1
114 """
115 if len(self.path_data) == 0:
116 raise ValueError('No free energy data found.')
118 lambdas = self.path_data[:, 1]
119 U1 = self.path_data[:, 2]
120 U2 = self.path_data[:, 3]
121 delta_F = trapezoid(U2 - U1, lambdas)
122 return delta_F
125def get_lambda(step, n_switch):
126 """ Return lambda value along the switching path """
127 assert step >= 0 and step <= n_switch
128 t = step / (n_switch - 1)
129 return t**5 * (70 * t**4 - 315 * t**3 + 540 * t**2 - 420 * t + 126)