ADMPDispPmeForce

This is a convenient wrapper for dispersion PME calculations It wrapps all the environment parameters of multipolar PME calculation The so called "environment paramters" means parameters that do not need to be differentiable

Source code in dmff/admp/disp_pme.py
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
class ADMPDispPmeForce:
    '''
    This is a convenient wrapper for dispersion PME calculations
    It wrapps all the environment parameters of multipolar PME calculation
    The so called "environment paramters" means parameters that do not need to be differentiable
    '''

    def __init__(self, box, rc, ethresh, pmax, lpme=True):

        self.rc = rc
        self.ethresh = ethresh
        self.pmax = pmax
        # Need a different function for dispersion ??? Need tests
        self.lpme = lpme
        if lpme:
            kappa, K1, K2, K3 = setup_ewald_parameters(rc, ethresh, box)
            self.kappa = kappa
            self.K1 = K1
            self.K2 = K2
            self.K3 = K3
        else:
            self.kappa = 0.0
            self.K1 = 0
            self.K2 = 0
            self.K3 = 0
        self.pme_order = 6
        # setup calculators
        self.refresh_calculators()
        return


    def generate_get_energy(self):
        def get_energy(positions, box, pairs, c_list, mScales):
            return energy_disp_pme(positions, box, pairs, 
                                  c_list, mScales,
                                  self.kappa, self.K1, self.K2, self.K3, self.pmax,
                                  self.d6_recip, self.d8_recip, self.d10_recip, lpme=self.lpme)
        return get_energy


    def update_env(self, attr, val):
        '''
        Update the environment of the calculator
        '''
        setattr(self, attr, val)
        self.refresh_calculators()


    def refresh_calculators(self):
        '''
        refresh the energy and force calculator according to the current environment
        '''
        self.d6_recip = generate_pme_recip(Ck_6, self.kappa, True, self.pme_order, self.K1, self.K2, self.K3, 0)
        if self.pmax >= 8:
            self.d8_recip = generate_pme_recip(Ck_8, self.kappa, True, self.pme_order, self.K1, self.K2, self.K3, 0)
        else:
            self.d8_recip = None
        if self.pmax >= 10:
            self.d10_recip = generate_pme_recip(Ck_10, self.kappa, True, self.pme_order, self.K1, self.K2, self.K3, 0)
        else:
            self.d10_recip = None
        # create the energy calculator according to PME environment
        self.get_energy = self.generate_get_energy()
        self.get_forces = value_and_grad(self.get_energy)
        return

refresh_calculators()

refresh the energy and force calculator according to the current environment

Source code in dmff/admp/disp_pme.py
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
def refresh_calculators(self):
    '''
    refresh the energy and force calculator according to the current environment
    '''
    self.d6_recip = generate_pme_recip(Ck_6, self.kappa, True, self.pme_order, self.K1, self.K2, self.K3, 0)
    if self.pmax >= 8:
        self.d8_recip = generate_pme_recip(Ck_8, self.kappa, True, self.pme_order, self.K1, self.K2, self.K3, 0)
    else:
        self.d8_recip = None
    if self.pmax >= 10:
        self.d10_recip = generate_pme_recip(Ck_10, self.kappa, True, self.pme_order, self.K1, self.K2, self.K3, 0)
    else:
        self.d10_recip = None
    # create the energy calculator according to PME environment
    self.get_energy = self.generate_get_energy()
    self.get_forces = value_and_grad(self.get_energy)
    return

update_env(attr, val)

Update the environment of the calculator

Source code in dmff/admp/disp_pme.py
53
54
55
56
57
58
def update_env(self, attr, val):
    '''
    Update the environment of the calculator
    '''
    setattr(self, attr, val)
    self.refresh_calculators()

disp_pme_real(positions, box, pairs, c_list, mScales, kappa, pmax)

This function calculates the dispersion real space energy It expands the atomic parameters to pairwise parameters

Input

positions: Na * 3: positions box: 3 * 3: box, axes arranged in row pairs: Np * 3: interacting pair indices and topology distance c_list: Na * (pmax-4)/2: atomic dispersion coefficients mScales: (Nexcl,): permanent multipole-multipole interaction exclusion scalings: 1-2, 1-3 ... covalent_map: Na * Na: topological distances between atoms, if i, j are topologically distant, then covalent_map[i, j] == 0 kappa: float: kappa in A^-1 pmax: int array: maximal exponents (p) to compute, e.g., (6, 8, 10)

Output

ene: dispersion pme realspace energy

