Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1import numpy as np
2from ase.calculators.calculator import Calculator
3from ase.calculators.qmmm import combine_lj_lorenz_berthelot
4from ase import units
5import copy
7k_c = units.Hartree * units.Bohr
10class CombineMM(Calculator):
11 implemented_properties = ['energy', 'forces']
13 def __init__(self, idx, apm1, apm2, calc1, calc2,
14 sig1, eps1, sig2, eps2, rc=7.0, width=1.0):
15 """A calculator that combines two MM calculators
16 (TIPnP, Counterions, ...)
18 parameters:
20 idx: List of indices of atoms belonging to calculator 1
21 apm1,2: atoms pr molecule of each subsystem (NB: apm for TIP4P is 3!)
22 calc1,2: calculator objects for each subsystem
23 sig1,2, eps1,2: LJ parameters for each subsystem. Should be a numpy
24 array of length = apm
25 rc = long range cutoff
26 width = width of cutoff region.
28 Currently the interactions are limited to being:
29 - Nonbonded
30 - Hardcoded to two terms:
31 - Coulomb electrostatics
32 - Lennard-Jones
34 It could of course benefit from being more like the EIQMMM class
35 where the interactions are switchable. But this is in princple
36 just meant for adding counter ions to a qmmm simulation to neutralize
37 the charge of the total systemn
39 Maybe it can combine n MM calculators in the future?
40 """
42 self.idx = idx
43 self.apm1 = apm1 # atoms per mol for LJ calculator
44 self.apm2 = apm2
46 self.rc = rc
47 self.width = width
49 self.atoms1 = None
50 self.atoms2 = None
51 self.mask = None
53 self.calc1 = calc1
54 self.calc2 = calc2
56 self.sig1 = sig1
57 self.eps1 = eps1
58 self.sig2 = sig2
59 self.eps2 = eps2
61 Calculator.__init__(self)
63 def initialize(self, atoms):
64 self.mask = np.zeros(len(atoms), bool)
65 self.mask[self.idx] = True
67 constraints = atoms.constraints
68 atoms.constraints = []
69 self.atoms1 = atoms[self.mask]
70 self.atoms2 = atoms[~self.mask]
72 atoms.constraints = constraints
74 self.atoms1.calc = self.calc1
75 self.atoms2.calc = self.calc2
77 self.cell = atoms.cell
78 self.pbc = atoms.pbc
80 self.sigma, self.epsilon =\
81 combine_lj_lorenz_berthelot(self.sig1, self.sig2,
82 self.eps1, self.eps2)
84 self.make_virtual_mask()
86 def calculate(self, atoms, properties, system_changes):
87 Calculator.calculate(self, atoms, properties, system_changes)
89 if self.atoms1 is None:
90 self.initialize(atoms)
92 pos1 = atoms.positions[self.mask]
93 pos2 = atoms.positions[~self.mask]
94 self.atoms1.set_positions(pos1)
95 self.atoms2.set_positions(pos2)
97 # positions and charges for the coupling term, which should
98 # include virtual charges and sites:
99 spm1 = self.atoms1.calc.sites_per_mol
100 spm2 = self.atoms2.calc.sites_per_mol
101 xpos1 = self.atoms1.calc.add_virtual_sites(pos1)
102 xpos2 = self.atoms2.calc.add_virtual_sites(pos2)
104 xc1 = self.atoms1.calc.get_virtual_charges(self.atoms1)
105 xc2 = self.atoms2.calc.get_virtual_charges(self.atoms2)
107 xpos1 = xpos1.reshape((-1, spm1, 3))
108 xpos2 = xpos2.reshape((-1, spm2, 3))
110 e_c, f_c = self.coulomb(xpos1, xpos2, xc1, xc2, spm1, spm2)
112 e_lj, f1, f2 = self.lennard_jones(self.atoms1, self.atoms2)
114 f_lj = np.zeros((len(atoms), 3))
115 f_lj[self.mask] += f1
116 f_lj[~self.mask] += f2
118 # internal energy, forces of each subsystem:
119 f12 = np.zeros((len(atoms), 3))
120 e1 = self.atoms1.get_potential_energy()
121 fi1 = self.atoms1.get_forces()
123 e2 = self.atoms2.get_potential_energy()
124 fi2 = self.atoms2.get_forces()
126 f12[self.mask] += fi1
127 f12[~self.mask] += fi2
129 self.results['energy'] = e_c + e_lj + e1 + e2
130 self.results['forces'] = f_c + f_lj + f12
132 def get_virtual_charges(self, atoms):
133 if self.atoms1 is None:
134 self.initialize(atoms)
136 vc1 = self.atoms1.calc.get_virtual_charges(atoms[self.mask])
137 vc2 = self.atoms2.calc.get_virtual_charges(atoms[~self.mask])
138 # Need to expand mask with possible new virtual sites.
139 # Virtual sites should ALWAYS be put AFTER actual atoms, like in
140 # TIP4P: OHHX, OHHX, ...
142 vc = np.zeros(len(vc1) + len(vc2))
143 vc[self.virtual_mask] = vc1
144 vc[~self.virtual_mask] = vc2
146 return vc
148 def add_virtual_sites(self, positions):
149 vs1 = self.atoms1.calc.add_virtual_sites(positions[self.mask])
150 vs2 = self.atoms2.calc.add_virtual_sites(positions[~self.mask])
151 vs = np.zeros((len(vs1) + len(vs2), 3))
153 vs[self.virtual_mask] = vs1
154 vs[~self.virtual_mask] = vs2
156 return vs
158 def make_virtual_mask(self):
159 virtual_mask = []
160 ct1 = 0
161 ct2 = 0
162 for i in range(len(self.mask)):
163 virtual_mask.append(self.mask[i])
164 if self.mask[i]:
165 ct1 += 1
166 if not self.mask[i]:
167 ct2 += 1
168 if ((ct2 == self.apm2) &
169 (self.apm2 != self.atoms2.calc.sites_per_mol)):
170 virtual_mask.append(False)
171 ct2 = 0
172 if ((ct1 == self.apm1) &
173 (self.apm1 != self.atoms1.calc.sites_per_mol)):
174 virtual_mask.append(True)
175 ct1 = 0
177 self.virtual_mask = np.array(virtual_mask)
179 def coulomb(self, xpos1, xpos2, xc1, xc2, spm1, spm2):
180 energy = 0.0
181 forces = np.zeros((len(xc1) + len(xc2), 3))
183 self.xpos1 = xpos1
184 self.xpos2 = xpos2
186 R1 = xpos1
187 R2 = xpos2
188 F1 = np.zeros_like(R1)
189 F2 = np.zeros_like(R2)
190 C1 = xc1.reshape((-1, np.shape(xpos1)[1]))
191 C2 = xc2.reshape((-1, np.shape(xpos2)[1]))
192 # Vectorized evaluation is not as trivial when spm1 != spm2.
193 # This is pretty inefficient, but for ~1-5 counter ions as region 1
194 # it should not matter much ..
195 # There is definitely room for improvements here.
196 cell = self.cell.diagonal()
197 for m1, (r1, c1) in enumerate(zip(R1, C1)):
198 for m2, (r2, c2) in enumerate(zip(R2, C2)):
199 r00 = r2[0] - r1[0]
200 shift = np.zeros(3)
201 for i, periodic in enumerate(self.pbc):
202 if periodic:
203 L = cell[i]
204 shift[i] = (r00[i] + L / 2.) % L - L / 2. - r00[i]
205 r00 += shift
207 d00 = (r00**2).sum()**0.5
208 t = 1
209 dtdd = 0
210 if d00 > self.rc:
211 continue
212 elif d00 > self.rc - self.width:
213 y = (d00 - self.rc + self.width) / self.width
214 t -= y**2 * (3.0 - 2.0 * y)
215 dtdd = r00 * 6 * y * (1.0 - y) / (self.width * d00)
217 for a1 in range(spm1):
218 for a2 in range(spm2):
219 r = r2[a2] - r1[a1] + shift
220 d2 = (r**2).sum()
221 d = d2**0.5
222 e = k_c * c1[a1] * c2[a2] / d
223 energy += t * e
225 F1[m1, a1] -= t * (e / d2) * r
226 F2[m2, a2] += t * (e / d2) * r
228 F1[m1, 0] -= dtdd * e
229 F2[m2, 0] += dtdd * e
231 F1 = F1.reshape((-1, 3))
232 F2 = F2.reshape((-1, 3))
234 # Redist forces but dont save forces in org calculators
235 atoms1 = self.atoms1.copy()
236 atoms1.calc = copy.copy(self.calc1)
237 atoms1.calc.atoms = atoms1
238 F1 = atoms1.calc.redistribute_forces(F1)
239 atoms2 = self.atoms2.copy()
240 atoms2.calc = copy.copy(self.calc2)
241 atoms2.calc.atoms = atoms2
242 F2 = atoms2.calc.redistribute_forces(F2)
244 forces = np.zeros((len(self.atoms), 3))
245 forces[self.mask] = F1
246 forces[~self.mask] = F2
247 return energy, forces
249 def lennard_jones(self, atoms1, atoms2):
250 pos1 = atoms1.get_positions().reshape((-1, self.apm1, 3))
251 pos2 = atoms2.get_positions().reshape((-1, self.apm2, 3))
253 f1 = np.zeros_like(atoms1.positions)
254 f2 = np.zeros_like(atoms2.positions)
255 energy = 0.0
257 cell = self.cell.diagonal()
258 for q, p1 in enumerate(pos1): # molwise loop
259 eps = self.epsilon
260 sig = self.sigma
262 R00 = pos2[:, 0] - p1[0, :]
264 # cutoff from first atom of each mol
265 shift = np.zeros_like(R00)
266 for i, periodic in enumerate(self.pbc):
267 if periodic:
268 L = cell[i]
269 shift[:, i] = (R00[:, i] + L / 2) % L - L / 2 - R00[:, i]
270 R00 += shift
272 d002 = (R00**2).sum(1)
273 d00 = d002**0.5
274 x1 = d00 > self.rc - self.width
275 x2 = d00 < self.rc
276 x12 = np.logical_and(x1, x2)
277 y = (d00[x12] - self.rc + self.width) / self.width
278 t = np.zeros(len(d00))
279 t[x2] = 1.0
280 t[x12] -= y**2 * (3.0 - 2.0 * y)
281 dt = np.zeros(len(d00))
282 dt[x12] -= 6.0 / self.width * y * (1.0 - y)
283 for qa in range(len(p1)):
284 if ~np.any(eps[qa, :]):
285 continue
286 R = pos2 - p1[qa, :] + shift[:, None]
287 d2 = (R**2).sum(2)
288 c6 = (sig[qa, :]**2 / d2)**3
289 c12 = c6**2
290 e = 4 * eps[qa, :] * (c12 - c6)
291 energy += np.dot(e.sum(1), t)
292 f = t[:, None, None] * (24 * eps[qa, :] *
293 (2 * c12 - c6) / d2)[:, :, None] * R
294 f00 = - (e.sum(1) * dt / d00)[:, None] * R00
295 f2 += f.reshape((-1, 3))
296 f1[q * self.apm1 + qa, :] -= f.sum(0).sum(0)
297 f1[q * self.apm1, :] -= f00.sum(0)
298 f2[::self.apm2, :] += f00
300 return energy, f1, f2
302 def redistribute_forces(self, forces):
303 f1 = self.calc1.redistribute_forces(forces[self.virtual_mask])
304 f2 = self.calc2.redistribute_forces(forces[~self.virtual_mask])
305 # and then they are back on the real atom centers so
306 f = np.zeros((len(self.atoms), 3))
307 f[self.mask] = f1
308 f[~self.mask] = f2
309 return f