File: tip3p.py

package info (click to toggle)
python-ase 3.24.0-1
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 15,448 kB
  • sloc: python: 144,945; xml: 2,728; makefile: 113; javascript: 47
file content (167 lines) | stat: -rw-r--r-- 5,434 bytes parent folder | download | duplicates (3)
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
"""TIP3P potential."""

import numpy as np

import ase.units as units
from ase.calculators.calculator import Calculator, all_changes

qH = 0.417
sigma0 = 3.15061
epsilon0 = 0.1521 * units.kcal / units.mol
rOH = 0.9572
angleHOH = 104.52
thetaHOH = 104.52 / 180 * np.pi  # we keep this for backwards compatibility


class TIP3P(Calculator):
    implemented_properties = ['energy', 'forces']
    nolabel = True
    pcpot = None

    def __init__(self, rc=5.0, width=1.0):
        """TIP3P potential.

        rc: float
            Cutoff radius for Coulomb part.
        width: float
            Width for cutoff function for Coulomb part.
        """
        self.rc = rc
        self.width = width
        Calculator.__init__(self)
        self.sites_per_mol = 3

    def calculate(self, atoms=None,
                  properties=['energy'],
                  system_changes=all_changes):
        Calculator.calculate(self, atoms, properties, system_changes)

        R = self.atoms.positions.reshape((-1, 3, 3))
        Z = self.atoms.numbers
        pbc = self.atoms.pbc
        cell = self.atoms.cell.diagonal()
        nh2o = len(R)

        assert (self.atoms.cell == np.diag(cell)).all(), 'not orthorhombic'
        assert ((cell >= 2 * self.rc) | ~pbc).all(), 'cutoff too large'  # ???
        if Z[0] == 8:
            o = 0
        else:
            o = 2
        assert (Z[o::3] == 8).all()
        assert (Z[(o + 1) % 3::3] == 1).all()
        assert (Z[(o + 2) % 3::3] == 1).all()

        charges = np.array([qH, qH, qH])
        charges[o] *= -2

        energy = 0.0
        forces = np.zeros((3 * nh2o, 3))

        for m in range(nh2o - 1):
            DOO = R[m + 1:, o] - R[m, o]
            shift = np.zeros_like(DOO)
            for i, periodic in enumerate(pbc):
                if periodic:
                    L = cell[i]
                    shift[:, i] = (DOO[:, i] + L / 2) % L - L / 2 - DOO[:, i]
            DOO += shift
            d2 = (DOO**2).sum(1)
            d = d2**0.5
            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
            t = np.zeros(len(d))  # cutoff function
            t[x2] = 1.0
            t[x12] -= y**2 * (3.0 - 2.0 * y)
            dtdd = np.zeros(len(d))
            dtdd[x12] -= 6.0 / self.width * y * (1.0 - y)
            c6 = (sigma0**2 / d2)**3
            c12 = c6**2
            e = 4 * epsilon0 * (c12 - c6)
            energy += np.dot(t, e)
            F = (24 * epsilon0 * (2 * c12 - c6) / d2 * t -
                 e * dtdd / d)[:, np.newaxis] * DOO
            forces[m * 3 + o] -= F.sum(0)
            forces[m * 3 + 3 + o::3] += F

            for j in range(3):
                D = R[m + 1:] - R[m, j] + shift[:, np.newaxis]
                r2 = (D**2).sum(axis=2)
                r = r2**0.5
                e = charges[j] * charges / r * units.Hartree * units.Bohr
                energy += np.dot(t, e).sum()
                F = (e / r2 * t[:, np.newaxis])[:, :, np.newaxis] * D
                FOO = -(e.sum(1) * dtdd / d)[:, np.newaxis] * DOO
                forces[(m + 1) * 3 + o::3] += FOO
                forces[m * 3 + o] -= FOO.sum(0)
                forces[(m + 1) * 3:] += F.reshape((-1, 3))
                forces[m * 3 + j] -= F.sum(axis=0).sum(axis=0)

        if self.pcpot:
            e, f = self.pcpot.calculate(np.tile(charges, nh2o),
                                        self.atoms.positions)
            energy += e
            forces += f

        self.results['energy'] = energy
        self.results['forces'] = forces

    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 redistribute_forces(self, forces):
        return forces

    def get_virtual_charges(self, atoms):
        charges = np.empty(len(atoms))
        charges[:] = qH
        if atoms.numbers[0] == 8:
            charges[::3] = -2 * qH
        else:
            charges[2::3] = -2 * qH
        return charges


class PointChargePotential:
    def __init__(self, mmcharges):
        """Point-charge potential for TIP3P.

        Only used for testing QMMM.
        """
        self.mmcharges = mmcharges
        self.mmpositions = None
        self.mmforces = None

    def set_positions(self, mmpositions, com_pv=None):
        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