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,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]
|