mlipops 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mlipops-0.5.0/LICENSE +21 -0
- mlipops-0.5.0/PKG-INFO +20 -0
- mlipops-0.5.0/README.md +44 -0
- mlipops-0.5.0/mlipops/__init__.py +9 -0
- mlipops-0.5.0/mlipops/coulomb.py +131 -0
- mlipops-0.5.0/mlipops/coulombewald.py +281 -0
- mlipops-0.5.0/mlipops/coulombnc.py +120 -0
- mlipops-0.5.0/mlipops/coulombpme.py +559 -0
- mlipops-0.5.0/mlipops/coulombrf.py +119 -0
- mlipops-0.5.0/mlipops/dftd3.py +206 -0
- mlipops-0.5.0/mlipops/dftd3_triton.py +86 -0
- mlipops-0.5.0/mlipops/neighborlist.py +314 -0
- mlipops-0.5.0/mlipops/neighborlist_triton.py +130 -0
- mlipops-0.5.0/mlipops/pairwise.py +126 -0
- mlipops-0.5.0/mlipops/pme_triton.py +185 -0
- mlipops-0.5.0/mlipops/utils.py +153 -0
- mlipops-0.5.0/mlipops/utils_triton.py +33 -0
- mlipops-0.5.0/mlipops/zbl.py +95 -0
- mlipops-0.5.0/mlipops.egg-info/PKG-INFO +20 -0
- mlipops-0.5.0/mlipops.egg-info/SOURCES.txt +23 -0
- mlipops-0.5.0/mlipops.egg-info/dependency_links.txt +1 -0
- mlipops-0.5.0/mlipops.egg-info/requires.txt +1 -0
- mlipops-0.5.0/mlipops.egg-info/top_level.txt +1 -0
- mlipops-0.5.0/pyproject.toml +26 -0
- mlipops-0.5.0/setup.cfg +4 -0
mlipops-0.5.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Peter Eastman
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
mlipops-0.5.0/PKG-INFO
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mlipops
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: PyTorch operations for use in creating machine learning interatomic potentials
|
|
5
|
+
Author: Peter Eastman
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/peastman/mlipops
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Environment :: GPU
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Operating System :: MacOS
|
|
13
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
14
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
15
|
+
Classifier: Programming Language :: Python
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Chemistry
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: torch
|
|
20
|
+
Dynamic: license-file
|
mlipops-0.5.0/README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# MLIPOps
|
|
2
|
+
|
|
3
|
+
MLIPOps is a collection of PyTorch modules for use in creating machine learning interatomic potentials (MLIPs). It
|
|
4
|
+
is written in Python using PyTorch and Triton to do calculations. It is based around the following design principles.
|
|
5
|
+
|
|
6
|
+
- Pure Python. This avoids the compatibility, maintainability, and distribution challenges that come with compiled
|
|
7
|
+
extensions.
|
|
8
|
+
- Highly portable. It works on any computer that PyTorch can run on.
|
|
9
|
+
- Fast performance on any hardware that Triton supports. Currently that includes NVIDIA and AMD GPUs, with others
|
|
10
|
+
likely to be added in the future.
|
|
11
|
+
- Clean, simple code. We rely on `torch.compile` as the first line of optimization, adding Triton kernels only for
|
|
12
|
+
calculations PyTorch cannot do a satisfactory job of optimizing on its own. This keeps the code base simple and easy
|
|
13
|
+
to understand.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
To install, check out the code and type the following command from the root directory.
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
pip install .
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
PyPI packages will be coming once the code matures a little more.
|
|
24
|
+
|
|
25
|
+
## Documentation
|
|
26
|
+
|
|
27
|
+
See the [User Guide](https://openmm.github.io/mlipops/dev/userguide.html) and [API documentation](https://openmm.github.io/mlipops/dev/api.html)
|
|
28
|
+
for instructions on how to use MLIPOps.
|
|
29
|
+
|
|
30
|
+
## Features
|
|
31
|
+
|
|
32
|
+
These are the currently implemented features. Expect this list to grow with time.
|
|
33
|
+
|
|
34
|
+
- Neighbor list construction
|
|
35
|
+
- Coulomb interactions
|
|
36
|
+
- No cutoff: charges and dipoles, non-periodic systems
|
|
37
|
+
- Reaction field: charges only, periodic and non-periodic systems
|
|
38
|
+
- Ewald summation: charges and dipoles, periodic systems
|
|
39
|
+
- Particle Mesh Ewald: charges and dipoles, periodic systems
|
|
40
|
+
- Ziegler-Biersack-Littmark (ZBL) potential
|
|
41
|
+
- DFT-D3(BJ) dispersion
|
|
42
|
+
- Arbitrary pairwise potentials
|
|
43
|
+
- Batch computation
|
|
44
|
+
- Periodic boundary conditions with triclinic boxes
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from .coulombewald import CoulombEwald
|
|
2
|
+
from .coulombnc import CoulombNC
|
|
3
|
+
from .coulombpme import CoulombPME
|
|
4
|
+
from .coulombrf import CoulombRF
|
|
5
|
+
from .dftd3 import DFTD3
|
|
6
|
+
from .neighborlist import NeighborList
|
|
7
|
+
from .pairwise import Pairwise
|
|
8
|
+
from .utils import periodic_displacements, batch_periodic_displacements, pairwise_displacements, batch_pairwise_displacements, get_covalent_radii
|
|
9
|
+
from .zbl import ZBL
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import torch
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def point_charge_interaction(pairs, r, delta, charge):
|
|
7
|
+
"""Compute the Coulomb interaction between point charges. This function is designed for use with Pairwise."""
|
|
8
|
+
return charge[pairs[:,0]]*charge[pairs[:,1]]/r
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def dipole_interaction(pairs, r, delta, params):
|
|
12
|
+
"""Compute the Coulomb interaction between multipoles, each having a charge and dipole moment. This function is
|
|
13
|
+
designed for use with Pairwise."""
|
|
14
|
+
charge, dipole = params
|
|
15
|
+
p1 = pairs[:,0]
|
|
16
|
+
p2 = pairs[:,1]
|
|
17
|
+
c1 = charge[p1]
|
|
18
|
+
c2 = charge[p2]
|
|
19
|
+
d1 = dipole[p1]
|
|
20
|
+
d2 = dipole[p2]
|
|
21
|
+
d1delta = (d1*delta).sum(axis=1)
|
|
22
|
+
d2delta = (d2*delta).sum(axis=1)
|
|
23
|
+
denom3 = r**-3
|
|
24
|
+
energy_cc = c1*c2/r
|
|
25
|
+
energy_cd = (c2*d1delta - c1*d2delta)*denom3
|
|
26
|
+
energy_dd = ((d1*d2).sum(axis=1) - 3*d1delta*d2delta*r**-2)*denom3
|
|
27
|
+
return energy_cc + energy_cd + energy_dd
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ErfcScaledInteraction(object):
|
|
31
|
+
"""Scale an interaction function by erfc(alpha*r), where alpha is a constant. This corresponds to the direct space
|
|
32
|
+
part of Ewald summation. This is a callable object designed for use with Pairwise."""
|
|
33
|
+
def __init__(self, computation: Callable, alpha: float):
|
|
34
|
+
self.computation = computation
|
|
35
|
+
self.alpha = alpha
|
|
36
|
+
|
|
37
|
+
def __call__(self, pairs, r, delta, params):
|
|
38
|
+
return torch.erfc(self.alpha*r)*self.computation(pairs, r, delta, params)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ErfScaledInteraction(object):
|
|
42
|
+
"""Scale an interaction function by erf(alpha*r), where alpha is a constant. This corresponds to the reciprocal
|
|
43
|
+
space part of Ewald summation. This is a callable object designed for use with Pairwise."""
|
|
44
|
+
def __init__(self, computation: Callable, alpha: float):
|
|
45
|
+
self.computation = computation
|
|
46
|
+
self.alpha = alpha
|
|
47
|
+
|
|
48
|
+
def __call__(self, pairs, r, delta, params):
|
|
49
|
+
return torch.erf(self.alpha*r)*self.computation(pairs, r, delta, params)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ReactionFieldInteraction(object):
|
|
53
|
+
"""Compute the Coulomb interaction between point charges using reaction field. This function is designed for use
|
|
54
|
+
with Pairwise."""
|
|
55
|
+
def __init__(self, cutoff: float, dielectric: float):
|
|
56
|
+
self.k = (cutoff**-3)*(dielectric-1)/(2*dielectric+1)
|
|
57
|
+
self.c = 3*dielectric/(cutoff*(2*dielectric+1))
|
|
58
|
+
|
|
59
|
+
def __call__(self, pairs, r, delta, params):
|
|
60
|
+
return params[pairs[:,0]]*params[pairs[:,1]]*(1/r + self.k*r**2 - self.c)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ErfcScaledDipoleInteraction(object):
|
|
64
|
+
"""Compute the Coulomb interaction between multipoles, each having a charge and dipole moment, with the strength
|
|
65
|
+
scaled by erfc(alpha*r). This corresponds to the direct space part of Ewald summation. This is a callable object
|
|
66
|
+
designed for use with Pairwise."""
|
|
67
|
+
def __init__(self, alpha: float):
|
|
68
|
+
self.alpha = alpha
|
|
69
|
+
self.temp1 = 2*alpha/math.sqrt(math.pi)
|
|
70
|
+
self.temp2 = 4*alpha**3/math.sqrt(math.pi)
|
|
71
|
+
|
|
72
|
+
def __call__(self, pairs, r, delta, params):
|
|
73
|
+
charge, dipole = params
|
|
74
|
+
p1 = pairs[:,0]
|
|
75
|
+
p2 = pairs[:,1]
|
|
76
|
+
c1 = charge[p1]
|
|
77
|
+
c2 = charge[p2]
|
|
78
|
+
d1 = dipole[p1]
|
|
79
|
+
d2 = dipole[p2]
|
|
80
|
+
d1delta = (d1*delta).sum(axis=1)
|
|
81
|
+
d2delta = (d2*delta).sum(axis=1)
|
|
82
|
+
alphar = self.alpha*r
|
|
83
|
+
rinv2 = r**-2
|
|
84
|
+
expfactor = torch.exp(-alphar**2)
|
|
85
|
+
|
|
86
|
+
# See equations 2.8 and 2.10 of https://doi.org/10.1063/1.1324708.
|
|
87
|
+
|
|
88
|
+
b0 = torch.erfc(alphar)/r
|
|
89
|
+
b1 = rinv2*(b0 + self.temp1*expfactor)
|
|
90
|
+
b2 = rinv2*(3*b1 + self.temp2*expfactor)
|
|
91
|
+
g0 = c1*c2
|
|
92
|
+
g1 = c2*d1delta - c1*d2delta + (d1*d2).sum(axis=1)
|
|
93
|
+
g2 = -d1delta*d2delta
|
|
94
|
+
return (b0*g0 + b1*g1 + b2*g2)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class ErfScaledDipoleInteraction(object):
|
|
98
|
+
"""Compute the Coulomb interaction between multipoles, each having a charge and dipole moment, with the strength
|
|
99
|
+
scaled by erf(alpha*r). This corresponds to the reciprocal space part of Ewald summation. This is a callable object
|
|
100
|
+
designed for use with Pairwise."""
|
|
101
|
+
def __init__(self, alpha: float):
|
|
102
|
+
self.alpha = alpha
|
|
103
|
+
self.temp1 = 2*alpha/math.sqrt(math.pi)
|
|
104
|
+
self.temp2 = 4*alpha**3/math.sqrt(math.pi)
|
|
105
|
+
|
|
106
|
+
def __call__(self, pairs, r, delta, params):
|
|
107
|
+
# This calculation is identical to the one for ErfcScaledDipoleInteraction above, except that erfc() and its
|
|
108
|
+
# derivative are replaced by erf().
|
|
109
|
+
|
|
110
|
+
charge, dipole = params
|
|
111
|
+
p1 = pairs[:,0]
|
|
112
|
+
p2 = pairs[:,1]
|
|
113
|
+
c1 = charge[p1]
|
|
114
|
+
c2 = charge[p2]
|
|
115
|
+
d1 = dipole[p1]
|
|
116
|
+
d2 = dipole[p2]
|
|
117
|
+
d1delta = (d1*delta).sum(axis=1)
|
|
118
|
+
d2delta = (d2*delta).sum(axis=1)
|
|
119
|
+
alphar = self.alpha*r
|
|
120
|
+
rinv2 = r**-2
|
|
121
|
+
expfactor = -torch.exp(-alphar**2)
|
|
122
|
+
|
|
123
|
+
# See equations 2.8 and 2.10 of https://doi.org/10.1063/1.1324708.
|
|
124
|
+
|
|
125
|
+
b0 = torch.erf(alphar)/r
|
|
126
|
+
b1 = rinv2*(b0 + self.temp1*expfactor)
|
|
127
|
+
b2 = rinv2*(3*b1 + self.temp2*expfactor)
|
|
128
|
+
g0 = c1*c2
|
|
129
|
+
g1 = c2*d1delta - c1*d2delta + (d1*d2).sum(axis=1)
|
|
130
|
+
g2 = -d1delta*d2delta
|
|
131
|
+
return (b0*g0 + b1*g1 + b2*g2)
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import math
|
|
3
|
+
from . import coulomb
|
|
4
|
+
from .neighborlist import NeighborList
|
|
5
|
+
from .pairwise import Pairwise
|
|
6
|
+
from .utils import periodic_displacements
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CoulombEwald(torch.nn.Module):
|
|
10
|
+
"""Compute Coulomb interactions using Ewald summation.
|
|
11
|
+
|
|
12
|
+
This class computes the energy of an infinite set of multipoles repeating periodically through space. The
|
|
13
|
+
interaction is divided into a short range part, which is computed in direct space, and a long range part, which is
|
|
14
|
+
computed in reciprocal space. The division between the two is set by a parameter `alpha`, which can be adjusted to
|
|
15
|
+
minimize the total cost of computing both parts.
|
|
16
|
+
|
|
17
|
+
By default, the multipoles are simply point charges. You can also include dipoles by passing
|
|
18
|
+
`max_multipole='dipole'` to the constructor. In that case, you must provide dipole moments along with charges when
|
|
19
|
+
invoking the module.
|
|
20
|
+
|
|
21
|
+
You can optionally specify that certain interactions should be omitted when computing the energy. This is typically
|
|
22
|
+
used for nearby atoms within the same molecule. When two atoms are listed as an exclusion, only the interaction of
|
|
23
|
+
each with the same periodic copy of the other (that is, not applying periodic boundary conditions) is excluded.
|
|
24
|
+
Each atom still interacts with all the periodic copies of the other.
|
|
25
|
+
|
|
26
|
+
Due to the way the reciprocal space term is calculated, it is impossible to prevent it from including excluded
|
|
27
|
+
interactions. The direct space term therefore compensates for it, subtracting off the energy that was incorrectly
|
|
28
|
+
included in reciprocal space. The sum of the two terms thus yields the correct energy with the interaction fully
|
|
29
|
+
excluded.
|
|
30
|
+
|
|
31
|
+
In addition to calculating energy and forces, this class can compute the electric field at arbitrary points in
|
|
32
|
+
space. To do this, call compute_field().
|
|
33
|
+
|
|
34
|
+
When you create an instance of this class, you must specify the value of Coulomb's constant 1/(4*pi*eps0). Its
|
|
35
|
+
value depends on the units used for energy and distance. The value you specify thus sets the unit system. See the
|
|
36
|
+
User Guide for the values in common unit systems.
|
|
37
|
+
"""
|
|
38
|
+
def __init__(self, neighbor_list: NeighborList, exclusions: torch.Tensor, kmaxx: int, kmaxy: int, kmaxz: int,
|
|
39
|
+
alpha: float, prefactor: float, cutoff: float | None = None, max_multipole='charge'):
|
|
40
|
+
"""Create on object for computing Coulomb interactions.
|
|
41
|
+
|
|
42
|
+
Parameters
|
|
43
|
+
----------
|
|
44
|
+
neighbor_list: NeighborList
|
|
45
|
+
the NeighborList used to identify direct space interactions. It determines the direct space cutoff
|
|
46
|
+
distance, the device to run on, and whether padding is used to enable caching of neighbors.
|
|
47
|
+
exclusions: torch.Tensor
|
|
48
|
+
a tensor of shape (n_exclusions, 2). Each row contains the indices of two particles whose interaction
|
|
49
|
+
should be omitted.
|
|
50
|
+
kmaxx: int
|
|
51
|
+
the index of the maximum wave vector in the x direction. All vectors between -kmaxx and kmaxx are included.
|
|
52
|
+
kmaxy: int
|
|
53
|
+
the index of the maximum wave vector in the x direction. All vectors between -kmaxy and kmaxy are included.
|
|
54
|
+
kmaxz: int
|
|
55
|
+
the index of the maximum wave vector in the x direction. All vectors between -kmaxz and kmaxz are included.
|
|
56
|
+
alpha: float
|
|
57
|
+
the coefficient of the erf() function used to separate the energy into direct and reciprocal space terms
|
|
58
|
+
prefactor: float
|
|
59
|
+
Coulomb's constant 1/(4*pi*eps0). This sets the unit system.
|
|
60
|
+
cutoff: float | None
|
|
61
|
+
the cutoff distance used when computing direct space interactions. If None, the NeighborList's cutoff
|
|
62
|
+
is used. This argument is useful when a single NeighborList is shared by multiple interactions that use
|
|
63
|
+
different cutoffs. The value may never be greater than the NeighborList's cutoff.
|
|
64
|
+
max_multipole: str
|
|
65
|
+
the maximum multipole order for each particle. Allowed options are `'charge'` (point charges only) and
|
|
66
|
+
`'dipole'` (charges and dipoles).
|
|
67
|
+
"""
|
|
68
|
+
if neighbor_list.include_self or neighbor_list.include_symmetric:
|
|
69
|
+
raise ValueError('The neighbor list for Coulomb should not include self interactions or symmetric interactions')
|
|
70
|
+
if kmaxx < 1:
|
|
71
|
+
raise ValueError('kmaxx must be positive')
|
|
72
|
+
if kmaxy < 1:
|
|
73
|
+
raise ValueError('kmaxy must be positive')
|
|
74
|
+
if kmaxz < 1:
|
|
75
|
+
raise ValueError('kmaxz must be positive')
|
|
76
|
+
if alpha <= 0:
|
|
77
|
+
raise ValueError('alpha must be positive')
|
|
78
|
+
if prefactor <= 0:
|
|
79
|
+
raise ValueError('prefactor must be positive')
|
|
80
|
+
if cutoff is not None and cutoff > neighbor_list.cutoff:
|
|
81
|
+
raise ValueError("The cutoff cannot be larger than the NeighborList's cutoff")
|
|
82
|
+
super().__init__()
|
|
83
|
+
device = neighbor_list.device
|
|
84
|
+
self.neighbor_list = neighbor_list
|
|
85
|
+
self.register_buffer('exclusions', exclusions)
|
|
86
|
+
self.kmaxx = kmaxx
|
|
87
|
+
self.kmaxy = kmaxy
|
|
88
|
+
self.kmaxz = kmaxz
|
|
89
|
+
self.alpha = alpha
|
|
90
|
+
self._exp_coeff = -1/(4*alpha*alpha)
|
|
91
|
+
self.prefactor = prefactor
|
|
92
|
+
self.cutoff = neighbor_list.cutoff if cutoff is None else cutoff
|
|
93
|
+
self.max_multipole = max_multipole
|
|
94
|
+
if max_multipole == 'charge':
|
|
95
|
+
self.direct = Pairwise(coulomb.ErfcScaledInteraction(coulomb.point_charge_interaction, alpha), self.cutoff, exclusions)
|
|
96
|
+
self.exclusion_correction = Pairwise(coulomb.ErfScaledInteraction(coulomb.point_charge_interaction, alpha), None)
|
|
97
|
+
elif max_multipole == 'dipole':
|
|
98
|
+
self.direct = Pairwise(coulomb.ErfcScaledDipoleInteraction(alpha), self.cutoff, exclusions)
|
|
99
|
+
self.exclusion_correction = Pairwise(coulomb.ErfScaledDipoleInteraction(alpha), None)
|
|
100
|
+
else:
|
|
101
|
+
raise ValueError(f'Illegal value for max_multipole: {max_multipole}')
|
|
102
|
+
|
|
103
|
+
# Compute the list of wave vector indices. Because of symmetry, we only need to include half of them.
|
|
104
|
+
|
|
105
|
+
nx = kmaxx+1
|
|
106
|
+
ny = 2*kmaxy+1
|
|
107
|
+
nz = 2*kmaxz+1
|
|
108
|
+
index = torch.arange(nx*ny*nz, device=device)
|
|
109
|
+
wave_indices = torch.stack([index//(ny*nz), (index//nz)%ny-kmaxy, index%nz-kmaxz], dim=1)
|
|
110
|
+
mask = (wave_indices[:,0] > 0) | (wave_indices[:,1] > 0) | ((wave_indices[:,1] == 0) & (wave_indices[:,2] > 0))
|
|
111
|
+
self.wave_indices = torch.nn.Parameter(wave_indices[mask].to(torch.float32), requires_grad=False)
|
|
112
|
+
|
|
113
|
+
def forward(self, positions: torch.Tensor, charges: torch.Tensor, box_vectors: torch.Tensor,
|
|
114
|
+
include_direct: bool = True, include_reciprocal: bool = True, dipoles: torch.Tensor | None = None,
|
|
115
|
+
batch: torch.Tensor | None = None) -> torch.Tensor:
|
|
116
|
+
"""Compute the interaction.
|
|
117
|
+
|
|
118
|
+
Parameters
|
|
119
|
+
----------
|
|
120
|
+
positions: torch.Tensor
|
|
121
|
+
a Tensor of shape (n_particles, 3) containing the Cartesian coordinates of each particle
|
|
122
|
+
charges:
|
|
123
|
+
a Tensor of shape (n_particles,) containing the charge of each particle
|
|
124
|
+
box_vectors: torch.Tensor | None
|
|
125
|
+
if batch is None, a Tensor of shape (3, 3) containing box vectors defining the periodic box. If batch is
|
|
126
|
+
not None, a Tensor of shape (n_systems, 3, 3) containing the box vectors for each system. If None, periodic
|
|
127
|
+
boundary conditions are not used.
|
|
128
|
+
include_direct: bool
|
|
129
|
+
specifies whether the direct space term should be included in the result
|
|
130
|
+
include_reciprocal: bool
|
|
131
|
+
specifies whether the reciprocal space term should be included in the result
|
|
132
|
+
dipoles: torch.Tensor | None
|
|
133
|
+
a Tensor of shape (n_particles, 3) containing the dipole moment of each particle. If max_multipole is
|
|
134
|
+
'charge', this is ignored.
|
|
135
|
+
batch: torch.Tensor | None
|
|
136
|
+
a Tensor of shape (n_particles,) containing the index of the system each particle belongs to. This must be
|
|
137
|
+
sorted in ascending order, and every system must contain at least one particle. If None, the interaction
|
|
138
|
+
is computed for a single system instead of a batch of systems.
|
|
139
|
+
|
|
140
|
+
Returns
|
|
141
|
+
-------
|
|
142
|
+
torch.Tensor:
|
|
143
|
+
a torch.Tensor containing the energy of the interaction. If batch is None, this is a scalar containing the
|
|
144
|
+
total energy. Otherwise, it has shape (n_systems,) containing the energy of each system in the batch.
|
|
145
|
+
"""
|
|
146
|
+
if batch is None:
|
|
147
|
+
num_systems = 1
|
|
148
|
+
else:
|
|
149
|
+
num_systems = batch.max()+1
|
|
150
|
+
energy = torch.zeros((num_systems,), dtype=torch.float32, device=positions.device)
|
|
151
|
+
if include_direct:
|
|
152
|
+
neighbors = self.neighbor_list(positions, box_vectors, batch)
|
|
153
|
+
if self.max_multipole == 'charge':
|
|
154
|
+
params = charges
|
|
155
|
+
else:
|
|
156
|
+
params = (charges, dipoles)
|
|
157
|
+
energy += self.direct(positions, params, neighbors, box_vectors, batch)
|
|
158
|
+
if self.exclusions is not None:
|
|
159
|
+
energy -= self.exclusion_correction(positions, params, self.exclusions, None, batch)
|
|
160
|
+
if include_reciprocal:
|
|
161
|
+
if batch is None:
|
|
162
|
+
volume = box_vectors.diag().prod()
|
|
163
|
+
energy -= torch.sum(charges**2)*self.alpha/math.sqrt(torch.pi)
|
|
164
|
+
energy -= 0.5*torch.pi*torch.sum(charges)**2/(volume*self.alpha*self.alpha)
|
|
165
|
+
if self.max_multipole != 'charge':
|
|
166
|
+
energy -= (2/3)*torch.sum(dipoles*dipoles)*self.alpha**3/math.sqrt(torch.pi)
|
|
167
|
+
energy += self._compute_recip_energy(positions, charges, dipoles, box_vectors)
|
|
168
|
+
else:
|
|
169
|
+
volume = torch.einsum('ijj->ij', box_vectors).prod(dim=1)
|
|
170
|
+
sum_charges2 = torch.zeros_like(energy)
|
|
171
|
+
sum_charges2.scatter_add_(0, batch, charges**2)
|
|
172
|
+
energy -= sum_charges2*self.alpha/math.sqrt(torch.pi)
|
|
173
|
+
sum_charges = torch.zeros_like(energy)
|
|
174
|
+
sum_charges.scatter_add_(0, batch, charges)
|
|
175
|
+
energy -= 0.5*torch.pi*sum_charges**2/(volume*self.alpha*self.alpha)
|
|
176
|
+
if self.max_multipole != 'charge':
|
|
177
|
+
sum_dipoles2 = torch.zeros_like(energy)
|
|
178
|
+
sum_dipoles2.scatter_add_(0, batch, (dipoles**2).sum(axis=1))
|
|
179
|
+
energy -= (2/3)*sum_dipoles2*self.alpha**3/math.sqrt(torch.pi)
|
|
180
|
+
energy += self._compute_recip_energy_batch(positions, charges, dipoles, box_vectors, batch, num_systems)
|
|
181
|
+
return self.prefactor*energy
|
|
182
|
+
|
|
183
|
+
def compute_field(self, field_positions: torch.Tensor, positions: torch.Tensor, charges: torch.Tensor,
|
|
184
|
+
box_vectors: torch.Tensor, include_direct: bool = True, include_reciprocal: bool = True,
|
|
185
|
+
dipoles: torch.Tensor | None = None) -> torch.Tensor:
|
|
186
|
+
"""Compute the electric field produced by the particles at a set of points.
|
|
187
|
+
|
|
188
|
+
Parameters
|
|
189
|
+
----------
|
|
190
|
+
field_positions: torch.Tensor
|
|
191
|
+
a Tensor of shape (n_points, 3) containing the positions at which to compute the field
|
|
192
|
+
positions: torch.Tensor
|
|
193
|
+
a Tensor of shape (n_particles, 3) containing the Cartesian coordinates of each particle
|
|
194
|
+
charges:
|
|
195
|
+
a Tensor of shape (n_particles,) containing the charge of each particle
|
|
196
|
+
box_vectors: torch.Tensor
|
|
197
|
+
a Tensor of shape (3, 3) containing box vectors defining the periodic box.
|
|
198
|
+
include_direct: bool
|
|
199
|
+
specifies whether the direct space term should be included in the result
|
|
200
|
+
include_reciprocal: bool
|
|
201
|
+
specifies whether the reciprocal space term should be included in the result
|
|
202
|
+
dipoles: torch.Tensor | None
|
|
203
|
+
a Tensor of shape (n_particles, 3) containing the dipole moment of each particle. If max_multipole is
|
|
204
|
+
'charge', this is ignored.
|
|
205
|
+
|
|
206
|
+
Returns
|
|
207
|
+
-------
|
|
208
|
+
torch.Tensor:
|
|
209
|
+
a Tensor of shape (n_points, 3) containing the electric field at each of the points
|
|
210
|
+
"""
|
|
211
|
+
if include_direct:
|
|
212
|
+
delta = periodic_displacements(field_positions.view((-1,1,3))-positions, box_vectors)
|
|
213
|
+
r = torch.linalg.vector_norm(delta, dim=2, keepdim=True)
|
|
214
|
+
temp1 = 2*self.alpha/math.sqrt(math.pi)
|
|
215
|
+
temp2 = 4*self.alpha**3/math.sqrt(math.pi)
|
|
216
|
+
alphar = self.alpha*r
|
|
217
|
+
rinv2 = r**-2
|
|
218
|
+
expfactor = torch.exp(-alphar**2)
|
|
219
|
+
b0 = torch.erfc(alphar)/r
|
|
220
|
+
b1 = rinv2*(b0 + temp1*expfactor)
|
|
221
|
+
b2 = rinv2*(3*b1 + temp2*expfactor)
|
|
222
|
+
field = charges.unsqueeze(1)*delta*b1
|
|
223
|
+
if self.max_multipole != 'charge':
|
|
224
|
+
field += -b1*dipoles + b2*(dipoles.unsqueeze(0)*delta).sum(axis=2, keepdim=True)*delta
|
|
225
|
+
field = torch.where((r > 0)*(r < self.cutoff), field, 0)
|
|
226
|
+
field = field.sum(dim=1)
|
|
227
|
+
else:
|
|
228
|
+
field = torch.zeros_like(field_positions)
|
|
229
|
+
if include_reciprocal:
|
|
230
|
+
sum1, sum2, k, ak, recip_box_vectors = self._compute_recip_sums(positions, charges, dipoles, box_vectors)
|
|
231
|
+
phase = k@field_positions.T
|
|
232
|
+
temp = 8*torch.pi*recip_box_vectors.diag().prod()*ak.unsqueeze(1)*(sum1.unsqueeze(1)*torch.sin(phase) - sum2.unsqueeze(1)*torch.cos(phase))
|
|
233
|
+
field += (temp.unsqueeze(2)*k.unsqueeze(1)).sum(dim=0)
|
|
234
|
+
return self.prefactor*field
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _compute_recip_sums(self, positions: torch.Tensor, charges: torch.Tensor, dipoles: torch.Tensor | None,
|
|
238
|
+
box_vectors: torch.Tensor):
|
|
239
|
+
recip_box_vectors = torch.linalg.inv(box_vectors)
|
|
240
|
+
k = self.wave_indices@(2*torch.pi*recip_box_vectors.T)
|
|
241
|
+
phase = k@positions.T
|
|
242
|
+
if self.max_multipole == 'charge':
|
|
243
|
+
sum1 = (torch.cos(phase)*charges).sum(dim=1)
|
|
244
|
+
sum2 = (torch.sin(phase)*charges).sum(dim=1)
|
|
245
|
+
else:
|
|
246
|
+
kd = k@dipoles.T
|
|
247
|
+
sum1 = (torch.cos(phase)*charges - torch.sin(phase)*kd).sum(dim=1)
|
|
248
|
+
sum2 = (torch.sin(phase)*charges + torch.cos(phase)*kd).sum(dim=1)
|
|
249
|
+
k2 = (k*k).sum(dim=1)
|
|
250
|
+
ak = torch.exp(self._exp_coeff*k2)/k2
|
|
251
|
+
return sum1, sum2, k, ak, recip_box_vectors
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _compute_recip_energy(self, positions: torch.Tensor, charges: torch.Tensor, dipoles: torch.Tensor | None,
|
|
255
|
+
box_vectors: torch.Tensor):
|
|
256
|
+
sum1, sum2, _, ak, recip_box_vectors = self._compute_recip_sums(positions, charges, dipoles, box_vectors)
|
|
257
|
+
energy = torch.sum(ak*(sum1**2 + sum2**2))
|
|
258
|
+
return energy*4*torch.pi*recip_box_vectors.diag().prod()
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _compute_recip_energy_batch(self, positions: torch.Tensor, charges: torch.Tensor, dipoles: torch.Tensor | None,
|
|
262
|
+
box_vectors: torch.Tensor, batch: torch.Tensor | None, num_systems: int):
|
|
263
|
+
recip_box_vectors = torch.linalg.inv(box_vectors)
|
|
264
|
+
k = self.wave_indices.unsqueeze(0)@(2*torch.pi*recip_box_vectors.transpose(1, 2))
|
|
265
|
+
phase = torch.einsum('ijk,ik->ji', k[batch], positions)
|
|
266
|
+
scatter_index = batch.expand(self.wave_indices.shape[0], -1).T
|
|
267
|
+
if self.max_multipole == 'charge':
|
|
268
|
+
sum1 = torch.zeros((num_systems, self.wave_indices.shape[0]), dtype=torch.float32, device=positions.device)
|
|
269
|
+
sum2 = torch.zeros((num_systems, self.wave_indices.shape[0]), dtype=torch.float32, device=positions.device)
|
|
270
|
+
sum1.scatter_add_(0, scatter_index, (torch.cos(phase)*charges).T)
|
|
271
|
+
sum2.scatter_add_(0, scatter_index, (torch.sin(phase)*charges).T)
|
|
272
|
+
else:
|
|
273
|
+
kd = torch.einsum('ijk,ik->ji', k[batch], dipoles)
|
|
274
|
+
sum1 = torch.zeros((num_systems, self.wave_indices.shape[0]), dtype=torch.float32, device=positions.device)
|
|
275
|
+
sum2 = torch.zeros((num_systems, self.wave_indices.shape[0]), dtype=torch.float32, device=positions.device)
|
|
276
|
+
sum1.scatter_add_(0, scatter_index, (torch.cos(phase)*charges - torch.sin(phase)*kd).T)
|
|
277
|
+
sum2.scatter_add_(0, scatter_index, (torch.sin(phase)*charges + torch.cos(phase)*kd).T)
|
|
278
|
+
k2 = (k*k).sum(dim=2)
|
|
279
|
+
ak = torch.exp(self._exp_coeff*k2)/k2
|
|
280
|
+
energy = torch.sum(ak*(sum1**2 + sum2**2), dim=1)
|
|
281
|
+
return energy*4*torch.pi*torch.diagonal(recip_box_vectors, dim1=1, dim2=2).prod(dim=1)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
from . import coulomb
|
|
3
|
+
from .neighborlist import NeighborList
|
|
4
|
+
from .pairwise import Pairwise
|
|
5
|
+
from .utils import periodic_displacements
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CoulombNC(torch.nn.Module):
|
|
9
|
+
"""Compute Coulomb interactions with no cutoff.
|
|
10
|
+
|
|
11
|
+
This class computes the energy of a set of multipoles. Because it directly calculates all interactions regardless
|
|
12
|
+
of distance, it can only be used for non-periodic systems. It is primarily useful for small, isolated molecules.
|
|
13
|
+
|
|
14
|
+
By default, the multipoles are simply point charges. You can also include dipoles by passing
|
|
15
|
+
`max_multipole='dipole'` to the constructor. In that case, you must provide dipole moments along with charges when
|
|
16
|
+
invoking the module.
|
|
17
|
+
|
|
18
|
+
You can optionally specify that certain interactions should be omitted when computing the energy. This is typically
|
|
19
|
+
used for nearby atoms within the same molecule.
|
|
20
|
+
|
|
21
|
+
In addition to calculating energy and forces, this class can compute the electric field at arbitrary points in
|
|
22
|
+
space. To do this, call compute_field().
|
|
23
|
+
|
|
24
|
+
When you create an instance of this class, you must specify the value of Coulomb's constant 1/(4*pi*eps0). Its
|
|
25
|
+
value depends on the units used for energy and distance. The value you specify thus sets the unit system. See the
|
|
26
|
+
User Guide for the values in common unit systems.
|
|
27
|
+
"""
|
|
28
|
+
def __init__(self, exclusions: torch.Tensor, prefactor: float, max_multipole='charge', device: str = 'cpu'):
|
|
29
|
+
"""Create on object for computing Coulomb interactions.
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
exclusions: torch.Tensor
|
|
34
|
+
a tensor of shape (n_exclusions, 2). Each row contains the indices of two particles whose interaction
|
|
35
|
+
should be omitted.
|
|
36
|
+
prefactor: float
|
|
37
|
+
Coulomb's constant 1/(4*pi*eps0). This sets the unit system.
|
|
38
|
+
max_multipole: str
|
|
39
|
+
the maximum multipole order for each particle. Allowed options are `'charge'` (point charges only) and
|
|
40
|
+
`'dipole'` (charges and dipoles).
|
|
41
|
+
device: str
|
|
42
|
+
the PyTorch device to perform calculation on.
|
|
43
|
+
"""
|
|
44
|
+
if prefactor <= 0:
|
|
45
|
+
raise ValueError('prefactor must be positive')
|
|
46
|
+
super().__init__()
|
|
47
|
+
self.neighbor_list = NeighborList(device=device)
|
|
48
|
+
self.register_buffer('exclusions', exclusions)
|
|
49
|
+
self.prefactor = prefactor
|
|
50
|
+
self.max_multipole = max_multipole
|
|
51
|
+
if max_multipole == 'charge':
|
|
52
|
+
computation = coulomb.point_charge_interaction
|
|
53
|
+
elif max_multipole == 'dipole':
|
|
54
|
+
computation = coulomb.dipole_interaction
|
|
55
|
+
else:
|
|
56
|
+
raise ValueError(f'Illegal value for max_multipole: {max_multipole}')
|
|
57
|
+
self.pairwise = Pairwise(computation, None, exclusions)
|
|
58
|
+
|
|
59
|
+
def forward(self, positions: torch.Tensor, charges: torch.Tensor, dipoles: torch.Tensor | None = None,
|
|
60
|
+
batch: torch.Tensor | None = None) -> torch.Tensor:
|
|
61
|
+
"""Compute the interaction.
|
|
62
|
+
|
|
63
|
+
Parameters
|
|
64
|
+
----------
|
|
65
|
+
positions: torch.Tensor
|
|
66
|
+
a Tensor of shape (n_particles, 3) containing the Cartesian coordinates of each particle
|
|
67
|
+
charges: torch.Tensor
|
|
68
|
+
a Tensor of shape (n_particles,) containing the charge of each particle
|
|
69
|
+
dipoles: torch.Tensor | None
|
|
70
|
+
a Tensor of shape (n_particles, 3) containing the dipole moment of each particle. If max_multipole is
|
|
71
|
+
'charge', this is ignored.
|
|
72
|
+
batch: torch.Tensor | None
|
|
73
|
+
a Tensor of shape (n_particles,) containing the index of the system each particle belongs to. This must be
|
|
74
|
+
sorted in ascending order, and every system must contain at least one particle. If None, the interaction
|
|
75
|
+
is computed for a single system instead of a batch of systems.
|
|
76
|
+
|
|
77
|
+
Returns
|
|
78
|
+
-------
|
|
79
|
+
torch.Tensor:
|
|
80
|
+
a torch.Tensor containing the energy of the interaction. If batch is None, this is a scalar containing the
|
|
81
|
+
total energy. Otherwise, it has shape (n_systems,) containing the energy of each system in the batch.
|
|
82
|
+
"""
|
|
83
|
+
neighbors = self.neighbor_list(positions, None, batch)
|
|
84
|
+
if self.max_multipole == 'charge':
|
|
85
|
+
params = charges
|
|
86
|
+
else:
|
|
87
|
+
params = (charges, dipoles)
|
|
88
|
+
energy = self.pairwise(positions, params, neighbors, None, batch)
|
|
89
|
+
return self.prefactor*energy
|
|
90
|
+
|
|
91
|
+
def compute_field(self, field_positions: torch.Tensor, positions: torch.Tensor, charges: torch.Tensor,
|
|
92
|
+
dipoles: torch.Tensor | None = None) -> torch.Tensor:
|
|
93
|
+
"""Compute the electric field produced by the particles at a set of points.
|
|
94
|
+
|
|
95
|
+
Parameters
|
|
96
|
+
----------
|
|
97
|
+
field_positions: torch.Tensor
|
|
98
|
+
a Tensor of shape (n_points, 3) containing the positions at which to compute the field
|
|
99
|
+
positions: torch.Tensor
|
|
100
|
+
a Tensor of shape (n_particles, 3) containing the Cartesian coordinates of each particle
|
|
101
|
+
charges:
|
|
102
|
+
a Tensor of shape (n_particles,) containing the charge of each particle
|
|
103
|
+
dipoles: torch.Tensor | None
|
|
104
|
+
a Tensor of shape (n_particles, 3) containing the dipole moment of each particle. If max_multipole is
|
|
105
|
+
'charge', this is ignored.
|
|
106
|
+
|
|
107
|
+
Returns
|
|
108
|
+
-------
|
|
109
|
+
torch.Tensor:
|
|
110
|
+
a Tensor of shape (n_points, 3) containing the electric field at each of the points
|
|
111
|
+
"""
|
|
112
|
+
delta = periodic_displacements(field_positions.view((-1,1,3))-positions, None)
|
|
113
|
+
r = torch.linalg.vector_norm(delta, dim=2, keepdim=True)
|
|
114
|
+
denom3 = r**-3
|
|
115
|
+
field = charges.unsqueeze(1)*delta*denom3
|
|
116
|
+
if self.max_multipole != 'charge':
|
|
117
|
+
field += (3*(dipoles.unsqueeze(0)*delta).sum(axis=2, keepdim=True)*delta*r**-2 - dipoles)*denom3
|
|
118
|
+
field = torch.where((r > 0), field, 0)
|
|
119
|
+
field = field.sum(dim=1)
|
|
120
|
+
return self.prefactor*field
|