1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235
|
import numpy as np
import ase.units as units
from ase.calculators.calculator import Calculator, all_changes
from ase.data import atomic_masses
from ase.geometry import find_mic
# Electrostatic constant
k_c = units.Hartree * units.Bohr
# Force field parameters
q_me = 0.206
q_c = 0.247
q_n = -0.453
sigma_me = 3.775
sigma_c = 3.650
sigma_n = 3.200
epsilon_me = 0.7824 * units.kJ / units.mol
epsilon_c = 0.544 * units.kJ / units.mol
epsilon_n = 0.6276 * units.kJ / units.mol
r_mec = 1.458
r_cn = 1.157
r_men = r_mec + r_cn
m_me = atomic_masses[6] + 3 * atomic_masses[1]
m_c = atomic_masses[6]
m_n = atomic_masses[7]
def combine_lj_lorenz_berthelot(sigma, epsilon):
"""Combine LJ parameters according to the Lorenz-Berthelot rule"""
sigma_c = np.zeros((len(sigma), len(sigma)))
epsilon_c = np.zeros_like(sigma_c)
for ii in range(len(sigma)):
sigma_c[:, ii] = (sigma[ii] + sigma) / 2
epsilon_c[:, ii] = (epsilon[ii] * epsilon) ** 0.5
return sigma_c, epsilon_c
class ACN(Calculator):
implemented_properties = ['energy', 'forces']
nolabel = True
def __init__(self, rc=5.0, width=1.0):
"""Three-site potential for acetonitrile.
Atom sequence must be:
MeCNMeCN ... MeCN or NCMeNCMe ... NCMe
When performing molecular dynamics (MD), forces are redistributed
and only Me and N sites propagated based on a scheme for MD of
rigid triatomic molecules from Ciccotti et al. Molecular Physics
1982 (https://doi.org/10.1080/00268978200100942). Apply constraints
using the FixLinearTriatomic to fix the geometry of the acetonitrile
molecules.
rc: float
Cutoff radius for Coulomb interactions.
width: float
Width for cutoff function for Coulomb interactions.
References:
https://doi.org/10.1080/08927020108024509
"""
self.rc = rc
self.width = width
self.forces = None
Calculator.__init__(self)
self.sites_per_mol = 3
self.pcpot = None
def calculate(self, atoms=None,
properties=['energy'],
system_changes=all_changes):
Calculator.calculate(self, atoms, properties, system_changes)
Z = atoms.numbers
masses = atoms.get_masses()
if Z[0] == 7:
n = 0
me = 2
sigma = np.array([sigma_n, sigma_c, sigma_me])
epsilon = np.array([epsilon_n, epsilon_c, epsilon_me])
else:
n = 2
me = 0
sigma = np.array([sigma_me, sigma_c, sigma_n])
epsilon = np.array([epsilon_me, epsilon_c, epsilon_n])
assert (Z[n::3] == 7).all(), 'incorrect atoms sequence'
assert (Z[1::3] == 6).all(), 'incorrect atoms sequence'
assert (masses[n::3] == m_n).all(), 'incorrect masses'
assert (masses[1::3] == m_c).all(), 'incorrect masses'
assert (masses[me::3] == m_me).all(), 'incorrect masses'
R = self.atoms.positions.reshape((-1, 3, 3))
pbc = self.atoms.pbc
cd = self.atoms.cell.diagonal()
nm = len(R)
assert (self.atoms.cell == np.diag(cd)).all(), 'not orthorhombic'
assert ((cd >= 2 * self.rc) | ~pbc).all(), 'cutoff too large'
charges = self.get_virtual_charges(atoms[:3])
# LJ parameters
sigma_co, epsilon_co = combine_lj_lorenz_berthelot(sigma, epsilon)
energy = 0.0
self.forces = np.zeros((3 * nm, 3))
for m in range(nm - 1):
Dmm = R[m + 1:, 1] - R[m, 1]
# MIC PBCs
Dmm_min, Dmm_min_len = find_mic(Dmm, atoms.cell, pbc)
shift = Dmm_min - Dmm
# Smooth cutoff
cut, dcut = self.cutoff(Dmm_min_len)
for j in range(3):
D = R[m + 1:] - R[m, j] + shift[:, np.newaxis]
D_len2 = (D**2).sum(axis=2)
D_len = D_len2**0.5
# Coulomb interactions
e = charges[j] * charges / D_len * k_c
energy += np.dot(cut, e).sum()
F = (e / D_len2 * cut[:, np.newaxis])[:, :, np.newaxis] * D
Fmm = -(e.sum(1) * dcut / Dmm_min_len)[:, np.newaxis] * Dmm_min
self.forces[(m + 1) * 3:] += F.reshape((-1, 3))
self.forces[m * 3 + j] -= F.sum(axis=0).sum(axis=0)
self.forces[(m + 1) * 3 + 1::3] += Fmm
self.forces[m * 3 + 1] -= Fmm.sum(0)
# LJ interactions
c6 = (sigma_co[:, j]**2 / D_len2)**3
c12 = c6**2
e = 4 * epsilon_co[:, j] * (c12 - c6)
energy += np.dot(cut, e).sum()
F = (24 * epsilon_co[:, j] * (2 * c12 -
c6) / D_len2 * cut[:, np.newaxis])[:, :, np.newaxis] * D
Fmm = -(e.sum(1) * dcut / Dmm_min_len)[:, np.newaxis] * Dmm_min
self.forces[(m + 1) * 3:] += F.reshape((-1, 3))
self.forces[m * 3 + j] -= F.sum(axis=0).sum(axis=0)
self.forces[(m + 1) * 3 + 1::3] += Fmm
self.forces[m * 3 + 1] -= Fmm.sum(0)
if self.pcpot:
e, f = self.pcpot.calculate(np.tile(charges, nm),
self.atoms.positions)
energy += e
self.forces += f
self.results['energy'] = energy
self.results['forces'] = self.forces
def redistribute_forces(self, forces):
return forces
def get_molcoms(self, nm):
molcoms = np.zeros((nm, 3))
for m in range(nm):
molcoms[m] = self.atoms[m * 3:(m + 1) * 3].get_center_of_mass()
return molcoms
def cutoff(self, d):
x1 = d > self.rc - self.width
x2 = d < self.rc
x12 = np.logical_and(x1, x2)
y = (d[x12] - self.rc + self.width) / self.width
cut = np.zeros(len(d)) # cutoff function
cut[x2] = 1.0
cut[x12] -= y**2 * (3.0 - 2.0 * y)
dtdd = np.zeros(len(d))
dtdd[x12] -= 6.0 / self.width * y * (1.0 - y)
return cut, dtdd
def embed(self, charges):
"""Embed atoms in point-charges."""
self.pcpot = PointChargePotential(charges)
return self.pcpot
def check_state(self, atoms, tol=1e-15):
system_changes = Calculator.check_state(self, atoms, tol)
if self.pcpot and self.pcpot.mmpositions is not None:
system_changes.append('positions')
return system_changes
def add_virtual_sites(self, positions):
return positions # no virtual sites
def get_virtual_charges(self, atoms):
charges = np.empty(len(atoms))
Z = atoms.numbers
if Z[0] == 7:
n = 0
me = 2
else:
n = 2
me = 0
charges[me::3] = q_me
charges[1::3] = q_c
charges[n::3] = q_n
return charges
class PointChargePotential:
def __init__(self, mmcharges):
"""Point-charge potential for ACN.
Only used for testing QMMM.
"""
self.mmcharges = mmcharges
self.mmpositions = None
self.mmforces = None
def set_positions(self, mmpositions):
self.mmpositions = mmpositions
def calculate(self, qmcharges, qmpositions):
energy = 0.0
self.mmforces = np.zeros_like(self.mmpositions)
qmforces = np.zeros_like(qmpositions)
for C, R, F in zip(self.mmcharges, self.mmpositions, self.mmforces):
d = qmpositions - R
r2 = (d**2).sum(1)
e = units.Hartree * units.Bohr * C * r2**-0.5 * qmcharges
energy += e.sum()
f = (e / r2)[:, np.newaxis] * d
qmforces += f
F -= f.sum(0)
self.mmpositions = None
return energy, qmforces
def get_forces(self, calc):
return self.mmforces
|