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,998 @@
1
+ """Manages generation of molecular templates.
2
+
3
+ Author: Cameron F. Abrams <cfa22@drexel.edu>
4
+ """
5
+ import logging
6
+ import os
7
+ import shutil
8
+
9
+ from copy import deepcopy
10
+ from itertools import product
11
+
12
+ import numpy as np
13
+ import pandas as pd
14
+
15
+ from ..core import projectfilesystem as pfs
16
+ from ..core.bondtemplate import BondTemplate, BondTemplateList, ReactionBond, ReactionBondList
17
+ from ..core.topocoord import TopoCoord
18
+ from ..cure.chain import ChainManager
19
+ from ..cure.reaction import Reaction, ReactionList, reaction_stage
20
+ from ..external.ambertools import GAFFParameterize
21
+ from ..external.command import run
22
+ from ..external.gromacs import mdp_modify,gro_from_trr
23
+ from ..geometry.matrix4 import Matrix4
24
+ from ..io.gro import GRX_ATTRIBUTES
25
+
26
+ logger=logging.getLogger(__name__)
27
+
28
+
29
+ def _yield_bonds_as_df(R: Reaction, TC: TopoCoord, resid_mapper) -> pd.DataFrame:
30
+ """Returns a DataFrame of reaction bonds by matching template bond records to atom instances.
31
+
32
+ Args:
33
+ R (Reaction): a Reaction
34
+ TC (TopoCoord): molecule topology and coordinates
35
+ resid_mapper (list[dict]): per-reactant maps from in-reactant resid to in-product resid
36
+
37
+ Returns:
38
+ pd.DataFrame: one row per bond with columns ai, aj, ri, rj, order, reactantName
39
+ """
40
+ rows = []
41
+ for bondrec in R.bonds:
42
+ atom_keys = bondrec['atoms']
43
+ assert len(atom_keys) == 2, f'bond record must have exactly 2 atoms, got {len(atom_keys)}'
44
+ atomrecs = [R.atoms[k] for k in atom_keys]
45
+ atom_names = [rec['atom'] for rec in atomrecs]
46
+ in_product_resids = [resid_mapper[atomrecs[x]['reactant'] - 1][atomrecs[x]['resid']] for x in (0, 1)]
47
+ ai = TC.get_gro_attribute_by_attributes('globalIdx', {'resNum': in_product_resids[0], 'atomName': atom_names[0]})
48
+ aj = TC.get_gro_attribute_by_attributes('globalIdx', {'resNum': in_product_resids[1], 'atomName': atom_names[1]})
49
+ rows.append({'ai': ai, 'aj': aj, 'ri': in_product_resids[0], 'rj': in_product_resids[1],
50
+ 'order': bondrec['order'], 'reactantName': R.product})
51
+ return pd.DataFrame(rows)
52
+
53
+ class Molecule:
54
+ def __init__(self, name='', generator:Reaction=None, origin:str=None):
55
+ self.name = name
56
+ self.parentname = name # stereoisomer parent
57
+ self.TopoCoord = TopoCoord()
58
+ self.generator:Reaction = generator
59
+ self.sequence = []
60
+ self.origin = origin
61
+ self.reaction_bonds:ReactionBondList = []
62
+ self.bond_templates:BondTemplateList = []
63
+ self.symmetry_relateds = []
64
+ self.stereocenters = [] # list of atomnames
65
+ self.stereoisomers = {}
66
+ self.nconformers = 0
67
+ self.conformers_dict = {}
68
+ self.conformers = [] # just a list of gro file basenames
69
+ self.zrecs = []
70
+ self.is_reactant = False
71
+ self.chain_manager = ChainManager()
72
+
73
+ @classmethod
74
+ def New(cls, mol_name, generator: Reaction, molrec: dict | None = None):
75
+ """Generates a new, partially populated Molecule based on directives in the configuration input.
76
+
77
+ Args:
78
+ mol_name (str): name of molecule
79
+ generator (Reaction): reaction that generates this molecule, if applicable
80
+ molrec (dict, optional): dictionary of directives for this molecule, defaults to None
81
+
82
+ Returns:
83
+ Molecule: a new Molecule object
84
+ """
85
+ M = cls(name=mol_name, generator=generator)
86
+ if not molrec: return M
87
+ M.symmetry_relateds = molrec.get('symmetry_equivalent_atoms', [])
88
+ M.stereocenters = molrec.get('stereocenters', [])
89
+ # expand stereocenters: any atom in a symmetry-equivalent set with a declared
90
+ # stereocenter pulls the rest of that set in as stereocenters too
91
+ extra = set()
92
+ for stc in M.stereocenters:
93
+ for sc in M.symmetry_relateds:
94
+ if stc in sc:
95
+ extra.update(sc)
96
+ extra -= set(M.stereocenters)
97
+ M.stereocenters.extend(extra)
98
+ logger.debug(f'{M.name} stereocenters: {M.stereocenters}')
99
+ # generate shells for new stereoisomers
100
+ M.create_new_stereoisomers()
101
+ logger.debug(f'{M.name} stereoisomers: {[s.name for s in M.stereoisomers.values()]}')
102
+ M.conformers_dict = molrec.get('conformers', {})
103
+ logger.debug(f'{M.name} conformers_dict {M.conformers_dict}')
104
+ return M
105
+
106
+ def update_zrecs(self, zrecs, moldict):
107
+ """Updates the "z-records" based on z's declared in the input configuration file.
108
+
109
+ Args:
110
+ zrecs (dict): zrecs extracted from configuration file for this molecule
111
+ moldict (dict): dictionary of available molecules
112
+ """
113
+ def replace_if_greater(rec, D, matchattr=[], maxattr=[]):
114
+ if not matchattr or not maxattr: return
115
+ for r in D:
116
+ matched = all([r[a] == rec[a] for a in matchattr])
117
+ if matched:
118
+ replace = all([r[a] < rec[a] for a in maxattr])
119
+ if replace:
120
+ D.remove(r)
121
+ D.append(rec)
122
+ return True
123
+ return False
124
+ logger.debug(f'Update zrecs in {self.name} from {zrecs}')
125
+ seq = self.sequence
126
+ for zr in zrecs:
127
+ resid = zr['resid'] - 1
128
+ rname = seq[resid]
129
+ target = moldict[rname]
130
+ logger.debug(f'{target.name} {target.zrecs} ->')
131
+ found = replace_if_greater(zr, target.zrecs, matchattr=['resid', 'atom'], maxattr=['z'])
132
+ if not found: target.zrecs.append(zr)
133
+ logger.debug(f'-> {target.name} {target.zrecs}')
134
+
135
+ def determine_sequence(self, moldict):
136
+ """Recursively determines the sequence of a molecule using the network of reactions that must be executed to generate it from primitives.
137
+
138
+ Args:
139
+ moldict (dict): dictionary of available molecules
140
+
141
+ Returns:
142
+ list: list of resnames in order of sequence
143
+ """
144
+ if not self.generator: return [self.parentname]
145
+ R:Reaction = self.generator
146
+ thisseq = []
147
+ for rid, rname in R.reactants.items():
148
+ parentname = moldict[rname].parentname
149
+ thisseq.extend(moldict[parentname].determine_sequence(moldict))
150
+ # logger.debug(thisseq)
151
+ return thisseq
152
+
153
+ def set_sequence_from_moldict(self, moldict):
154
+ """Sets the sequence of this molecule using the recursive determine_sequence method.
155
+
156
+ Args:
157
+ moldict (dict): dictionary of available molecules
158
+
159
+ Returns:
160
+ Molecule: self
161
+ """
162
+ self.sequence = self.determine_sequence(moldict)
163
+ return self
164
+
165
+ def set_sequence_from_coordinates(self):
166
+ """set_sequence Establish the sequence-list (residue names in order) based on resNum attributes in atom list
167
+ """
168
+ adf = self.TopoCoord.gro_DataFrame('atoms')
169
+ trial_sequence = []
170
+ current_resid = 0
171
+ for i, r in adf.iterrows():
172
+ ri = r['resNum']
173
+ rn = r['resName']
174
+ if ri != current_resid:
175
+ current_resid = ri
176
+ trial_sequence.append(rn)
177
+ assert trial_sequence == self.sequence, f'Error: trial {trial_sequence} does not equal self.seq {self.sequence}'
178
+ return self
179
+
180
+ def create_new_stereoisomers(self):
181
+ """Generates new molecules to hold stereoisomers of self.
182
+
183
+ Returns:
184
+ None: returns early if no action taken
185
+ """
186
+ if self.generator: return # we only consider stereoisomers on monomers
187
+ if not self.stereocenters: return
188
+ basename = self.name + '-S'
189
+ b = [[0, 1] for _ in range(len(self.stereocenters))]
190
+ sseq = product(*b)
191
+ next(sseq) # skip the unmodified; it's the base molecule
192
+ for x in sseq:
193
+ s = ''.join([str(_) for _ in x])
194
+ mname = f'{basename}{s}'
195
+ self.stereoisomers[mname] = Molecule.New(mname, None)
196
+ self.stereoisomers[mname].parentname = self.name
197
+
198
+ def initialize_molecule_rings(self):
199
+ """initialize_molecule_rings generates the dictionary of rings
200
+
201
+ the dictionary of rings is keyed on ring size
202
+ """
203
+ TC = self.TopoCoord
204
+ TC.Topology.detect_rings()
205
+ logger.debug(f'Detected {len(TC.Topology.rings)} unique rings.')
206
+
207
+ def initialize_monomer_grx_attributes(self):
208
+ """initialize_monomer_grx_attributes initializes all GRX attributes of atoms in molecule
209
+ """
210
+ logger.debug(f'{self.name}')
211
+ TC = self.TopoCoord
212
+ TC.set_gro_attribute('z', 0)
213
+ TC.set_gro_attribute('nreactions', 0)
214
+ TC.set_gro_attribute('molecule', 1)
215
+ TC.set_gro_attribute('molecule_name', self.name)
216
+ for att in ['sea_idx','bondchain','bondchain_idx']:
217
+ TC.set_gro_attribute(att,-1)
218
+ # set symmetry class indices
219
+ sea_idx=1
220
+ logger.debug(f'{self.name}: symmetry_relateds {self.symmetry_relateds}')
221
+ for s in self.symmetry_relateds:
222
+ # logger.debug(f'sea_idx {sea_idx} set for set {s}')
223
+ for atomName in s:
224
+ # logger.debug(f'{atomName} {sea_idx}')
225
+ TC.set_gro_attribute_by_attributes('sea_idx', sea_idx, {'atomName': atomName})
226
+ sea_idx += 1
227
+ # set z and nreactions
228
+ idx = []
229
+ for zr in self.zrecs:
230
+ an = zr['atom']
231
+ rnum = zr['resid']
232
+ if rnum != 1: continue
233
+ z = zr['z']
234
+ logger.debug(f'{self.name} setting z for {an} {rnum} {z}')
235
+ TC.set_gro_attribute_by_attributes('z', z, {'atomName': an, 'resNum': rnum})
236
+ idx.append(TC.get_gro_attribute_by_attributes('globalIdx', {'atomName': an, 'resNum': rnum}))
237
+ for sr in self.symmetry_relateds:
238
+ # logger.debug(f'{self.name}: setting z for {an}, considering sr {sr}')
239
+ if an in sr:
240
+ for bn in sr:
241
+ if bn == an: continue
242
+ # logger.debug(f'{self.name}: setting z for {bn}')
243
+ idx.append(TC.get_gro_attribute_by_attributes('globalIdx', {'atomName': bn, 'resNum': rnum}))
244
+ TC.set_gro_attribute_by_attributes('z', z, {'atomName': bn, 'resNum': rnum})
245
+
246
+ # TC.idx_lists['bondchain']=[]
247
+ self.chain_manager = ChainManager(create_if_missing=True)
248
+ pairs = product(idx, idx)
249
+ for i, j in pairs:
250
+ if i < j:
251
+ iname = TC.get_gro_attribute_by_attributes('atomName', {'globalIdx': i})
252
+ jname = TC.get_gro_attribute_by_attributes('atomName', {'globalIdx': j})
253
+ if TC.Topology.bondlist.are_bonded(i, j) and iname.startswith('C') and jname.startswith('C'):
254
+ # this monomer has two carbon atoms capable of reacting
255
+ # that are bound to each other -- this means that
256
+ # the two originated as a double-bond.
257
+ # *If* there is one with three hydrogens
258
+ # (remember this is an activated monomer)
259
+ # then it is the "tail"; the other is the "head".
260
+ i_nH = TC.count_H(i)
261
+ j_nH = TC.count_H(j)
262
+ if i_nH == 3 and j_nH != 3:
263
+ # i is the tail
264
+ entry = [j, i]
265
+ elif i_nH != 3 and j_nH == 3:
266
+ # j is the tail
267
+ entry = [i, j]
268
+ else:
269
+ logger.warning(f'In molecule {self.name}, cannot identify bonded reactive head and tail atoms\nAssuming {j} is head and {i} is tail')
270
+ entry = [j, i]
271
+ # logger.debug(f'Adding {entry} to chainlist of {self.name}')
272
+ self.chain_manager.injest_bond(entry[0], entry[1])
273
+ # TC.idx_lists['bondchain'].append(entry)
274
+ # TC.reset_grx_attributes_from_idx_list('bondchain')
275
+ self.chain_manager.to_dataframe(TC.Coordinates.A)
276
+ self.initialize_molecule_rings()
277
+
278
+ def previously_parameterized(self):
279
+ """Returns True if a gro file exists in the project molecule/parameterized directory for this molecule.
280
+
281
+ Returns:
282
+ bool: True if gro file found
283
+ """
284
+ rval = True
285
+ for ext in ['gro']:
286
+ rval = rval and pfs.exists(os.path.join(pfs.Dirs.molecules_parameterized, f'{self.name}.{ext}'))
287
+ return rval
288
+
289
+ def parameterize(self, outname='', input_structure_format='mol2', ambertools={}):
290
+ """Manages GAFF parameterization of this molecule.
291
+
292
+ Args:
293
+ outname (str, optional): output file basename, defaults to ''
294
+ input_structure_format (str, optional): input structure format, defaults to 'mol2' ('pdb' is other possibility)
295
+ ambertools (dict): ambertools configuration directives, defaults to {}
296
+ """
297
+ assert os.path.exists(f'{self.name}.{input_structure_format}'),f'Cannot parameterize molecule {self.name} without {self.name}.{input_structure_format} as input'
298
+ if outname == '':
299
+ outname = f'{self.name}'
300
+ if input_structure_format == 'pdb':
301
+ # Convert PDB → mol2 before antechamber so that repeated CONECT entries
302
+ # (which encode double bonds in PDB format) are written as proper bond
303
+ # orders in the mol2 bond table rather than duplicated single bonds.
304
+ from .coordinates import Coordinates
305
+ coords = Coordinates.read_pdb(f'{self.name}.pdb')
306
+ coords.write_mol2(f'{self.name}.mol2', molname=self.name)
307
+ logger.info(f'Converted {self.name}.pdb → {self.name}.mol2 with bond orders from CONECT records')
308
+ input_structure_format = 'mol2'
309
+ GAFFParameterize(self.name, outname, input_structure_format=input_structure_format, ambertools=ambertools)
310
+ self.load_top_gro(f'{outname}.top', f'{outname}.gro', mol2filename=f'{outname}.mol2', wrap_coords=False)
311
+ self.initialize_molecule_rings()
312
+ self.TopoCoord.write_tpx(f'{outname}.tpx')
313
+
314
+ def minimize(self, outname=''):
315
+ """Manages invocation of vacuum minimization.
316
+
317
+ Args:
318
+ outname (str, optional): output file basename, defaults to ''
319
+ """
320
+ if outname == '':
321
+ outname = f'{self.name}'
322
+ self.TopoCoord.vacuum_minimize(outname)
323
+
324
+ def relax(self, relax_dict):
325
+ """Manages invocation of MD relaxations.
326
+
327
+ Args:
328
+ relax_dict (dict): dictionary of simulation directives
329
+ """
330
+ deffnm = relax_dict.get('deffnm', f'{self.name}-relax')
331
+ nsteps = relax_dict.get('nsteps', 10000)
332
+ temperature = relax_dict.get('temperature', 10000)
333
+ n = self.name
334
+ boxsize = np.array(self.TopoCoord.maxspan()) + 2 * np.ones(3)
335
+ self.center_coords(new_boxsize=boxsize)
336
+ mdp_prefix = 'single-molecule-nvt'
337
+ pfs.checkout(pfs.Dirs.mdp_file(mdp_prefix))
338
+ mdp_modify(f'{mdp_prefix}.mdp', {'nsteps': nsteps, 'gen-vel': 'yes', 'ref_t': temperature, 'gen-temp': temperature})
339
+ logger.info(f'In vacuo equilibration of {self.name}.gro for {nsteps} steps at {temperature} K')
340
+ self.TopoCoord.grompp_and_mdrun(out=deffnm, mdp=mdp_prefix, boxSize=boxsize)
341
+
342
+ def center_coords(self, new_boxsize: np.ndarray = None):
343
+ """Wrapper for the TopoCoord.center_coords method.
344
+
345
+ Args:
346
+ new_boxsize (np.ndarray, optional): new box size, defaults to None
347
+ """
348
+ self.TopoCoord.center_coords(new_boxsize)
349
+
350
+ def generate(self, outname='', available_molecules={}, gaff={}, ambertools={}):
351
+ """Manages generating topology and coordinates for self.
352
+
353
+ Args:
354
+ outname (str, optional): output file basename, defaults to ''
355
+ available_molecules (dict, optional): dictionary of available molecules, defaults to {}
356
+ gaff (dict): GAFF configuration directives, defaults to {}
357
+ ambertools (dict): ambertools configuration directives, defaults to {}
358
+ """
359
+ logger.debug(f'{self.name}.generate() begins')
360
+ if outname == '': outname = f'{self.name}'
361
+ do_minimization = gaff.get('minimize_molecules', True)
362
+ do_parameterization = False
363
+ if self.generator:
364
+ R = self.generator
365
+ if R.stage in [reaction_stage.cure, reaction_stage.param, reaction_stage.cap]: do_parameterization = True
366
+ self.TopoCoord = TopoCoord()
367
+ logger.debug(f'Using reaction {R.name} ({str(R.stage)}) to generate {self.name} parent {self.parentname}')
368
+ isf = 'mol2'
369
+ resid_mapper = []
370
+ for ri in R.reactants.values():
371
+ logger.debug(f'Adding {ri}')
372
+ new_reactant = deepcopy(available_molecules[ri])
373
+ new_reactant.TopoCoord.write_mol2(filename=f'{self.name}-reactant{ri}-prebonding.mol2', molname=self.name)
374
+ rnr = len(new_reactant.sequence)
375
+ shifts = self.TopoCoord.merge(new_reactant.TopoCoord, self_cm=self.chain_manager, other_cm=new_reactant.chain_manager)
376
+ # for ln in self.TopoCoord.Coordinates.A.head().to_string().split('\n'): logger.debug(ln)
377
+ resid_mapper.append({k:v for k,v in zip(range(1,rnr+1),range(1+shifts[2],1+rnr+shifts[2]))})
378
+ # logger.debug(f'{self.name}: resid_mapper {resid_mapper}')
379
+ # logger.debug(f'{self.TopoCoord.idx_lists}')
380
+ # logger.debug(f'\n{self.TopoCoord.Coordinates.A.to_string()}')
381
+ # logger.debug(f'composite prebonded molecule in box {self.TopoCoord.Coordinates.box}')
382
+ self.TopoCoord.write_mol2(filename=f'{self.name}-prebonding.mol2', molname=self.name)
383
+ self.set_sequence_from_coordinates()
384
+ bdf = _yield_bonds_as_df(R, self.TopoCoord, resid_mapper)
385
+ # logger.debug(f'Generation of {self.name}: composite molecule has {len(self.sequence)} resids')
386
+ # logger.debug(f'generation of {self.name}: composite molecule:\n{composite_mol.TopoCoord.Coordinates.A.to_string()}')
387
+ self.make_bonds(bdf, available_molecules, R.stage)
388
+ # self.TopoCoord.set_gro_attribute('reactantName',R.product)
389
+ self.TopoCoord.set_gro_attribute('sea_idx', -1) # turn off symmetry-equivalence for multimers
390
+ self.TopoCoord.set_gro_attribute('molecule', 1)
391
+ self.TopoCoord.set_gro_attribute('molecule_name', self.name)
392
+ self.write_gro_attributes(GRX_ATTRIBUTES, f'{R.product}.grx')
393
+ # if pfs.exists(f'molecules/inputs/{self.name}.mol2'): # an override structure is present
394
+ # logger.debug(f'Using override input molecules/inputs/{self.name}.{isf} as a generator')
395
+ # pfs.checkout(f'molecules/inputs/{self.name}.{isf}')
396
+ # else:
397
+ self.TopoCoord.write_mol2(filename=f'{self.name}.mol2', molname=self.name)
398
+ if not do_parameterization:
399
+ self.TopoCoord.write_gro(f'{self.name}.gro', grotitle=self.name)
400
+ self.TopoCoord.write_top(f'{self.name}.top')
401
+ # if pfs.exists(f'molecules/inputs/{self.name}.pdb'): # an override structure is present
402
+ # isf='pdb'
403
+ # logger.debug(f'Using override input molecules/inputs/{self.name}.{isf} as a generator')
404
+ # pfs.checkout(f'molecules/inputs/{self.name}.{isf}')
405
+ else:
406
+ # this molecule was not assigned a generator: implies it is a monomer and must have a parameterization
407
+ input_structure_formats = ['mol2','pdb']
408
+ isf = None
409
+ for isf in input_structure_formats:
410
+ if pfs.exists(f'{pfs.Dirs.molecules_inputs}/{self.name}.{isf}'):
411
+ logger.debug(f'Using input {pfs.Dirs.molecules_inputs}/{self.name}.{isf} as a generator')
412
+ pfs.checkout(f'{pfs.Dirs.molecules_inputs}/{self.name}.{isf}')
413
+ break
414
+ assert isf, 'Error: no valid input structure file found'
415
+ do_parameterization = True
416
+
417
+ reactantName = self.name
418
+ if do_parameterization:
419
+ self.parameterize(outname, input_structure_format=isf, ambertools=ambertools)
420
+ else:
421
+ if self.name != self.parentname:
422
+ logger.info(f'Built {self.name} using topology of {self.parentname}; copying {self.parentname}.top to {self.name}.top')
423
+ self.load_top_gro(f'{self.parentname}.top', f'{self.name}.gro', tpxfilename=f'{self.parentname}.tpx', wrap_coords=False)
424
+ shutil.copy(f'{self.parentname}.top', f'{self.name}.top')
425
+ shutil.copy(f'{self.parentname}.grx', f'{self.name}.grx')
426
+ shutil.copy(f'{self.parentname}.tpx', f'{self.name}.tpx')
427
+
428
+ if do_minimization:
429
+ self.minimize(outname)
430
+ self.set_sequence_from_coordinates()
431
+ if not self.generator:
432
+ self.TopoCoord.set_gro_attribute('reactantName', reactantName)
433
+ self.initialize_monomer_grx_attributes()
434
+ self.write_gro_attributes(GRX_ATTRIBUTES, f'{reactantName}.grx')
435
+ else:
436
+ if do_parameterization or self.name != self.parentname:
437
+ grx=f'{reactantName}.grx'
438
+ if (os.path.exists(grx)):
439
+ self.TopoCoord.read_gro_attributes(grx)
440
+ #self.reset_chains_from_attributes()
441
+ # logger.debug(f'{self.name} gro\n{self.TopoCoord.Coordinates.A.to_string()}')
442
+ self.prepare_new_bonds(available_molecules=available_molecules)
443
+
444
+ # for ln in self.TopoCoord.Coordinates.A.head().to_string().split('\n'): logger.debug(ln)
445
+ logger.info(f'{self.name}: {self.get_molecular_weight():.2f} g/mol')
446
+ logger.debug('Done.')
447
+
448
+ def get_molecular_weight(self):
449
+ """Returns the molecular weight of self.
450
+
451
+ Returns:
452
+ float: molecular weight in g/mol
453
+ """
454
+ mass = self.TopoCoord.Topology.total_mass(units='gromacs') # g
455
+ return mass
456
+
457
+ def prepare_new_bonds(self, available_molecules={}):
458
+ """Populates the bond templates and reaction bonds for self.
459
+
460
+ Args:
461
+ available_molecules (dict, optional): dictionary of available molecules, defaults to {}
462
+ """
463
+ # logger.debug(f'set_reaction_bonds: molecules {list(available_molecules.keys())}')
464
+ R = self.generator
465
+ if not R:
466
+ return
467
+ self.reaction_bonds = []
468
+ self.bond_templates = []
469
+ TC = self.TopoCoord
470
+ # logger.debug(f'prepare_new_bonds {self.name}: chainlists {TC.idx_lists["bondchain"]}')
471
+ for bondrec in R.bonds:
472
+ atom_keys = bondrec['atoms']
473
+ order = bondrec['order']
474
+ assert len(atom_keys) == 2
475
+ atomrecs = [R.atoms[x] for x in atom_keys]
476
+ atom_names = [x['atom'] for x in atomrecs]
477
+ reactant_keys = [x['reactant'] for x in atomrecs]
478
+ in_reactant_resids = [x['resid'] for x in atomrecs]
479
+ if reactant_keys[0] == reactant_keys[1]: # this is an intraresidue bond
480
+ reactant_names = [R.reactants[reactant_keys[0]]]
481
+ else:
482
+ reactant_names = [R.reactants[x] for x in reactant_keys]
483
+ reactant_sequences = [available_molecules[x].sequence for x in reactant_names]
484
+ product_sequence = []
485
+ for seq in reactant_sequences:
486
+ product_sequence.extend(seq)
487
+ # logger.debug(f'product_sequence {product_sequence}')
488
+ sequence_residue_idx_origins = [0, 0]
489
+ if len(reactant_sequences) == 2:
490
+ sequence_residue_idx_origins[1] = len(reactant_sequences[0])
491
+ in_product_resids = [in_reactant_resids[x] + sequence_residue_idx_origins[x] for x in [0, 1]]
492
+ # logger.debug(f'in_product_resids {in_product_resids}')
493
+ in_product_resnames = [product_sequence[in_product_resids[x] - 1] for x in [0, 1]]
494
+ atom_idx = [TC.get_gro_attribute_by_attributes('globalIdx', {'resNum': in_product_resids[x], 'atomName': atom_names[x]}) for x in [0, 1]]
495
+ # logger.debug(f'{R.name} names {atom_names} in_product_resids {in_product_resids} idx {atom_idx}')
496
+ bystander_resids, bystander_resnames, bystander_atomidx, bystander_atomnames = TC.get_bystanders(atom_idx)
497
+ oneaway_resids, oneaway_resnames, oneaway_atomidx, oneaway_atomnames = TC.get_oneaways(atom_idx, chain_manager=self.chain_manager)
498
+ # logger.debug(f'{self.name} bystanders {bystander_resids} {bystander_resnames} {bystander_atomidx} {bystander_atomnames}')
499
+ # logger.debug(f'{self.name} oneaways {oneaway_resids} {oneaway_resnames} {oneaway_atomidx} {oneaway_atomnames}')
500
+ self.reaction_bonds.append(ReactionBond(atom_idx, in_product_resids, order, bystander_resids, bystander_atomidx, oneaway_resids, oneaway_atomidx))
501
+ intraresidue = in_product_resids[0] == in_product_resids[1]
502
+ self.bond_templates.append(BondTemplate(atom_names, in_product_resnames, intraresidue, order, bystander_resnames, bystander_atomnames, oneaway_resnames, oneaway_atomnames))
503
+
504
+ def idx_mappers(self, otherTC: TopoCoord, other_bond, bystanders, oneaways, uniq_atom_idx: set):
505
+ """Computes the mapping dictionary from molecule template index to instance index in the other TopoCoord.
506
+
507
+ Args:
508
+ otherTC (TopoCoord): the other TopoCoord
509
+ other_bond: 2 atom indices of the bond in the other TopoCoord
510
+ bystanders: bystander lists, one for each reacting atom
511
+ oneaways: oneaways, one for each atom in the bond
512
+ uniq_atom_idx (set): set of unique atoms in template that must be mapped to instance
513
+
514
+ Raises:
515
+ Exception: if there is a buggy double-counting of one or more indexes
516
+
517
+ Returns:
518
+ tuple: two-way dictionaries of index mappers instance<->template
519
+ """
520
+ assert len(other_bond) == 2
521
+ assert len(bystanders) == 2
522
+ assert len(oneaways) == 2
523
+ ut = uniq_atom_idx.copy()
524
+ logger.debug(f'Template name {self.name}')
525
+ i_idx, j_idx = other_bond
526
+ i_resName, i_resNum, i_atomName = otherTC.get_gro_attribute_by_attributes(['resName','resNum','atomName'],{'globalIdx':i_idx})
527
+ j_resName, j_resNum, j_atomName = otherTC.get_gro_attribute_by_attributes(['resName','resNum','atomName'],{'globalIdx':j_idx})
528
+ logger.debug(f'i_idx {i_idx} i_resName {i_resName} i_resNum {i_resNum} i_atomName {i_atomName}')
529
+ logger.debug(f'j_idx {j_idx} j_resName {j_resName} j_resNum {j_resNum} j_atomName {j_atomName}')
530
+ # identify the template bond represented by the other_bond parameter
531
+ ij = []
532
+ for RB, BT in zip(self.reaction_bonds, self.bond_templates):
533
+ temp_resids = RB.resids
534
+ temp_iname, temp_jname = BT.names
535
+ temp_iresname, temp_jresname = BT.resnames
536
+ temp_bystander_resids = RB.bystander_resids
537
+ temp_oneaway_resids = RB.oneaway_resids
538
+ # logger.debug(f'temp_iresname {temp_iresname} temp_iname {temp_iname}')
539
+ # logger.debug(f'temp_jresname {temp_jresname} temp_jname {temp_jname}')
540
+ if (i_atomName, i_resName) == (temp_iname, temp_iresname):
541
+ ij = [0, 1]
542
+ break # found it -- stop looking
543
+ elif (i_atomName, i_resName) == (temp_jname, temp_jresname):
544
+ ij = [1, 0]
545
+ break
546
+ assert len(ij) == 2, f'Mappers using template {self.name} unable to map from instance bond {i_resName}-{i_resNum}-{i_atomName}---{j_resName}-{j_resNum}-{j_atomName}'
547
+ inst_resids = [i_resNum, j_resNum]
548
+ inst_resids = [inst_resids[ij[x]] for x in [0, 1]]
549
+ inst_bystander_resids = [bystanders[ij[x]] for x in [0, 1]]
550
+ inst_oneaway_resids = [oneaways[ij[x]] for x in [0, 1]]
551
+ assert all([len(inst_bystander_resids[x]) == len(temp_bystander_resids[x]) for x in [0, 1]]), f'Error: bystander count mismatch'
552
+ # use dataframe merges to create globalIdx maps
553
+ instdf = otherTC.Coordinates.A
554
+ tempdf = self.TopoCoord.Coordinates.A
555
+ inst2temp = {}
556
+ temp2inst = {}
557
+ # logger.debug(f'inst resids from {[i_resNum,j_resNum,*inst_bystanders]}')
558
+ # logger.debug(f'temp resids from {[temp_iresid,temp_jresid,*temp_bystanders]}')
559
+ for inst, temp in zip([*inst_resids, *inst_bystander_resids[0], *inst_bystander_resids[1],*inst_oneaway_resids],
560
+ [*temp_resids, *temp_bystander_resids[0], *temp_bystander_resids[1],*temp_oneaway_resids]):
561
+ if inst and temp: # None's in the bystander lists and oneaways lists should be ignored
562
+ logger.debug(f'map inst resid {inst} to template resid {temp}')
563
+ idf = instdf[instdf['resNum'] == inst][['globalIdx','atomName']].copy()
564
+ # logger.debug(f'idf res {inst}:\n{idf.to_string()}')
565
+ tdf = tempdf[tempdf['resNum'] == temp][['globalIdx','atomName']].copy()
566
+ # logger.debug(f'tdf res {temp}:\n{tdf.to_string()}')
567
+ tdf = tdf.merge(idf, on='atomName', how='inner', suffixes=('_template','_instance'))
568
+ # logger.debug(f'merged\n{tdf.to_string()}')
569
+ for i, r in tdf.iterrows():
570
+ temp_idx = r['globalIdx_template']
571
+ inst_idx = r['globalIdx_instance']
572
+ # logger.debug(f't {temp_idx} <-> i {inst_idx}')
573
+ # only map template atoms that are identified in the passed in set
574
+ if temp_idx in ut:
575
+ ut.remove(temp_idx)
576
+ temp2inst[temp_idx] = inst_idx
577
+ inst2temp[inst_idx] = temp_idx
578
+ if temp_idx in temp2inst and temp2inst[temp_idx] != inst_idx:
579
+ raise Exception(f'Error: temp_idx {temp_idx} already claimed in temp2inst; bug')
580
+ assert len(inst2temp) == len(temp2inst), f'Error: could not establish two-way dict of atom globalIdx'
581
+ return (inst2temp, temp2inst)
582
+
583
+ def get_angles_dihedrals(self, bond):
584
+ """Returns copies of selections from the Topology interaction-type dataframes that contain the two atoms indicated in the bond.
585
+
586
+ Args:
587
+ bond: 2-element list-like container of ints
588
+
589
+ Raises:
590
+ Exception: if a NaN is found in any selection
591
+
592
+ Returns:
593
+ tuple: copies of the dataframe selections for angles, dihedrals, and 1-4 pairs
594
+ """
595
+ ai, aj = bond
596
+ d = self.TopoCoord.Topology.D['angles']
597
+ ad = d[((d.ai == ai) & (d.aj == aj)) |
598
+ ((d.ai == aj) & (d.aj == ai)) |
599
+ ((d.aj == ai) & (d.ak == aj)) |
600
+ ((d.aj == aj) & (d.ak == ai))].copy()
601
+ d = self.TopoCoord.Topology.D['dihedrals']
602
+ td = d[((d.ai == ai) & (d.aj == aj)) |
603
+ ((d.ai == aj) & (d.aj == ai)) |
604
+ ((d.aj == ai) & (d.ak == aj)) |
605
+ ((d.aj == aj) & (d.ak == ai)) |
606
+ ((d.ak == ai) & (d.al == aj)) |
607
+ ((d.ak == aj) & (d.al == ai))].copy()
608
+ check = True
609
+ for a in ['ai', 'aj', 'ak', 'al']:
610
+ check = check and td[a].isnull().values.any()
611
+ if check:
612
+ logger.error('NAN in molecule/dihedrals')
613
+ raise ValueError('NAN in molecule/dihedrals')
614
+
615
+ d = self.TopoCoord.Topology.D['pairs']
616
+ paird = pd.DataFrame()
617
+ for ai, al in zip(td.ai, td.al):
618
+ tpair = d[((d.ai == ai) & (d.aj == al)) |
619
+ ((d.ai == al) & (d.aj == ai))].copy()
620
+ paird = pd.concat((paird, tpair), ignore_index=True)
621
+ check = True
622
+ for a in ['ai', 'aj']:
623
+ check = check and paird[a].isnull().values.any()
624
+ if check:
625
+ logger.error('NAN in molecule/pairs')
626
+ raise ValueError('NAN in molecule/pairs')
627
+ return ad, td, paird
628
+
629
+ def get_resname(self, internal_resid):
630
+ """Returns the residue name at position internal_resid in the molecule's sequence.
631
+
632
+ Args:
633
+ internal_resid (int): molecule-internal residue index
634
+
635
+ Returns:
636
+ str: residue name
637
+ """
638
+ return self.sequence[internal_resid - 1]
639
+
640
+ # def inherit_attribute_from_reactants(self,attribute,available_molecules,increment=True,no_increment_if_negative=True):
641
+ # """inherit_attribute_from_reactants populate certain atom attributes in molecule from its constituent reactants
642
+
643
+ # :param attribute: _description_
644
+ # :type attribute: _type_
645
+ # :param available_molecules: _description_
646
+ # :type available_molecules: _type_
647
+ # :param increment: _description_, defaults to True
648
+ # :type increment: bool, optional
649
+ # :param no_increment_if_negative: _description_, defaults to True
650
+ # :type no_increment_if_negative: bool, optional
651
+ # """
652
+ # adf=self.TopoCoord.Coordinates.A
653
+ # ordered_attribute_idx=[]
654
+ # curr_max=0
655
+ # # logger.debug(f'{self.name}({adf.shape[0]}) inheriting {attribute} from {self.sequence}')
656
+ # # logger.debug(f'available molecules {list(available_molecules.keys())}')
657
+ # for i,r in enumerate(self.sequence):
658
+ # '''
659
+ # for this residue number, read the list of unique atom names
660
+ # '''
661
+ # namesinres=list(adf[adf['resNum']==(i+1)]['atomName'])
662
+ # '''
663
+ # access coordinates of standalone residue template with this name 'r' on the list of available molecules
664
+ # '''
665
+ # rdf=available_molecules[r].TopoCoord.Coordinates.A
666
+ # '''
667
+ # get the attribute values from residue template
668
+ # '''
669
+ # x=list(rdf[rdf['atomName'].isin(namesinres)][attribute])
670
+ # # logger.debug(f'{r}->{len(x)}')
671
+ # '''
672
+ # increment these attribute value based on residue number in this molecule
673
+ # '''
674
+ # if increment:
675
+ # i_x=[]
676
+ # for y in x:
677
+ # if y>0 or (y<0 and not no_increment_if_negative):
678
+ # i_x.append(y+curr_max)
679
+ # else:
680
+ # i_x.append(y)
681
+ # curr_max=max(i_x)
682
+ # ordered_attribute_idx.extend(i_x)
683
+ # assert len(ordered_attribute_idx)==adf.shape[0]
684
+ # adf[attribute]=ordered_attribute_idx
685
+
686
+ def merge(self, other):
687
+ """Merges TopoCoord from other into self's TopoCoord.
688
+
689
+ Args:
690
+ other (Molecule): another Molecule
691
+
692
+ Returns:
693
+ tuple: a shift tuple (returned by Coordinates.merge())
694
+ """
695
+ shifts = self.TopoCoord.merge(other.TopoCoord, self_cm=self.chain_manager, other_cm=other.chain_manager)
696
+ return shifts
697
+
698
+ def load_top_gro(self, topfilename, grofilename, tpxfilename='', mol2filename='', wrap_coords=False, ignore_bonds=False, overwrite_coordinates=False):
699
+ """Generates a new TopoCoord member object for this molecule by reading in a Gromacs topology file and a Gromacs gro file.
700
+
701
+ Args:
702
+ topfilename (str): Gromacs topology file
703
+ grofilename (str): Gromacs gro file
704
+ tpxfilename (str, optional): extended topology file, defaults to ''
705
+ mol2filename (str, optional): alternative coordinate mol2 file, defaults to ''
706
+ wrap_coords (bool): if True, wrap coordinates into the box after reading gro, defaults to False
707
+ ignore_bonds (bool): if True, skip reading bonds from mol2, defaults to False
708
+ overwrite_coordinates (bool): if True, overwrite existing coordinates when reading mol2, defaults to False
709
+ """
710
+ self.TopoCoord = TopoCoord(topfilename=topfilename, grofilename=grofilename, tpxfilename=tpxfilename, mol2filename=mol2filename, wrap_coords=wrap_coords, ignore_bonds=ignore_bonds, overwrite_coordinates=overwrite_coordinates)
711
+
712
+ def set_gro_attribute(self, attribute, srs):
713
+ """Sets attribute of atoms to srs (drills through to Coordinates.set_atomset_attributes()).
714
+
715
+ Args:
716
+ attribute (str): name of attribute
717
+ srs: scalar or list-like attribute values in same ordering as self.A
718
+ """
719
+ self.TopoCoord.set_gro_attribute(attribute, srs)
720
+
721
+ def read_gro_attributes(self, grxfilename, attribute_list=[]):
722
+ """Reads attributes from file into self.TopoCoord.Coordinates.A.
723
+
724
+ Args:
725
+ grxfilename (str): name of input file
726
+ attribute_list (list, optional): list of attributes to take, defaults to [] (take all)
727
+ """
728
+ self.TopoCoord.read_gro_attributes(grxfilename, attribute_list=attribute_list, chain_manager=self.chain_manager)
729
+
730
+ def write_gro_attributes(self, attribute_list, grxfilename):
731
+ """Writes atomic attributes to a file.
732
+
733
+ Args:
734
+ attribute_list (list): list of attributes to write
735
+ grxfilename (str): name of output file
736
+ """
737
+ self.TopoCoord.write_gro_attributes(attribute_list, grxfilename)
738
+
739
+ def make_bonds(self, bdf: pd.DataFrame, moldict, stage):
740
+ """Adds new bonds to the molecule's topology and deletes any sacrificial hydrogens.
741
+
742
+ Args:
743
+ bdf (pd.DataFrame): pandas dataframe identifying new bonds
744
+ moldict (dict): dictionary of available molecular templates
745
+ stage (reaction_stage): enumerated parameter indicating reaction_stage
746
+ """
747
+ TC = self.TopoCoord
748
+ explicit_sacrificial_Hs = {}
749
+ for i, r in bdf.iterrows():
750
+ aname, bname = [TC.get_gro_attribute_by_attributes('atomName', {'globalIdx': x}) for x in [r.ai, r.rj]]
751
+ logger.debug(f'generating {self.name} bond {r.ri}:{aname}:{r.ai}-{r.rj}:{bname}:{r.aj} order {r.order}')
752
+ if r.ri != r.rj:
753
+ resid_sets = TC.get_resid_sets([r.ai, r.aj])
754
+ hxi, hxj = self.transrot(r.ai, r.ri, r.aj, r.rj, connected_resids=resid_sets[1])
755
+ explicit_sacrificial_Hs[i] = [hxi, hxj]
756
+ if stage in [reaction_stage.cure, reaction_stage.param, reaction_stage.cap]:
757
+ template_source = 'ambertools'
758
+ else:
759
+ template_source = 'internal' # signals that a template molecule should be identified to parameterize this bond
760
+ TC.update_topology_and_coordinates(bdf, moldict, explicit_sacH=explicit_sacrificial_Hs, template_source=template_source, chain_manager=self.chain_manager)
761
+ self.initialize_molecule_rings()
762
+
763
+ def transrot(self, at_idx, at_resid, from_idx, from_resid, connected_resids=[]):
764
+ """Given a composite molecule, translates and rotates the piece downstream of the yet-to-be-created bond to minimize steric overlaps and identify the best two sacrificial hydrogens.
765
+
766
+ Args:
767
+ at_idx (int): global index of left-hand atom in new bond
768
+ at_resid (int): resid of left-hand residue
769
+ from_idx (int): global index of right-hand atom in new bond
770
+ from_resid (int): resid of right-hand residue
771
+ connected_resids (list, optional): list of all other resids attached to right-hand residue that should move with it, defaults to []
772
+
773
+ Returns:
774
+ tuple: 2-tuple containing global indices of the sacrificial hydrogens
775
+ """
776
+ # Rotate and translate
777
+ if at_resid == from_resid:
778
+ return # should never happen but JIC
779
+ logger.debug(f'{self.name} connected resids {connected_resids}')
780
+ TC = self.TopoCoord
781
+ ATC = TopoCoord()
782
+ BTC = TopoCoord()
783
+ C = TC.gro_DataFrame('atoms')
784
+ ATC.Coordinates.A = C[C['resNum'] == at_resid].copy()
785
+ bresids = connected_resids.copy()
786
+ bresids.append(from_resid)
787
+ BTC.Coordinates.A = C[C['resNum'].isin(bresids)].copy()
788
+ for ln in BTC.Coordinates.A.to_string().split('\n'): logger.debug(ln)
789
+ NONROT = C[~C['resNum'].isin(bresids)].shape[0]
790
+ logger.debug(f'{self.TopoCoord.Coordinates.A.shape[0]} atoms')
791
+ logger.debug(f'holding {at_resid} ({NONROT})')
792
+ logger.debug(f'rotating/translating {bresids} ({BTC.Coordinates.A.shape[0]})')
793
+ assert self.TopoCoord.Coordinates.A.shape[0] == (NONROT + BTC.Coordinates.A.shape[0])
794
+ mypartners = TC.Topology.bondlist.partners_of(at_idx)
795
+ otpartners = TC.Topology.bondlist.partners_of(from_idx)
796
+ logger.debug(f'Partners of {at_idx} {mypartners}')
797
+ logger.debug(f'Partners of {from_idx} {otpartners}')
798
+ myHpartners = {k: v for k, v in zip(mypartners, [C[C['globalIdx'] == i]['atomName'].values[0] for i in mypartners]) if v.startswith('H')}
799
+ otHpartners = {k: v for k, v in zip(otpartners, [C[C['globalIdx'] == i]['atomName'].values[0] for i in otpartners]) if v.startswith('H')}
800
+ myHighestH = {k: v for k, v in myHpartners.items() if v == max([k for k in myHpartners.values()], key=lambda x: int(x.split('H')[1] if x.split('H')[1] != '' else '0'))}
801
+ otHighestH = {k: v for k, v in otHpartners.items() if v == max([k for k in otHpartners.values()], key=lambda x: int(x.split('H')[1] if x.split('H')[1] != '' else '0'))}
802
+ assert len(myHighestH) == 1
803
+ assert len(otHighestH) == 1
804
+ logger.debug(f'Highest-named H partner of {at_idx} is {myHighestH}')
805
+ logger.debug(f'Highest-named H partner of {from_idx} is {otHighestH}')
806
+ assert len(myHpartners) > 0, f'Error: atom {at_idx} does not have a deletable H atom!'
807
+ assert len(otHpartners) > 0, f'Error: atom {from_idx} does not have a deletable H atom!'
808
+
809
+ Ri = TC.get_R(at_idx)
810
+ Rj = TC.get_R(from_idx)
811
+ logger.debug(f'Ri {at_idx} {Ri} {type(Ri)} {Ri.dtype}')
812
+ logger.debug(f'Rj {from_idx} {Rj} {type(Rj)} {Rj.dtype}')
813
+ overall_maximum = (-1.e9, -1, -1)
814
+ coord_trials = {}
815
+ for myH, myHnm in myHpartners.items(): # keys are globalIdx's, values are names
816
+ coord_trials[myH] = {}
817
+ Rh = TC.get_R(myH)
818
+ logger.debug(f' Rh {myH} {Rh} {Rh.dtype}')
819
+ Rih = Ri - Rh
820
+ Rih *= 1.0 / np.linalg.norm(Rih)
821
+ for otH, otHnm in otHpartners.items():
822
+ logger.debug(f'{self.name}: Considering {myH} {otH}')
823
+ coord_trials[myH][otH] = deepcopy(BTC)
824
+ # logger.debug(f'\n{coord_trials[myH][otH].Coordinates.A.to_string()}')
825
+ Rk = coord_trials[myH][otH].get_R(otH)
826
+ logger.debug(f'{self.name}: otH {otH} Rk {Rk} {Rk.dtype}')
827
+ Rkj = Rk - Rj
828
+ Rkj *= 1.0 / np.linalg.norm(Rkj)
829
+ logger.debug(f'Rkj {Rkj} {Rkj.dtype} Rih {Rih} {Rih.dtype}')
830
+ #Rhk=Rh-Rk
831
+ #rhk=np.linalg.norm(Rhk)
832
+ cp = np.cross(Rkj,Rih)
833
+ c = np.dot(Rkj,Rih)
834
+ v = np.array([[0,-cp[2],cp[1]],[cp[2],0,-cp[0]],[-cp[1],cp[0],0]])
835
+ v2 = np.dot(v,v)
836
+ I = np.array([[1.,0.,0.],[0.,1.,0.],[0.,0.,1.]])
837
+ # R is the rotation matrix that will rotate donb to align with accb
838
+ R = I + v + v2 / (1. + c)
839
+ # logger.debug(f'{self.name}: R:\n{R}')
840
+ # rotate translate all donor atoms!
841
+ coord_trials[myH][otH].rotate(R)
842
+ Rk = coord_trials[myH][otH].get_R(otH)
843
+ # overlap the two H atoms by translation
844
+ Rik = Rh - Rk
845
+ coord_trials[myH][otH].translate(Rik)
846
+ minD = TC.minimum_distance(coord_trials[myH][otH], self_excludes=[myH], other_excludes=[otH])
847
+ logger.debug(f'{self.name}: minD {minD}')
848
+ if minD > overall_maximum[0]:
849
+ overall_maximum = (minD, myH, otH)
850
+ logger.debug(f'{self.name}: overall_maximum {overall_maximum}')
851
+ minD, myH, otH = overall_maximum
852
+ BTC = coord_trials[myH][otH]
853
+ TC.overwrite_coords(BTC)
854
+ TC.swap_atom_names(myH, list(myHighestH.keys())[0])
855
+ TC.swap_atom_names(otH, list(otHighestH.keys())[0])
856
+ return myH, otH
857
+
858
+ def atoms_w_same_attribute_as(self, find_dict={}, same_attribute='', return_attribute=''):
859
+ """Returns a list of atom attributes named in return_attribute from atoms that share an attribute named in same_attribute with the atom identified by find_dict.
860
+
861
+ Args:
862
+ find_dict (dict, optional): dictionary of attribute:value pairs that should uniquely identify an atom, defaults to {}
863
+ same_attribute (str, optional): name of attribute used to screen atoms, defaults to ''
864
+ return_attribute (str, optional): attribute value to return a list of from the screened atoms, defaults to ''
865
+
866
+ Returns:
867
+ list: list of attribute values
868
+ """
869
+ att_val = self.TopoCoord.get_gro_attribute_by_attributes(same_attribute, find_dict)
870
+ return self.TopoCoord.get_gro_attributelist_by_attributes(return_attribute, {same_attribute: att_val})
871
+
872
+ def flip_stereocenters(self, idxlist):
873
+ """Flips stereochemistry of atoms in idxlist.
874
+
875
+ Args:
876
+ idxlist (list): global indices of chiral atoms
877
+ """
878
+ self.TopoCoord.flip_stereocenters(idxlist)
879
+
880
+ def rotate_bond(self, a, b, deg):
881
+ """Rotates all atoms in molecule on b-side of a-b bond by deg degrees.
882
+
883
+ Args:
884
+ a (int): index of a
885
+ b (int): index of b
886
+ deg (float): angle of rotation in degrees
887
+ """
888
+ TC = self.TopoCoord
889
+ A = TC.Coordinates.A
890
+ branch = TopoCoord()
891
+ bl = deepcopy(self.TopoCoord.Topology.bondlist)
892
+ branchidx = bl.half_as_list((a, b), 99)
893
+ ra = TC.get_R(a)
894
+ rb = TC.get_R(b)
895
+ Rab = ra - rb
896
+ rab = Rab / np.linalg.norm(Rab)
897
+ O = rb
898
+ TC.translate(-1 * O)
899
+ branch.Coordinates.A = A[A['globalIdx'].isin(branchidx)].copy()
900
+ # do stuff
901
+ rx, ry, rz = rab
902
+ az = np.arctan2(ry, rx) # azimuthal angle of bond axis (radians)
903
+ ay = np.arctan2(np.sqrt(rx**2 + ry**2), rz) # polar angle of bond axis (radians)
904
+ M = (Matrix4()
905
+ .rot(np.degrees(az), 'z')
906
+ .rot(np.degrees(ay), 'y')
907
+ .rot(deg, 'z')
908
+ .rot(np.degrees(-ay), 'y')
909
+ .rot(np.degrees(-az), 'z'))
910
+ branch.rotate(M.m[:3,:3])
911
+ bdf = branch.Coordinates.A
912
+ A.loc[A['globalIdx'].isin(branchidx), ['posX', 'posY', 'posZ']] = bdf[['posX', 'posY', 'posZ']]
913
+ TC.translate(O)
914
+
915
+ # def sea_of(self,idx):
916
+ # clu=self.atoms_w_same_attribute_as(find_dict={'globalIdx':idx},
917
+ # same_attribute='sea_idx',
918
+ # return_attribute='globalIdx')
919
+ # return list(clu)
920
+
921
+ def generate_stereoisomers(self):
922
+ """Generates list of Molecule shells, one for each stereoisomer.
923
+
924
+ Returns:
925
+ None: returns early if no stereoisomers need to be generated
926
+ """
927
+ if self.TopoCoord.Topology.D['atoms'].shape[0] == 0: return # self has not yet acquired topology/coordinates
928
+ if len(self.stereoisomers) == 0: return
929
+ flip = [[0, 1] for _ in range(len(self.stereocenters))]
930
+ st_idx = [self.TopoCoord.get_gro_attribute_by_attributes('globalIdx', {'atomName': n}) for n in self.stereocenters]
931
+ P = product(*flip)
932
+ next(P) # one with no flips is the original molecule, so skip it
933
+ for p in P:
934
+ si_name = self.name + '-S' + ''.join([str(_) for _ in p])
935
+ if not si_name in self.stereoisomers:
936
+ logger.debug(f'{si_name} not found in dict of stereoisomers of {self.name}')
937
+ logger.debug(f'Stereocenter sequence {p} generates stereoisomer {si_name}')
938
+ M = self.stereoisomers[si_name]
939
+ M.origin = self.origin
940
+ M.TopoCoord = deepcopy(self.TopoCoord)
941
+ fsc = [st_idx[i] for i in range(len(self.stereocenters)) if p[i]]
942
+ M.flip_stereocenters(fsc)
943
+ M.TopoCoord.write_gro(f'{si_name}.gro')
944
+
945
+ def generate_conformers(self):
946
+ """generate_conformers generates this molecule's conformer instances using either gromacs or obabel
947
+ """
948
+ # only generates gro files
949
+ default_gromacs_params = {'ensemble': 'nvt', 'temperature': 600, 'ps': 100, 'begin_at': 50}
950
+ # if self.nconformers==0: return
951
+ cd = self.conformers_dict
952
+ if not cd: return
953
+ logger.debug(f'{self.name} conformer_dict {cd}')
954
+ self.nconformers = cd['count']
955
+ minimize = cd.get('minimize', False)
956
+ generator = cd.get('generator', {})
957
+ if generator == 'obabel' and not minimize:
958
+ logger.debug(f'Confomers generated by obabel should be energy-minimized. Indicate "mimimize: True" in the confomers directive for {self.name}')
959
+ minimize = True
960
+ if not generator: return
961
+ logger.info(f'Generating {self.nconformers*(1+len(self.stereoisomers))} conformers for {self.name}')
962
+ gronames = [f'{self.name}']
963
+ nd = cd.get('nzeros', 2)
964
+ for si in self.stereoisomers:
965
+ gronames.append(f'{si}')
966
+ for gro in gronames:
967
+ pfx = f'{gro}-C'
968
+ if generator['name'] == 'obabel':
969
+ compfile = f'{gro}-obabel-confs.gro'
970
+ run(f'obabel -igro {gro}.gro -O {compfile} --conformer --nconf {self.nconformers} --writeconformers')
971
+ out,err = run(f'wc -l {compfile}')
972
+ tok = out.split()
973
+ lpf = int(tok[0]) // self.nconformers
974
+ run(f'split -d -n {nd} -l {lpf} {compfile} {pfx} --additional-suffix=".gro"')
975
+ elif generator['name'] == 'gromacs':
976
+ params = generator.get('params', default_gromacs_params)
977
+ begin_at = params.get('begin_at', 0.0)
978
+ compfile = f'{gro}-gromacs-confs'
979
+ TC = self.TopoCoord if gro == self.name else self.stereoisomers[gro].TopoCoord
980
+ TC.vacuum_simulate(outname=f'{compfile}', nsamples=cd['count'], params=params)
981
+ gro_from_trr(compfile, nzero=nd, outpfx=pfx, b=begin_at)
982
+ # os.remove(f'{gro}-confs.gro')
983
+ fmt = r'{A}{B:0'+str(nd) + r'd}' # the trjconv command in gro_from_trr must generate these files
984
+ cfnl = [fmt.format(A=pfx,B=x) for x in range(self.nconformers)]
985
+ for mname in cfnl:
986
+ assert os.path.exists(f'{mname}.gro'), f'Error: Conformer coordinates file {mname}.gro not found'
987
+ logger.debug(f'Conformer coordinate filenames {cfnl}')
988
+ self.conformers.extend(cfnl)
989
+ if minimize:
990
+ saveTC = deepcopy(self.TopoCoord)
991
+ for mname in self.conformers:
992
+ self.TopoCoord.read_gro(f'{mname}.gro')
993
+ logger.info(f'Minimizing conformer {mname}')
994
+ self.TopoCoord.vacuum_minimize(outname=f'{mname}')
995
+ self.TopoCoord = saveTC
996
+
997
+ MoleculeDict = dict[str,Molecule]
998
+ MoleculeList = list[Molecule]