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.
Files changed (110) hide show
  1. htpolynet/__init__.py +2 -0
  2. htpolynet/analysis/__init__.py +0 -0
  3. htpolynet/analysis/analyze.py +256 -0
  4. htpolynet/analysis/plot.py +588 -0
  5. htpolynet/analysis/postsim.py +444 -0
  6. htpolynet/analysis/utils.py +399 -0
  7. htpolynet/cli.py +488 -0
  8. htpolynet/core/__init__.py +0 -0
  9. htpolynet/core/bondtemplate.py +112 -0
  10. htpolynet/core/configuration.py +83 -0
  11. htpolynet/core/coordinates.py +644 -0
  12. htpolynet/core/molecule.py +998 -0
  13. htpolynet/core/projectfilesystem.py +616 -0
  14. htpolynet/core/runtime.py +774 -0
  15. htpolynet/core/topocoord.py +2044 -0
  16. htpolynet/core/topology.py +1138 -0
  17. htpolynet/cure/__init__.py +0 -0
  18. htpolynet/cure/chain.py +135 -0
  19. htpolynet/cure/curecontroller.py +746 -0
  20. htpolynet/cure/expandreactions.py +294 -0
  21. htpolynet/cure/reaction.py +280 -0
  22. htpolynet/external/__init__.py +0 -0
  23. htpolynet/external/ambertools.py +97 -0
  24. htpolynet/external/command.py +73 -0
  25. htpolynet/external/gromacs.py +308 -0
  26. htpolynet/external/slurm.py +111 -0
  27. htpolynet/external/software.py +183 -0
  28. htpolynet/geometry/__init__.py +0 -0
  29. htpolynet/geometry/bondlist.py +205 -0
  30. htpolynet/geometry/lattice.py +32 -0
  31. htpolynet/geometry/linkcell.py +210 -0
  32. htpolynet/geometry/matrix4.py +110 -0
  33. htpolynet/geometry/ring.py +274 -0
  34. htpolynet/io/__init__.py +6 -0
  35. htpolynet/io/gro.py +195 -0
  36. htpolynet/io/mol2.py +210 -0
  37. htpolynet/io/pdb.py +157 -0
  38. htpolynet/resources/README.md +20 -0
  39. htpolynet/resources/__init__.py +0 -0
  40. htpolynet/resources/cfg/DGE-PAC-hi.yaml +115 -0
  41. htpolynet/resources/cfg/DGE-PAC-lo.yaml +115 -0
  42. htpolynet/resources/cfg/FDE-DFDA-hi.yaml +116 -0
  43. htpolynet/resources/cfg/FDE-DFDA-lo.yaml +116 -0
  44. htpolynet/resources/cfg/GMASTY-hi.yaml +158 -0
  45. htpolynet/resources/cfg/GMASTY-lo.yaml +158 -0
  46. htpolynet/resources/cfg/README.md +1 -0
  47. htpolynet/resources/cfg/STY.yaml +23 -0
  48. htpolynet/resources/cfg/pMSTY-hi.yaml +101 -0
  49. htpolynet/resources/cfg/pMSTY-lo.yaml +101 -0
  50. htpolynet/resources/cfg/pSTY-hi.yaml +101 -0
  51. htpolynet/resources/cfg/pSTY-lo.yaml +101 -0
  52. htpolynet/resources/example_depot/0-liquid-styrene.sh +134 -0
  53. htpolynet/resources/example_depot/0-liquid-styrene.tgz +0 -0
  54. htpolynet/resources/example_depot/1-polystyrene.sh +202 -0
  55. htpolynet/resources/example_depot/1-polystyrene.tgz +0 -0
  56. htpolynet/resources/example_depot/2-bisgma-styrene-thermoset.sh +353 -0
  57. htpolynet/resources/example_depot/2-bisgma-styrene-thermoset.tgz +0 -0
  58. htpolynet/resources/example_depot/3-pacm-dgeba-epoxy-thermoset.sh +242 -0
  59. htpolynet/resources/example_depot/3-pacm-dgeba-epoxy-thermoset.tgz +0 -0
  60. htpolynet/resources/example_depot/4-dfda-fde-epoxy-thermoset.sh +216 -0
  61. htpolynet/resources/example_depot/4-dfda-fde-epoxy-thermoset.tgz +0 -0
  62. htpolynet/resources/mdp/README.md +1 -0
  63. htpolynet/resources/mdp/drag-min.mdp +13 -0
  64. htpolynet/resources/mdp/drag-npt.mdp +36 -0
  65. htpolynet/resources/mdp/drag-nvt.mdp +31 -0
  66. htpolynet/resources/mdp/min.mdp +14 -0
  67. htpolynet/resources/mdp/npt.mdp +37 -0
  68. htpolynet/resources/mdp/nvt.mdp +29 -0
  69. htpolynet/resources/mdp/relax-min.mdp +13 -0
  70. htpolynet/resources/mdp/relax-npt.mdp +37 -0
  71. htpolynet/resources/mdp/relax-nvt.mdp +31 -0
  72. htpolynet/resources/mdp/single-molecule-min.mdp +12 -0
  73. htpolynet/resources/mdp/single-molecule-nvt.mdp +25 -0
  74. htpolynet/resources/molecules/inputs/DFA.pdb +62 -0
  75. htpolynet/resources/molecules/inputs/DGE.mol2 +115 -0
  76. htpolynet/resources/molecules/inputs/EMB.mol2 +50 -0
  77. htpolynet/resources/molecules/inputs/FDE.pdb +72 -0
  78. htpolynet/resources/molecules/inputs/GMA.mol2 +163 -0
  79. htpolynet/resources/molecules/inputs/PAC.mol2 +91 -0
  80. htpolynet/resources/molecules/inputs/STY.mol2 +44 -0
  81. htpolynet/resources/molecules/make-monomers.sh +64 -0
  82. htpolynet/resources/molecules/pics/DFA.png +0 -0
  83. htpolynet/resources/molecules/pics/DGE.png +0 -0
  84. htpolynet/resources/molecules/pics/EMB.png +0 -0
  85. htpolynet/resources/molecules/pics/FDE.png +0 -0
  86. htpolynet/resources/molecules/pics/GMA.png +0 -0
  87. htpolynet/resources/molecules/pics/PAC.png +0 -0
  88. htpolynet/resources/molecules/pics/STY.png +0 -0
  89. htpolynet/resources/molecules/sample-inputs/DFA.pdb +62 -0
  90. htpolynet/resources/molecules/sample-inputs/DGE.mol2 +115 -0
  91. htpolynet/resources/molecules/sample-inputs/EMB.mol2 +50 -0
  92. htpolynet/resources/molecules/sample-inputs/FDE.pdb +72 -0
  93. htpolynet/resources/molecules/sample-inputs/GMA.mol2 +163 -0
  94. htpolynet/resources/molecules/sample-inputs/PAC.mol2 +91 -0
  95. htpolynet/resources/molecules/sample-inputs/STY.mol2 +44 -0
  96. htpolynet/resources/tcl/readbonds.tcl +32 -0
  97. htpolynet/resources/tcl/readgrx.tcl +46 -0
  98. htpolynet/resources/tcl/render.tcl +74 -0
  99. htpolynet/utils/__init__.py +0 -0
  100. htpolynet/utils/banner.py +34 -0
  101. htpolynet/utils/checkpoint.py +85 -0
  102. htpolynet/utils/dataframetools.py +92 -0
  103. htpolynet/utils/inputcheck.py +49 -0
  104. htpolynet/utils/logsetup.py +44 -0
  105. htpolynet/utils/stringthings.py +39 -0
  106. htpolynet-2.0.0.dist-info/METADATA +78 -0
  107. htpolynet-2.0.0.dist-info/RECORD +110 -0
  108. htpolynet-2.0.0.dist-info/WHEEL +4 -0
  109. htpolynet-2.0.0.dist-info/entry_points.txt +2 -0
  110. 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
+ )