Source code in dmff/admp/disp_pme.py
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
def disp_pme_real(positions, box, pairs, 
        c_list, 
        mScales, 
        kappa, pmax):
    '''
    This function calculates the dispersion real space energy
    It expands the atomic parameters to pairwise parameters

    Input:
        positions:
            Na * 3: positions
        box:
            3 * 3: box, axes arranged in row
        pairs:
            Np * 3: interacting pair indices and topology distance
        c_list:
            Na * (pmax-4)/2: atomic dispersion coefficients
        mScales:
            (Nexcl,): permanent multipole-multipole interaction exclusion scalings: 1-2, 1-3 ...
        covalent_map:
            Na * Na: topological distances between atoms, if i, j are topologically distant, then covalent_map[i, j] == 0
        kappa:
            float: kappa in A^-1
        pmax:
            int array: maximal exponents (p) to compute, e.g., (6, 8, 10)

    Output:
        ene: dispersion pme realspace energy
    '''

    # expand pairwise parameters
    # pairs = pairs[pairs[:, 0] < pairs[:, 1]]
    pairs = pairs.at[:, :2].set(regularize_pairs(pairs[:, :2]))

    box_inv = jnp.linalg.inv(box)

    ri = distribute_v3(positions, pairs[:, 0])
    rj = distribute_v3(positions, pairs[:, 1])
    nbonds = pairs[:, 2]
    mscales = distribute_scalar(mScales, nbonds-1)

    buffer_scales = pair_buffer_scales(pairs[:, :2])
    mscales = mscales * buffer_scales

    ci = distribute_dispcoeff(c_list, pairs[:, 0])
    cj = distribute_dispcoeff(c_list, pairs[:, 1])

    ene_real = jnp.sum(
            disp_pme_real_kernel(ri, rj, ci, cj, box, box_inv, mscales, kappa, pmax)
            * buffer_scales
            )

    return jnp.sum(ene_real)

disp_pme_real_kernel(ri, rj, ci, cj, box, box_inv, mscales, kappa, pmax)

The kernel to calculate the realspace dispersion energy

Inputs

ri: Np * 3: position i rj: Np * 3: position j ci: Np * (pmax-4)/2: dispersion coeffs of i, c6, c8, c10 etc cj: Np * (pmax-4)/2: dispersion coeffs of j, c6, c8, c10 etc kappa: float: kappa pmax: int: largest p in 1/r^p, assume starting from 6 with increment of 2

Output

energy: float: the dispersion pme energy

Source code in dmff/admp/disp_pme.py
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
@partial(vmap, in_axes=(0, 0, 0, 0, None, None, 0, None, None), out_axes=(0))
@jit_condition(static_argnums=(8))
def disp_pme_real_kernel(ri, rj, ci, cj, box, box_inv, mscales, kappa, pmax):
    '''
    The kernel to calculate the realspace dispersion energy

    Inputs:
        ri: 
            Np * 3: position i
        rj:
            Np * 3: position j
        ci: 
            Np * (pmax-4)/2: dispersion coeffs of i, c6, c8, c10 etc
        cj:
            Np * (pmax-4)/2: dispersion coeffs of j, c6, c8, c10 etc
        kappa:
            float: kappa
        pmax:
            int: largest p in 1/r^p, assume starting from 6 with increment of 2

    Output:
        energy: 
            float: the dispersion pme energy
    '''
    dr = ri - rj
    dr = pbc_shift(dr, box, box_inv)
    dr2 = jnp.dot(dr, dr)

    x2 = kappa * kappa * dr2
    g = g_p(x2, pmax)
    dr6 = dr2 * dr2 * dr2
    ene = (mscales + g[0] - 1) * ci[0] * cj[0] / dr6
    if pmax >= 8:
        dr8 = dr6 * dr2
        ene += (mscales + g[1] - 1) * ci[1] * cj[1] / dr8
    if pmax >= 10:
        dr10 = dr8 * dr2
        ene += (mscales + g[2] - 1) * ci[2] * cj[2] / dr10
    return ene

disp_pme_self(c_list, kappa, pmax)

This function calculates the dispersion self energy

Inputs

c_list: Na * 3: dispersion susceptibilities C_6, C_8, C_10 kappa: float: kappa used in dispersion

Output

ene_self: float: the self energy

