htpolynet 2.0.0__py3-none-any.whl
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.
- htpolynet/__init__.py +2 -0
- htpolynet/analysis/__init__.py +0 -0
- htpolynet/analysis/analyze.py +256 -0
- htpolynet/analysis/plot.py +588 -0
- htpolynet/analysis/postsim.py +444 -0
- htpolynet/analysis/utils.py +399 -0
- htpolynet/cli.py +488 -0
- htpolynet/core/__init__.py +0 -0
- htpolynet/core/bondtemplate.py +112 -0
- htpolynet/core/configuration.py +83 -0
- htpolynet/core/coordinates.py +644 -0
- htpolynet/core/molecule.py +998 -0
- htpolynet/core/projectfilesystem.py +616 -0
- htpolynet/core/runtime.py +774 -0
- htpolynet/core/topocoord.py +2044 -0
- htpolynet/core/topology.py +1138 -0
- htpolynet/cure/__init__.py +0 -0
- htpolynet/cure/chain.py +135 -0
- htpolynet/cure/curecontroller.py +746 -0
- htpolynet/cure/expandreactions.py +294 -0
- htpolynet/cure/reaction.py +280 -0
- htpolynet/external/__init__.py +0 -0
- htpolynet/external/ambertools.py +97 -0
- htpolynet/external/command.py +73 -0
- htpolynet/external/gromacs.py +308 -0
- htpolynet/external/slurm.py +111 -0
- htpolynet/external/software.py +183 -0
- htpolynet/geometry/__init__.py +0 -0
- htpolynet/geometry/bondlist.py +205 -0
- htpolynet/geometry/lattice.py +32 -0
- htpolynet/geometry/linkcell.py +210 -0
- htpolynet/geometry/matrix4.py +110 -0
- htpolynet/geometry/ring.py +274 -0
- htpolynet/io/__init__.py +6 -0
- htpolynet/io/gro.py +195 -0
- htpolynet/io/mol2.py +210 -0
- htpolynet/io/pdb.py +157 -0
- htpolynet/resources/README.md +20 -0
- htpolynet/resources/__init__.py +0 -0
- htpolynet/resources/cfg/DGE-PAC-hi.yaml +115 -0
- htpolynet/resources/cfg/DGE-PAC-lo.yaml +115 -0
- htpolynet/resources/cfg/FDE-DFDA-hi.yaml +116 -0
- htpolynet/resources/cfg/FDE-DFDA-lo.yaml +116 -0
- htpolynet/resources/cfg/GMASTY-hi.yaml +158 -0
- htpolynet/resources/cfg/GMASTY-lo.yaml +158 -0
- htpolynet/resources/cfg/README.md +1 -0
- htpolynet/resources/cfg/STY.yaml +23 -0
- htpolynet/resources/cfg/pMSTY-hi.yaml +101 -0
- htpolynet/resources/cfg/pMSTY-lo.yaml +101 -0
- htpolynet/resources/cfg/pSTY-hi.yaml +101 -0
- htpolynet/resources/cfg/pSTY-lo.yaml +101 -0
- htpolynet/resources/example_depot/0-liquid-styrene.sh +134 -0
- htpolynet/resources/example_depot/0-liquid-styrene.tgz +0 -0
- htpolynet/resources/example_depot/1-polystyrene.sh +202 -0
- htpolynet/resources/example_depot/1-polystyrene.tgz +0 -0
- htpolynet/resources/example_depot/2-bisgma-styrene-thermoset.sh +353 -0
- htpolynet/resources/example_depot/2-bisgma-styrene-thermoset.tgz +0 -0
- htpolynet/resources/example_depot/3-pacm-dgeba-epoxy-thermoset.sh +242 -0
- htpolynet/resources/example_depot/3-pacm-dgeba-epoxy-thermoset.tgz +0 -0
- htpolynet/resources/example_depot/4-dfda-fde-epoxy-thermoset.sh +216 -0
- htpolynet/resources/example_depot/4-dfda-fde-epoxy-thermoset.tgz +0 -0
- htpolynet/resources/mdp/README.md +1 -0
- htpolynet/resources/mdp/drag-min.mdp +13 -0
- htpolynet/resources/mdp/drag-npt.mdp +36 -0
- htpolynet/resources/mdp/drag-nvt.mdp +31 -0
- htpolynet/resources/mdp/min.mdp +14 -0
- htpolynet/resources/mdp/npt.mdp +37 -0
- htpolynet/resources/mdp/nvt.mdp +29 -0
- htpolynet/resources/mdp/relax-min.mdp +13 -0
- htpolynet/resources/mdp/relax-npt.mdp +37 -0
- htpolynet/resources/mdp/relax-nvt.mdp +31 -0
- htpolynet/resources/mdp/single-molecule-min.mdp +12 -0
- htpolynet/resources/mdp/single-molecule-nvt.mdp +25 -0
- htpolynet/resources/molecules/inputs/DFA.pdb +62 -0
- htpolynet/resources/molecules/inputs/DGE.mol2 +115 -0
- htpolynet/resources/molecules/inputs/EMB.mol2 +50 -0
- htpolynet/resources/molecules/inputs/FDE.pdb +72 -0
- htpolynet/resources/molecules/inputs/GMA.mol2 +163 -0
- htpolynet/resources/molecules/inputs/PAC.mol2 +91 -0
- htpolynet/resources/molecules/inputs/STY.mol2 +44 -0
- htpolynet/resources/molecules/make-monomers.sh +64 -0
- htpolynet/resources/molecules/pics/DFA.png +0 -0
- htpolynet/resources/molecules/pics/DGE.png +0 -0
- htpolynet/resources/molecules/pics/EMB.png +0 -0
- htpolynet/resources/molecules/pics/FDE.png +0 -0
- htpolynet/resources/molecules/pics/GMA.png +0 -0
- htpolynet/resources/molecules/pics/PAC.png +0 -0
- htpolynet/resources/molecules/pics/STY.png +0 -0
- htpolynet/resources/molecules/sample-inputs/DFA.pdb +62 -0
- htpolynet/resources/molecules/sample-inputs/DGE.mol2 +115 -0
- htpolynet/resources/molecules/sample-inputs/EMB.mol2 +50 -0
- htpolynet/resources/molecules/sample-inputs/FDE.pdb +72 -0
- htpolynet/resources/molecules/sample-inputs/GMA.mol2 +163 -0
- htpolynet/resources/molecules/sample-inputs/PAC.mol2 +91 -0
- htpolynet/resources/molecules/sample-inputs/STY.mol2 +44 -0
- htpolynet/resources/tcl/readbonds.tcl +32 -0
- htpolynet/resources/tcl/readgrx.tcl +46 -0
- htpolynet/resources/tcl/render.tcl +74 -0
- htpolynet/utils/__init__.py +0 -0
- htpolynet/utils/banner.py +34 -0
- htpolynet/utils/checkpoint.py +85 -0
- htpolynet/utils/dataframetools.py +92 -0
- htpolynet/utils/inputcheck.py +49 -0
- htpolynet/utils/logsetup.py +44 -0
- htpolynet/utils/stringthings.py +39 -0
- htpolynet-2.0.0.dist-info/METADATA +78 -0
- htpolynet-2.0.0.dist-info/RECORD +110 -0
- htpolynet-2.0.0.dist-info/WHEEL +4 -0
- htpolynet-2.0.0.dist-info/entry_points.txt +2 -0
- htpolynet-2.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
"""Class for managing atom coordinate data.
|
|
2
|
+
|
|
3
|
+
Author: Cameron F. Abrams <cfa22@drexel.edu>
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import pandas as pd
|
|
11
|
+
|
|
12
|
+
from ..geometry.bondlist import Bondlist
|
|
13
|
+
from ..geometry.linkcell import Linkcell
|
|
14
|
+
from ..geometry.matrix4 import Matrix4
|
|
15
|
+
from ..geometry.ring import Ring, Segment
|
|
16
|
+
from ..io import gro as _gro_io
|
|
17
|
+
from ..io import mol2 as _mol2_io
|
|
18
|
+
from ..io import pdb as _pdb_io
|
|
19
|
+
from ..io.gro import GRX_ATTRIBUTES, GRX_GLOBALLY_UNIQUE, GRX_UNSET_DEFAULTS
|
|
20
|
+
from ..utils.dataframetools import get_rows_w_attribute, set_row_attribute, set_rows_attributes_from_dict
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Coordinates:
|
|
26
|
+
""" Handles atom coordinates.
|
|
27
|
+
|
|
28
|
+
The primary object is `A`, a pandas DataFrame with one row per atom. Each atom has attributes that may be found in a `gro` file and/or a `mol2` file, along with so-called extended attributes, which are used solely by HTPolyNet.
|
|
29
|
+
|
|
30
|
+
"""
|
|
31
|
+
gro_attributes = _gro_io.GRO_ATTRIBUTES
|
|
32
|
+
mol2_atom_attributes = _mol2_io.MOL2_ATOM_ATTRIBUTES
|
|
33
|
+
mol2_bond_attributes = _mol2_io.MOL2_BOND_ATTRIBUTES
|
|
34
|
+
mol2_bond_types = _mol2_io.MOL2_BOND_TYPES
|
|
35
|
+
|
|
36
|
+
def __init__(self, name=''):
|
|
37
|
+
"""Constructs an empty Coordinates object.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
name (str): a name string, defaults to ''
|
|
41
|
+
"""
|
|
42
|
+
self.name = name
|
|
43
|
+
self.metadat = {}
|
|
44
|
+
self.N = 0
|
|
45
|
+
self.A = pd.DataFrame()
|
|
46
|
+
self.mol2_bonds = pd.DataFrame()
|
|
47
|
+
self.mol2_bondlist = Bondlist()
|
|
48
|
+
self.linkcell = Linkcell(pbc_wrapper=self.wrap_point)
|
|
49
|
+
self.empty = True
|
|
50
|
+
self.box = np.zeros((3,3))
|
|
51
|
+
self.grx_attributes = GRX_ATTRIBUTES
|
|
52
|
+
self.parent = None
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def read_gro(cls, filename, wrap_coords=True):
|
|
56
|
+
"""Reads a Gromacs gro file.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
filename (str): name of gro file
|
|
60
|
+
wrap_coords (bool): if True, wrap atom coordinates into the box, defaults to True
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Coordinates: a new Coordinates instance
|
|
64
|
+
"""
|
|
65
|
+
inst = cls(filename)
|
|
66
|
+
if filename != '':
|
|
67
|
+
data = _gro_io.read(filename)
|
|
68
|
+
inst.name = data['name']
|
|
69
|
+
inst.N = data['N']
|
|
70
|
+
inst.metadat = data['metadat']
|
|
71
|
+
inst.A = data['A']
|
|
72
|
+
inst.box = data['box']
|
|
73
|
+
inst.empty = False
|
|
74
|
+
if wrap_coords:
|
|
75
|
+
inst.wrap_coords()
|
|
76
|
+
return inst
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def read_mol2(cls, filename):
|
|
80
|
+
"""Reads in a Sybyl MOL2 file into a Coordinates instance.
|
|
81
|
+
|
|
82
|
+
Note that this method only reads in MOLECULE, ATOM, and BOND sections.
|
|
83
|
+
All lengths are converted from Ångström to nm.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
filename (str): name of input mol2 file
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Coordinates: a new Coordinates instance
|
|
90
|
+
"""
|
|
91
|
+
inst = cls(name=filename)
|
|
92
|
+
data = _mol2_io.read(filename)
|
|
93
|
+
inst.name = data['name']
|
|
94
|
+
inst.N = data['N']
|
|
95
|
+
inst.metadat = data['metadat']
|
|
96
|
+
inst.A = data['A']
|
|
97
|
+
inst.mol2_bonds = data['mol2_bonds']
|
|
98
|
+
inst.mol2_bondlist = data['mol2_bondlist']
|
|
99
|
+
inst.empty = False
|
|
100
|
+
return inst
|
|
101
|
+
|
|
102
|
+
@classmethod
|
|
103
|
+
def read_pdb(cls, filename):
|
|
104
|
+
"""Reads a PDB file into a Coordinates instance.
|
|
105
|
+
|
|
106
|
+
CONECT records are parsed to build a bond table. Repeated entries for
|
|
107
|
+
the same atom pair within one CONECT line are interpreted as higher-order
|
|
108
|
+
bonds (e.g. two appearances → double bond, order '2'). Positions are
|
|
109
|
+
converted from Ångström to nm.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
filename (str): path to the PDB file
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Coordinates: a new Coordinates instance with mol2_bonds populated
|
|
116
|
+
"""
|
|
117
|
+
inst = cls(name=filename)
|
|
118
|
+
data = _pdb_io.read(filename)
|
|
119
|
+
inst.name = data['name']
|
|
120
|
+
inst.N = data['N']
|
|
121
|
+
inst.metadat = data['metadat']
|
|
122
|
+
inst.A = data['A']
|
|
123
|
+
inst.mol2_bonds = data['mol2_bonds']
|
|
124
|
+
inst.mol2_bondlist = data['mol2_bondlist']
|
|
125
|
+
inst.empty = False
|
|
126
|
+
return inst
|
|
127
|
+
|
|
128
|
+
@classmethod
|
|
129
|
+
def fcc(cls, a: float, nc=[1,1,1]):
|
|
130
|
+
"""Generates a Coordinates object that represents an FCC crystal.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
a (float): lattice parameter
|
|
134
|
+
nc (list): number of unit cells in the three lattice vector directions, defaults to [1,1,1]
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Coordinates: a Coordinates object
|
|
138
|
+
"""
|
|
139
|
+
from htpolynet.geometry.lattice import fcc as _fcc
|
|
140
|
+
posn = _fcc(a,nc)
|
|
141
|
+
N = len(posn)
|
|
142
|
+
inst = cls()
|
|
143
|
+
inst.A['globalIdx'] = list(range(1,N+1))
|
|
144
|
+
inst.A['atomName'] = 'AL'
|
|
145
|
+
inst.A['resNum'] = list(range(1,N+1))
|
|
146
|
+
inst.A['posX'] = posn[:,0]
|
|
147
|
+
inst.A['posY'] = posn[:,1]
|
|
148
|
+
inst.A['posZ'] = posn[:,2]
|
|
149
|
+
inst.A['resName'] = 'MET'
|
|
150
|
+
inst.N = N
|
|
151
|
+
return inst
|
|
152
|
+
|
|
153
|
+
def claim_parent(self, parent):
|
|
154
|
+
self.parent = parent
|
|
155
|
+
|
|
156
|
+
def set_box(self, box: np.ndarray):
|
|
157
|
+
"""Sets the box size from box.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
box (numpy.ndarray): 3-by-1 or 3-by-3 box size matrix
|
|
161
|
+
"""
|
|
162
|
+
if box.shape == (3, 1):
|
|
163
|
+
for i in range(3):
|
|
164
|
+
self.box[i, i] = box[i]
|
|
165
|
+
elif box.shape == (3, 3):
|
|
166
|
+
self.box = np.copy(box)
|
|
167
|
+
|
|
168
|
+
def total_volume(self, units='gromacs'):
|
|
169
|
+
"""Returns total volume of box.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
units (str): unit system designation; if 'SI' returns m^3, defaults to 'gromacs'
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
float: volume (in nm^3 if units is 'gromacs' or m^3 if units is 'SI')
|
|
176
|
+
"""
|
|
177
|
+
nm_per_m = 1.e9
|
|
178
|
+
vol = np.prod(self.box.diagonal()) # nm^3
|
|
179
|
+
return vol if units != 'SI' else vol / (nm_per_m ** 3)
|
|
180
|
+
|
|
181
|
+
def copy_coords(self, other):
|
|
182
|
+
"""Copies the posX, posY, and posZ atom attributes, and the box size, from other.A to self.A.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
other (Coordinates): the other Coordinates instance
|
|
186
|
+
"""
|
|
187
|
+
assert self.A.shape[0] == other.A.shape[0], f'Cannot copy -- atom count mismatch {self.A.shape[0]} vs {other.A.shape[0]}'
|
|
188
|
+
for c in ['posX', 'posY', 'posZ']:
|
|
189
|
+
otherpos = other.A[c].copy()
|
|
190
|
+
self.A[c] = otherpos
|
|
191
|
+
self.box = np.copy(other.box)
|
|
192
|
+
|
|
193
|
+
def subcoords(self, sub_adf: pd.DataFrame):
|
|
194
|
+
"""Generates a new Coordinates object to hold the atoms dataframe in 'sub_adf' parameter.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
sub_adf (pd.DataFrame): an atom dataframe
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Coordinates: a new Coordinates object
|
|
201
|
+
"""
|
|
202
|
+
newC = Coordinates()
|
|
203
|
+
newC.set_box(self.box)
|
|
204
|
+
newC.A = sub_adf
|
|
205
|
+
newC.N = sub_adf.shape[0]
|
|
206
|
+
return newC
|
|
207
|
+
|
|
208
|
+
def unwrap(self, P, O, pbc):
|
|
209
|
+
"""Shifts point P to its unwrapped closest periodic image to point O.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
P (np.ndarray(3,float)): a point
|
|
213
|
+
O (np.ndarray(3,float)): origin
|
|
214
|
+
pbc (np.ndarray(3,int)): directions in which pbc are applied
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
np.ndarray(3,float): a point
|
|
218
|
+
"""
|
|
219
|
+
ROP = self.mic(O - P, pbc)
|
|
220
|
+
PCPI = O - ROP
|
|
221
|
+
return PCPI
|
|
222
|
+
|
|
223
|
+
def pierces(self, B: pd.DataFrame, C: pd.DataFrame, pbc):
|
|
224
|
+
"""Determines whether or not bond represented by two points in B pierces ring represented by N points in C.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
B (pd.DataFrame): two points defining a bond
|
|
228
|
+
C (pd.DataFrame): N points defining a ring
|
|
229
|
+
pbc (list): periodic boundary condition flags, one per dimension
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
bool: True if ring is pierced
|
|
233
|
+
"""
|
|
234
|
+
BC = np.array(B[['posX', 'posY', 'posZ']])
|
|
235
|
+
CC = np.array(C[['posX', 'posY', 'posZ']])
|
|
236
|
+
# get both atoms in bond into CPI
|
|
237
|
+
# get all atoms in ring into CPI (but not necessarily wrt bond)
|
|
238
|
+
CC[1:] = np.array([self.unwrap(c, CC[0], pbc) for c in CC[1:]])
|
|
239
|
+
BC[0] = self.unwrap(BC[0], CC[0], pbc)
|
|
240
|
+
BC[1] = self.unwrap(BC[1], BC[0], pbc)
|
|
241
|
+
B[['posX', 'posY', 'posZ']] = BC
|
|
242
|
+
C[['posX', 'posY', 'posZ']] = CC
|
|
243
|
+
S = Segment(BC)
|
|
244
|
+
R = Ring(CC)
|
|
245
|
+
R.analyze()
|
|
246
|
+
do_it, point = R.segint(S)
|
|
247
|
+
return do_it
|
|
248
|
+
|
|
249
|
+
def linkcell_initialize(self, cutoff=0.0, populate=True, force_repopulate=False, save=True):
|
|
250
|
+
"""Initializes the link-cell structure for ring-pierce testing.
|
|
251
|
+
|
|
252
|
+
Assigns a ``linkcell_idx`` attribute to each ring atom and reactive
|
|
253
|
+
atom (z > 0) directly in ``self.A``; all other atoms get -1.
|
|
254
|
+
Results are cached to a ``.grx`` file keyed by cutoff value.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
cutoff (float): cell side length (nm), defaults to 0.0
|
|
258
|
+
populate (bool): if True, perform the assignment; defaults to True
|
|
259
|
+
force_repopulate (bool): if True, ignore any cached file; defaults to False
|
|
260
|
+
save (bool): if True, write the linkcell_idx column to a cache file; defaults to True
|
|
261
|
+
"""
|
|
262
|
+
logger.debug('Initializing link-cell structure')
|
|
263
|
+
self.linkcell.create(cutoff, self.box)
|
|
264
|
+
if populate:
|
|
265
|
+
lc_file = f'linkcell-{cutoff:.2f}.grx'
|
|
266
|
+
if os.path.exists(lc_file) and not force_repopulate:
|
|
267
|
+
logger.debug(f'Found {lc_file}; reading cached linkcell_idx.')
|
|
268
|
+
self.read_atomset_attributes(lc_file)
|
|
269
|
+
else:
|
|
270
|
+
mask = (
|
|
271
|
+
self.A['globalIdx'].isin(self.parent.Topology.rings.all_atoms())
|
|
272
|
+
| (self.A['z'] > 0)
|
|
273
|
+
)
|
|
274
|
+
self.linkcell.assign(self.A, mask=mask)
|
|
275
|
+
if save:
|
|
276
|
+
self.write_atomset_attributes(['linkcell_idx'], lc_file)
|
|
277
|
+
|
|
278
|
+
def geometric_center(self):
|
|
279
|
+
"""Computes and returns the geometric center of the atoms in self's A dataframe.
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
np.ndarray(3,float): geometric center
|
|
283
|
+
"""
|
|
284
|
+
a = self.A
|
|
285
|
+
return np.array([a.posX.mean(), a.posY.mean(), a.posZ.mean()])
|
|
286
|
+
|
|
287
|
+
def rij(self, i, j, pbc=None):
|
|
288
|
+
"""Computes distance between atoms i and j.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
i (int): globalIdx of first atom
|
|
292
|
+
j (int): globalIdx of second atom
|
|
293
|
+
pbc (array-like, optional): PBC flags per axis, defaults to [1,1,1]
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
float: distance between i and j (nm)
|
|
297
|
+
"""
|
|
298
|
+
if pbc is None:
|
|
299
|
+
pbc = [1, 1, 1]
|
|
300
|
+
if np.any(pbc) and np.allclose(self.box, 0):
|
|
301
|
+
logger.warning('Interatomic distance calculation using PBC with no boxsize set.')
|
|
302
|
+
Rij = self.mic(self.get_R(i) - self.get_R(j), pbc)
|
|
303
|
+
return np.sqrt(Rij.dot(Rij))
|
|
304
|
+
|
|
305
|
+
def mic(self, r, pbc=None):
|
|
306
|
+
"""Applies minimum image convention to displacement vector r.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
r (np.ndarray(3,float)): displacement vector
|
|
310
|
+
pbc (array-like, optional): PBC flags per axis, defaults to [1,1,1]
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
np.ndarray(3,float): minimum-image displacement vector
|
|
314
|
+
"""
|
|
315
|
+
if pbc is None:
|
|
316
|
+
pbc = [1, 1, 1]
|
|
317
|
+
b = self.box.diagonal()
|
|
318
|
+
mask = np.asarray(pbc, dtype=bool)
|
|
319
|
+
r = r.copy()
|
|
320
|
+
r[mask] -= np.round(r[mask] / b[mask]) * b[mask]
|
|
321
|
+
return r
|
|
322
|
+
|
|
323
|
+
def wrap_point(self, ri):
|
|
324
|
+
"""Wraps point ri into the central periodic image.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
ri (np.ndarray(3,float)): a point
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
tuple: the wrapped point and number of box lengths shifted per dimension
|
|
331
|
+
"""
|
|
332
|
+
b = self.box.diagonal()
|
|
333
|
+
R = ri % b
|
|
334
|
+
box_lengths = (-np.floor(ri / b)).astype(int)
|
|
335
|
+
return R, box_lengths
|
|
336
|
+
|
|
337
|
+
def wrap_coords(self):
|
|
338
|
+
"""Wraps all atomic coordinates into box."""
|
|
339
|
+
assert np.any(self.box), f'Cannot wrap if boxsize is not set: {self.box}'
|
|
340
|
+
b = self.box.diagonal()
|
|
341
|
+
pos = self.A[['posX','posY','posZ']].values
|
|
342
|
+
box_lengths = (-np.floor(pos / b)).astype(int)
|
|
343
|
+
self.A[['posX','posY','posZ']] = pos % b
|
|
344
|
+
self.A[['boxLx','boxLy','boxLz']] = box_lengths
|
|
345
|
+
|
|
346
|
+
def merge(self, other):
|
|
347
|
+
"""Merges two Coordinates objects.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
other (Coordinates): the other Coordinates object
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
tuple: integer shifts in atom index, bond index, and residue index
|
|
354
|
+
"""
|
|
355
|
+
idxshift = self.A.shape[0]
|
|
356
|
+
bdxshift = self.mol2_bonds.shape[0]
|
|
357
|
+
rdxshift = 0 if self.A.empty else int(self.A['resNum'].max())
|
|
358
|
+
nOtherBonds = 0
|
|
359
|
+
if not other.A.empty:
|
|
360
|
+
oa = other.A.copy()
|
|
361
|
+
oa['globalIdx'] += idxshift
|
|
362
|
+
oa['resNum'] += rdxshift
|
|
363
|
+
self.A = pd.concat((self.A, oa), ignore_index=True)
|
|
364
|
+
self.N = self.A.shape[0]
|
|
365
|
+
if not other.mol2_bonds.empty:
|
|
366
|
+
ob = other.mol2_bonds.copy()
|
|
367
|
+
nOtherBonds = ob.shape[0]
|
|
368
|
+
ob['bondIdx'] += bdxshift
|
|
369
|
+
ob[['ai', 'aj']] += idxshift
|
|
370
|
+
self.mol2_bonds = pd.concat((self.mol2_bonds, ob), ignore_index=True)
|
|
371
|
+
self.mol2_bondlist.update(ob)
|
|
372
|
+
self.metadat['N'] = self.N
|
|
373
|
+
self.metadat['nBonds'] = self.metadat.get('nBonds', 0) + nOtherBonds
|
|
374
|
+
return (idxshift, bdxshift, rdxshift)
|
|
375
|
+
|
|
376
|
+
def write_atomset_attributes(self, attributes, filename, formatters=[]):
|
|
377
|
+
"""Writes atom attributes to a GRX-format file.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
attributes (list): list of attribute names to write
|
|
381
|
+
filename (str): name of file to write
|
|
382
|
+
formatters (list): formatting methods per attribute, defaults to []
|
|
383
|
+
|
|
384
|
+
Raises:
|
|
385
|
+
Exception: if any item in attributes does not exist in the coordinates dataframe
|
|
386
|
+
"""
|
|
387
|
+
_gro_io.write_grx(filename, self.A, attributes, formatters=formatters)
|
|
388
|
+
|
|
389
|
+
def read_atomset_attributes(self, filename, attributes=[]):
|
|
390
|
+
"""Reads atomic attributes from a GRX-format file into self.A.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
filename (str): name of file
|
|
394
|
+
attributes (list): list of attributes to take, defaults to [] (take all)
|
|
395
|
+
|
|
396
|
+
Returns:
|
|
397
|
+
list: names of attributes that were read
|
|
398
|
+
"""
|
|
399
|
+
df, attributes_read = _gro_io.read_grx(filename, attributes)
|
|
400
|
+
self.A = self.A.merge(df, how='outer', on='globalIdx')
|
|
401
|
+
return attributes_read
|
|
402
|
+
|
|
403
|
+
def set_atomset_attribute(self, attribute, srs):
|
|
404
|
+
"""Sets attribute of atoms to srs.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
attribute (str): name of attribute
|
|
408
|
+
srs (scalar or list-like): attribute values in same ordering as self.A
|
|
409
|
+
"""
|
|
410
|
+
self.A[attribute] = srs
|
|
411
|
+
|
|
412
|
+
def atomcount(self):
|
|
413
|
+
"""Returns the number of atoms in the Coordinates object.
|
|
414
|
+
|
|
415
|
+
Returns:
|
|
416
|
+
int: number of atoms
|
|
417
|
+
"""
|
|
418
|
+
return self.N
|
|
419
|
+
|
|
420
|
+
def show_z_report(self):
|
|
421
|
+
"""Generates a text-based histogram of atom z-values (0-3), keyed by resname:atomname."""
|
|
422
|
+
zhists = {}
|
|
423
|
+
for r in self.A.itertuples():
|
|
424
|
+
n = r.atomName
|
|
425
|
+
nn = r.resName
|
|
426
|
+
k = f'{nn}:{n}'
|
|
427
|
+
z = r.z
|
|
428
|
+
if not k in zhists:
|
|
429
|
+
zhists[k] = np.zeros(4).astype(int)
|
|
430
|
+
zhists[k][z] += 1
|
|
431
|
+
for n in zhists:
|
|
432
|
+
if any([zhists[n][i] > 0 for i in range(1, 4)]):
|
|
433
|
+
logger.debug(f'Z-hist for {n} atoms:')
|
|
434
|
+
for i in range(4):
|
|
435
|
+
logger.debug(f'{i:>5d} ({zhists[n][i]:>6d}): ' + '*' * (zhists[n][i] // 10))
|
|
436
|
+
|
|
437
|
+
def return_bond_lengths(self, bdf):
|
|
438
|
+
"""Returns an ordered list of bond lengths for bonds in bdf.
|
|
439
|
+
|
|
440
|
+
Args:
|
|
441
|
+
bdf (pd.DataFrame): dataframe with 'ai' and 'aj' columns of atom indices indicating bonds
|
|
442
|
+
|
|
443
|
+
Returns:
|
|
444
|
+
list: atom distances
|
|
445
|
+
"""
|
|
446
|
+
lengths = []
|
|
447
|
+
for b in bdf.itertuples():
|
|
448
|
+
lengths.append(self.rij(b.ai, b.aj))
|
|
449
|
+
return lengths
|
|
450
|
+
|
|
451
|
+
def minimum_distance(self, other, self_excludes=None, other_excludes=None):
|
|
452
|
+
"""Computes and returns distance of closest approach between two sets of atoms.
|
|
453
|
+
|
|
454
|
+
Args:
|
|
455
|
+
other (Coordinates): other Coordinates instance
|
|
456
|
+
self_excludes (list, optional): atom globalIdx values in self to exclude, defaults to None
|
|
457
|
+
other_excludes (list, optional): atom globalIdx values in other to exclude, defaults to None
|
|
458
|
+
|
|
459
|
+
Returns:
|
|
460
|
+
float: minimum interatomic distance (nm)
|
|
461
|
+
"""
|
|
462
|
+
sp = self.A.loc[~self.A['globalIdx'].isin(self_excludes or []), ['posX','posY','posZ']].values
|
|
463
|
+
op = other.A.loc[~other.A['globalIdx'].isin(other_excludes or []), ['posX','posY','posZ']].values
|
|
464
|
+
diff = sp[:, np.newaxis, :] - op[np.newaxis, :, :]
|
|
465
|
+
return np.sqrt((diff * diff).sum(axis=2)).min()
|
|
466
|
+
|
|
467
|
+
def homog_trans(self, M: Matrix4, indices=None):
|
|
468
|
+
"""Applies homogeneous transformation matrix M to coordinates.
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
M (Matrix4): homogeneous transformation matrix
|
|
472
|
+
indices (list, optional): atom globalIdx values to transform; if None transforms all
|
|
473
|
+
"""
|
|
474
|
+
R = M.m[:3, :3]
|
|
475
|
+
t = M.m[:3, 3]
|
|
476
|
+
if not indices:
|
|
477
|
+
pos = self.A[['posX','posY','posZ']].values
|
|
478
|
+
self.A[['posX','posY','posZ']] = pos @ R.T + t
|
|
479
|
+
else:
|
|
480
|
+
mask = self.A['globalIdx'].isin(indices)
|
|
481
|
+
pos = self.A.loc[mask, ['posX','posY','posZ']].values
|
|
482
|
+
self.A.loc[mask, ['posX','posY','posZ']] = pos @ R.T + t
|
|
483
|
+
|
|
484
|
+
def rotate(self, R):
|
|
485
|
+
"""Rotates all coordinate vectors by rotation matrix R.
|
|
486
|
+
|
|
487
|
+
Args:
|
|
488
|
+
R (numpy.ndarray): rotation matrix (3x3)
|
|
489
|
+
"""
|
|
490
|
+
pos = self.A[['posX','posY','posZ']].values
|
|
491
|
+
self.A[['posX','posY','posZ']] = pos @ R.T
|
|
492
|
+
|
|
493
|
+
def translate(self, L):
|
|
494
|
+
"""Translates all coordinate vectors by displacement vector L.
|
|
495
|
+
|
|
496
|
+
Args:
|
|
497
|
+
L (numpy.ndarray): displacement vector (nm)
|
|
498
|
+
"""
|
|
499
|
+
pos = self.A[['posX','posY','posZ']].values
|
|
500
|
+
self.A[['posX','posY','posZ']] = pos + L
|
|
501
|
+
|
|
502
|
+
def maxspan(self):
|
|
503
|
+
"""Returns dimensions of orthorhombic convex hull enclosing Coordinates.
|
|
504
|
+
|
|
505
|
+
Returns:
|
|
506
|
+
numpy.ndarray: array of x-span, y-span, z-span
|
|
507
|
+
"""
|
|
508
|
+
sp = self.A[['posX', 'posY', 'posZ']]
|
|
509
|
+
return np.array(
|
|
510
|
+
[
|
|
511
|
+
sp.posX.max() - sp.posX.min(),
|
|
512
|
+
sp.posY.max() - sp.posY.min(),
|
|
513
|
+
sp.posZ.max() - sp.posZ.min()
|
|
514
|
+
]
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
def minmax(self):
|
|
518
|
+
"""Returns the lower-leftmost and upper-rightmost positions in the atoms dataframe.
|
|
519
|
+
|
|
520
|
+
Returns:
|
|
521
|
+
tuple(np.ndarray, np.ndarray): lower-leftmost and upper-rightmost points, respectively
|
|
522
|
+
"""
|
|
523
|
+
sp = self.A[['posX', 'posY', 'posZ']]
|
|
524
|
+
return np.array([sp.posX.min(), sp.posY.min(), sp.posZ.min()]), np.array([sp.posX.max(), sp.posY.max(), sp.posZ.max()])
|
|
525
|
+
|
|
526
|
+
def checkbox(self):
|
|
527
|
+
"""Checks that all atom positions fit within the designated box.
|
|
528
|
+
|
|
529
|
+
Returns:
|
|
530
|
+
tuple(bool, bool): True,True if both lower-leftmost and upper-rightmost points are within the box
|
|
531
|
+
"""
|
|
532
|
+
mm, MM = self.minmax()
|
|
533
|
+
bb = self.box.diagonal()
|
|
534
|
+
return mm < bb, MM > bb
|
|
535
|
+
|
|
536
|
+
def get_R(self, idx: int) -> np.ndarray:
|
|
537
|
+
"""Returns the cartesian position of atom with global index idx.
|
|
538
|
+
|
|
539
|
+
Args:
|
|
540
|
+
idx (int): global index of atom
|
|
541
|
+
|
|
542
|
+
Returns:
|
|
543
|
+
numpy.ndarray: cartesian position of the atom, shape (3,)
|
|
544
|
+
"""
|
|
545
|
+
return self.A.loc[self.A['globalIdx'] == idx, ['posX', 'posY', 'posZ']].values[0]
|
|
546
|
+
|
|
547
|
+
def get_atom_attribute(self, name, attributes):
|
|
548
|
+
"""Returns values of attributes listed in name from atoms specified by attribute:value pairs.
|
|
549
|
+
|
|
550
|
+
Args:
|
|
551
|
+
name (str or list): attribute(s) whose values are to be returned
|
|
552
|
+
attributes (dict): attribute:value pairs that specify the set of atoms to be considered
|
|
553
|
+
|
|
554
|
+
Returns:
|
|
555
|
+
scalar or Series: attribute value(s) of the matching row
|
|
556
|
+
"""
|
|
557
|
+
mask = pd.Series(True, index=self.A.index)
|
|
558
|
+
for k, v in attributes.items():
|
|
559
|
+
mask &= (self.A[k] == v)
|
|
560
|
+
return self.A.loc[mask, name].iloc[0]
|
|
561
|
+
|
|
562
|
+
def get_atoms_w_attribute(self,name,attributes):
|
|
563
|
+
"""Returns rows of atoms dataframe with columns named in name for atoms matching attributes.
|
|
564
|
+
|
|
565
|
+
Args:
|
|
566
|
+
name (list): attribute names for the columns to be returned
|
|
567
|
+
attributes (dict): attribute:value pairs that specify the set of atoms to be considered
|
|
568
|
+
|
|
569
|
+
Returns:
|
|
570
|
+
pd.DataFrame: matching dataframe segment
|
|
571
|
+
"""
|
|
572
|
+
df = self.A
|
|
573
|
+
return get_rows_w_attribute(df, name, attributes)
|
|
574
|
+
|
|
575
|
+
def set_atom_attribute(self, name, value, attributes):
|
|
576
|
+
"""Sets the attributes named in name to the given values for atoms matching attributes.
|
|
577
|
+
|
|
578
|
+
Args:
|
|
579
|
+
name (list): names of attributes to set
|
|
580
|
+
value (list): values of attributes to set (parallel to name)
|
|
581
|
+
attributes (dict): attribute:value pairs that specify the set of atoms to be considered
|
|
582
|
+
"""
|
|
583
|
+
df = self.A
|
|
584
|
+
set_row_attribute(df, name, value, attributes)
|
|
585
|
+
|
|
586
|
+
def delete_atoms(self, idx=[], reindex=True):
|
|
587
|
+
"""Deletes atoms whose global indices appear in idx.
|
|
588
|
+
|
|
589
|
+
If reindex is True, global indices are recalculated to be sequential starting at 1
|
|
590
|
+
with no gaps, and two new columns are added: 'oldGlobalIdx' (pre-deletion indices)
|
|
591
|
+
and 'globalIdxShift' (change from old to new index for each atom).
|
|
592
|
+
|
|
593
|
+
Args:
|
|
594
|
+
idx (list): atom indexes to delete, defaults to []
|
|
595
|
+
reindex (bool): reindex remaining atoms, defaults to True
|
|
596
|
+
"""
|
|
597
|
+
# logger.debug(f'Coordinates:delete_atoms {idx}')
|
|
598
|
+
adf = self.A
|
|
599
|
+
indexes_to_drop = adf[adf.globalIdx.isin(idx)].index
|
|
600
|
+
indexes_to_keep = set(range(adf.shape[0])) - set(indexes_to_drop)
|
|
601
|
+
self.A = adf.take(list(indexes_to_keep)).reset_index(drop=True)
|
|
602
|
+
if reindex:
|
|
603
|
+
adf = self.A
|
|
604
|
+
oldGI = adf['globalIdx'].copy()
|
|
605
|
+
adf['globalIdx'] = adf.index + 1
|
|
606
|
+
mapper = {k: v for k, v in zip(oldGI, adf['globalIdx'])}
|
|
607
|
+
self.N -= len(idx)
|
|
608
|
+
''' delete appropriate bonds '''
|
|
609
|
+
if not self.mol2_bonds.empty:
|
|
610
|
+
d = self.mol2_bonds
|
|
611
|
+
indexes_to_drop = d[(d.ai.isin(idx)) | (d.aj.isin(idx))].index
|
|
612
|
+
indexes_to_keep = set(range(d.shape[0])) - set(indexes_to_drop)
|
|
613
|
+
self.mol2_bonds = d.take(list(indexes_to_keep)).reset_index(drop=True)
|
|
614
|
+
if reindex:
|
|
615
|
+
d = self.mol2_bonds
|
|
616
|
+
d.ai = d.ai.map(mapper)
|
|
617
|
+
d.aj = d.aj.map(mapper)
|
|
618
|
+
d.bondIdx = d.index + 1
|
|
619
|
+
if 'nBonds' in self.metadat:
|
|
620
|
+
self.metadat['nBonds'] = len(self.mol2_bonds)
|
|
621
|
+
self.bondlist = Bondlist.fromDataFrame(self.mol2_bonds)
|
|
622
|
+
|
|
623
|
+
def write_gro(self, filename, grotitle=''):
|
|
624
|
+
"""Writes coordinates and, if present, velocities to a GROMACS-format coordinate file.
|
|
625
|
+
|
|
626
|
+
Args:
|
|
627
|
+
filename (str): name of file to write
|
|
628
|
+
grotitle (str): title line for the .gro file, defaults to ''
|
|
629
|
+
"""
|
|
630
|
+
_gro_io.write(filename, self.A, self.box, self.name, grotitle=grotitle)
|
|
631
|
+
|
|
632
|
+
def write_mol2(self, filename, bondsDF=pd.DataFrame(), molname='', other_attributes=pd.DataFrame()):
|
|
633
|
+
"""Writes a mol2-format file from coordinates and an optional bonds DataFrame.
|
|
634
|
+
|
|
635
|
+
Args:
|
|
636
|
+
filename (str): name of file to write
|
|
637
|
+
bondsDF (pandas.DataFrame): dataframe of bonds ['ai','aj'], defaults to empty DataFrame
|
|
638
|
+
molname (str): name of molecule, defaults to ''
|
|
639
|
+
other_attributes (pandas.DataFrame): auxiliary dataframe of attributes, defaults to empty DataFrame
|
|
640
|
+
"""
|
|
641
|
+
_mol2_io.write(
|
|
642
|
+
filename, self.A, self.mol2_bonds, self.metadat, self.name,
|
|
643
|
+
bondsDF=bondsDF, molname=molname, other_attributes=other_attributes,
|
|
644
|
+
)
|