Source code in dmff/admp/disp_pme.py
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
@jit_condition(static_argnums=(2))
def disp_pme_self(c_list, kappa, pmax):
    '''
    This function calculates the dispersion self energy

    Inputs:
        c_list:
            Na * 3: dispersion susceptibilities C_6, C_8, C_10
        kappa:
            float: kappa used in dispersion

    Output:
        ene_self:
            float: the self energy
    '''
    E_6 = -kappa**6/12 * jnp.sum(c_list[:, 0]**2)
    if pmax >= 8:
        E_8 = -kappa**8/48 * jnp.sum(c_list[:, 1]**2)
    if pmax >= 10:
        E_10 = -kappa**10/240 * jnp.sum(c_list[:, 2]**2)
    E = E_6
    if pmax >= 8:
        E += E_8
    if pmax >= 10:
        E += E_10
    return E

energy_disp_pme(positions, box, pairs, c_list, mScales, kappa, K1, K2, K3, pmax, recip_fn6, recip_fn8, recip_fn10, lpme=True)

Top level wrapper for dispersion pme

Input

positions: Na * 3: positions box: 3 * 3: box, axes arranged in row pairs: Np * 3: interacting pair indices and topology distance c_list: Na * (pmax-4)/2: atomic dispersion coefficients mScales: (Nexcl,): permanent multipole-multipole interaction exclusion scalings: 1-2, 1-3 ... covalent_map: Na * Na: topological distances between atoms, if i, j are topologically distant, then covalent_map[i, j] == 0 disp_pme_recip_fn: function: the reciprocal calculator, see recip.py kappa: float: kappa in A^-1 K1, K2, K3: int: max K for reciprocal calculations pmax: int array: maximal exponents (p) to compute, e.g., (6, 8, 10) lpme: bool: whether do pme or not, useful when doing cluster calculations

Output

energy: total dispersion pme energy

Source code in dmff/admp/disp_pme.py
 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
def energy_disp_pme(positions, box, pairs,
        c_list, mScales,
        kappa, K1, K2, K3, pmax, 
        recip_fn6, recip_fn8, recip_fn10, lpme=True):
    '''
    Top level wrapper for dispersion pme

    Input:
        positions:
            Na * 3: positions
        box:
            3 * 3: box, axes arranged in row
        pairs:
            Np * 3: interacting pair indices and topology distance
        c_list:
            Na * (pmax-4)/2: atomic dispersion coefficients
        mScales:
            (Nexcl,): permanent multipole-multipole interaction exclusion scalings: 1-2, 1-3 ...
        covalent_map:
            Na * Na: topological distances between atoms, if i, j are topologically distant, then covalent_map[i, j] == 0
        disp_pme_recip_fn:
            function: the reciprocal calculator, see recip.py
        kappa:
            float: kappa in A^-1
        K1, K2, K3:
            int: max K for reciprocal calculations
        pmax:
            int array: maximal exponents (p) to compute, e.g., (6, 8, 10)
        lpme:
            bool: whether do pme or not, useful when doing cluster calculations

    Output:
        energy: total dispersion pme energy
    '''

    if lpme is False:
        kappa = 0

    ene_real = disp_pme_real(positions, box, pairs, c_list, mScales, kappa, pmax)

    if lpme:
        ene_recip = recip_fn6(positions, box, c_list[:, 0, jnp.newaxis])
        if pmax >= 8:
            ene_recip += recip_fn8(positions, box, c_list[:, 1, jnp.newaxis])
        if pmax >= 10:
            ene_recip += recip_fn10(positions, box, c_list[:, 2, jnp.newaxis])
        ene_self = disp_pme_self(c_list, kappa, pmax)
        return ene_real + ene_recip + ene_self

    else:
        return ene_real

g_p(x2, pmax)

Compute the g(x, p) function

Inputs

x: float: the input variable pmax: int: the maximal powers of dispersion, here we assume evenly spacing even powers starting from 6 e.g., (6,), (6, 8) or (6, 8, 10)

Outputs

g: (p-4)//2: g(x, p)

Source code in dmff/admp/disp_pme.py
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
def g_p(x2, pmax):
    '''
    Compute the g(x, p) function

    Inputs:
        x:
            float: the input variable
        pmax:
            int: the maximal powers of dispersion, here we assume evenly spacing even powers starting from 6
            e.g., (6,), (6, 8) or (6, 8, 10)

    Outputs:
        g:
            (p-4)//2: g(x, p)
    '''

    x4 = x2 * x2
    x8 = x4 * x4
    exp_x2 = jnp.exp(-x2)
    g6 = 1 + x2 + 0.5*x4
    if pmax >= 8:
        g8 = g6 + x4*x2/6
    if pmax >= 10:
        g10 = g8 + x8/24

    if pmax == 6:
        g = jnp.array([g6])
    elif pmax == 8:
        g = jnp.array([g6, g8])
    elif pmax == 10:
        g = jnp.array([g6, g8, g10])

    return g * exp_x2