firecode 1.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.
- firecode/TEST_NOTEBOOK.ipynb +3940 -0
- firecode/__init__.py +0 -0
- firecode/__main__.py +118 -0
- firecode/_gaussian.py +97 -0
- firecode/algebra.py +405 -0
- firecode/ase_manipulations.py +879 -0
- firecode/atropisomer_module.py +516 -0
- firecode/automep.py +130 -0
- firecode/calculators/__init__.py +29 -0
- firecode/calculators/_gaussian.py +98 -0
- firecode/calculators/_mopac.py +242 -0
- firecode/calculators/_openbabel.py +154 -0
- firecode/calculators/_orca.py +129 -0
- firecode/calculators/_xtb.py +786 -0
- firecode/concurrent_test.py +119 -0
- firecode/embedder.py +2590 -0
- firecode/embedder_options.py +577 -0
- firecode/embeds.py +881 -0
- firecode/errors.py +65 -0
- firecode/graph_manipulations.py +333 -0
- firecode/hypermolecule_class.py +364 -0
- firecode/mep_relaxer.py +199 -0
- firecode/modify_settings.py +186 -0
- firecode/mprof.py +65 -0
- firecode/multiembed.py +148 -0
- firecode/nci.py +186 -0
- firecode/numba_functions.py +260 -0
- firecode/operators.py +776 -0
- firecode/optimization_methods.py +609 -0
- firecode/parameters.py +84 -0
- firecode/pka.py +275 -0
- firecode/profiler.py +17 -0
- firecode/pruning.py +421 -0
- firecode/pt.py +32 -0
- firecode/quotes.json +6651 -0
- firecode/quotes.py +9 -0
- firecode/reactive_atoms_classes.py +666 -0
- firecode/references.py +11 -0
- firecode/rmsd.py +74 -0
- firecode/settings.py +75 -0
- firecode/solvents.py +126 -0
- firecode/tests/C2F2H4.xyz +10 -0
- firecode/tests/C2H4.xyz +8 -0
- firecode/tests/CH3Cl.xyz +7 -0
- firecode/tests/HCOOH.xyz +7 -0
- firecode/tests/HCOOOH.xyz +8 -0
- firecode/tests/chelotropic.txt +3 -0
- firecode/tests/cyclical.txt +3 -0
- firecode/tests/dihedral.txt +2 -0
- firecode/tests/string.txt +3 -0
- firecode/tests/trimolecular.txt +9 -0
- firecode/tests.py +151 -0
- firecode/torsion_module.py +1035 -0
- firecode/utils.py +541 -0
- firecode-1.0.0.dist-info/LICENSE +165 -0
- firecode-1.0.0.dist-info/METADATA +321 -0
- firecode-1.0.0.dist-info/RECORD +59 -0
- firecode-1.0.0.dist-info/WHEEL +5 -0
- firecode-1.0.0.dist-info/top_level.txt +1 -0
firecode/embeds.py
ADDED
|
@@ -0,0 +1,881 @@
|
|
|
1
|
+
# coding=utf-8
|
|
2
|
+
'''
|
|
3
|
+
FIRECODE: Filtering Refiner and Embedder for Conformationally Dense Ensembles
|
|
4
|
+
Copyright (C) 2021-2024 Nicolò Tampellini
|
|
5
|
+
|
|
6
|
+
SPDX-License-Identifier: LGPL-3.0-or-later
|
|
7
|
+
|
|
8
|
+
This program is free software: you can redistribute it and/or modify
|
|
9
|
+
it under the terms of the GNU Lesser General Public License as published by
|
|
10
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
11
|
+
(at your option) any later version.
|
|
12
|
+
|
|
13
|
+
This program is distributed in the hope that it will be useful,
|
|
14
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
15
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
16
|
+
GNU Lesser General Public License for more details.
|
|
17
|
+
|
|
18
|
+
You should have received a copy of the GNU Lesser General Public License
|
|
19
|
+
along with this program. If not, see
|
|
20
|
+
https://www.gnu.org/licenses/lgpl-3.0.en.html#license-text.
|
|
21
|
+
|
|
22
|
+
'''
|
|
23
|
+
|
|
24
|
+
from copy import deepcopy
|
|
25
|
+
|
|
26
|
+
import numpy as np
|
|
27
|
+
|
|
28
|
+
from firecode.algebra import (align_vec_pair, norm, rot_mat_from_pointer,
|
|
29
|
+
vec_angle)
|
|
30
|
+
from firecode.ase_manipulations import ase_bend
|
|
31
|
+
from firecode.errors import TriangleError, ZeroCandidatesError
|
|
32
|
+
from firecode.graph_manipulations import get_sum_graph
|
|
33
|
+
from firecode.numba_functions import (compenetration_check,
|
|
34
|
+
get_torsion_fingerprint, tfd_similarity)
|
|
35
|
+
from firecode.rmsd import _rmsd_similarity
|
|
36
|
+
from firecode.torsion_module import _get_quadruplets
|
|
37
|
+
from firecode.utils import (cartesian_product, loadbar, polygonize, pretty_num,
|
|
38
|
+
rotation_matrix_from_vectors)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def string_embed(embedder):
|
|
42
|
+
'''
|
|
43
|
+
return poses: return embedded structures, ready to be refined
|
|
44
|
+
Algorithm used is the "string" algorithm (see docs).
|
|
45
|
+
'''
|
|
46
|
+
assert len(embedder.objects) == 2
|
|
47
|
+
|
|
48
|
+
def is_new_structure(coords, quadruplets, lru_cache, cache_size=5):
|
|
49
|
+
'''
|
|
50
|
+
Checks if the torsion fingerprint of a structure is
|
|
51
|
+
similar to the ones present in lru_cache. If the
|
|
52
|
+
structure is new, updates the cache.
|
|
53
|
+
'''
|
|
54
|
+
|
|
55
|
+
# get the structure torsion fingerprint
|
|
56
|
+
tfp = get_torsion_fingerprint(coords, quadruplets)
|
|
57
|
+
|
|
58
|
+
# compare it to the ones in lru_cache
|
|
59
|
+
for ref_tfp in lru_cache:
|
|
60
|
+
if tfd_similarity(tfp, ref_tfp):
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
# if it is different than all of them, add it to cache
|
|
64
|
+
lru_cache.append(tfp)
|
|
65
|
+
|
|
66
|
+
# update cache if that is too big
|
|
67
|
+
if len(lru_cache) == cache_size + 1:
|
|
68
|
+
lru_cache = lru_cache[1:]
|
|
69
|
+
|
|
70
|
+
return True
|
|
71
|
+
|
|
72
|
+
embedder.log(f'\n--> Performing string embed ({pretty_num(embedder.candidates)} candidates)')
|
|
73
|
+
|
|
74
|
+
conf_number = [len(mol.atomcoords) for mol in embedder.objects]
|
|
75
|
+
conf_indices = cartesian_product(*[np.array(range(i)) for i in conf_number])
|
|
76
|
+
# (n,2) vectors where the every element is the conformer index for that molecule
|
|
77
|
+
|
|
78
|
+
r_atoms_centers_indices = cartesian_product(*[np.array(range(len(mol.get_centers(0)[0]))) for mol in embedder.objects])
|
|
79
|
+
# for two mols with 3 and 2 centers: [[0 0][0 1][1 0][1 1][2 0][2 1]]
|
|
80
|
+
|
|
81
|
+
# explicit individual molecules
|
|
82
|
+
mol1, mol2 = embedder.objects
|
|
83
|
+
|
|
84
|
+
# get quadruplets needed for the tfd similarity check
|
|
85
|
+
constrained_indices = [[int(embedder.objects[0].reactive_indices[0]),
|
|
86
|
+
int(embedder.objects[1].reactive_indices[0] + embedder.ids[0])]]
|
|
87
|
+
|
|
88
|
+
quadruplets = _get_quadruplets(get_sum_graph((mol1.graph, mol2.graph), constrained_indices))
|
|
89
|
+
|
|
90
|
+
lru_cache = []
|
|
91
|
+
poses = []
|
|
92
|
+
for i, (c1, c2) in enumerate(conf_indices):
|
|
93
|
+
|
|
94
|
+
loadbar(i, len(conf_indices),
|
|
95
|
+
prefix='Embedding structures ',
|
|
96
|
+
suffix=f'({len(poses)} found)')
|
|
97
|
+
|
|
98
|
+
for ai1, ai2 in r_atoms_centers_indices:
|
|
99
|
+
for angle in embedder.systematic_angles:
|
|
100
|
+
|
|
101
|
+
ra1 = mol1.get_r_atoms(c1)[0]
|
|
102
|
+
ra2 = mol2.get_r_atoms(c2)[0]
|
|
103
|
+
|
|
104
|
+
p1 = ra1.center[ai1]
|
|
105
|
+
p2 = ra2.center[ai2]
|
|
106
|
+
ref_vec = ra1.orb_vecs[ai1]
|
|
107
|
+
mol_vec = ra2.orb_vecs[ai2]
|
|
108
|
+
|
|
109
|
+
mol2.rotation = rotation_matrix_from_vectors(mol_vec, -ref_vec)
|
|
110
|
+
|
|
111
|
+
if angle != 0:
|
|
112
|
+
delta_rot = rot_mat_from_pointer(ref_vec, angle)
|
|
113
|
+
mol2.rotation = delta_rot @ mol2.rotation
|
|
114
|
+
|
|
115
|
+
mol2.position = p1 - mol2.rotation @ p2
|
|
116
|
+
|
|
117
|
+
embedded_structure = get_embed((mol1, mol2), (c1, c2))
|
|
118
|
+
|
|
119
|
+
if compenetration_check(embedded_structure, ids=embedder.ids, thresh=embedder.options.clash_thresh):
|
|
120
|
+
if is_new_structure(embedded_structure, quadruplets, lru_cache):
|
|
121
|
+
poses.append(embedded_structure)
|
|
122
|
+
|
|
123
|
+
loadbar(1, 1, prefix='Embedding structures ')
|
|
124
|
+
|
|
125
|
+
if not poses:
|
|
126
|
+
s = ('\n--> Cyclical embed did not find any suitable disposition of molecules.\n' +
|
|
127
|
+
' This is probably because the two molecules cannot find a correct interlocking pose.\n' +
|
|
128
|
+
' Try expanding the conformational space with the csearch> operator or see the SHRINK keyword.')
|
|
129
|
+
embedder.log(s, p=False)
|
|
130
|
+
raise ZeroCandidatesError(s)
|
|
131
|
+
|
|
132
|
+
embedder.constrained_indices = _get_string_constrained_indices(embedder, len(poses))
|
|
133
|
+
|
|
134
|
+
return np.array(poses)
|
|
135
|
+
|
|
136
|
+
def _get_string_constrained_indices(embedder, n):
|
|
137
|
+
'''
|
|
138
|
+
Get constrained indices referring to the transition states, repeated n times.
|
|
139
|
+
:params n: int
|
|
140
|
+
:return: list of lists consisting in atomic pairs to be constrained.
|
|
141
|
+
'''
|
|
142
|
+
# Two molecules, string algorithm, one constraint for all, repeated n times
|
|
143
|
+
return np.array([[[int(embedder.objects[0].reactive_indices[0]),
|
|
144
|
+
int(embedder.objects[1].reactive_indices[0] + embedder.ids[0])]] for _ in range(n)])
|
|
145
|
+
|
|
146
|
+
def cyclical_embed(embedder, max_norm_delta=5):
|
|
147
|
+
'''
|
|
148
|
+
return threads: return embedded structures, with position and rotation attributes set, ready to be pumped
|
|
149
|
+
into embedder.structures. Algorithm used is the "cyclical" algorithm (see docs).
|
|
150
|
+
'''
|
|
151
|
+
|
|
152
|
+
if len(embedder.objects) == 2 and embedder.options.rigid:
|
|
153
|
+
return _fast_bimol_rigid_cyclical_embed(embedder, max_norm_delta=max_norm_delta)
|
|
154
|
+
# shortened, simplified version that is somewhat faster
|
|
155
|
+
|
|
156
|
+
def _get_directions(norms):
|
|
157
|
+
'''
|
|
158
|
+
Returns two or three vectors specifying the direction in which each molecule should be aligned
|
|
159
|
+
in the cyclical TS, pointing towards the center of the polygon.
|
|
160
|
+
'''
|
|
161
|
+
assert len(norms) in (2,3)
|
|
162
|
+
|
|
163
|
+
if len(norms) == 2:
|
|
164
|
+
return np.array([[0, 1,0],
|
|
165
|
+
[0,-1,0]])
|
|
166
|
+
|
|
167
|
+
vertices = np.zeros((3,2))
|
|
168
|
+
|
|
169
|
+
vertices[1] = np.array([norms[0],0])
|
|
170
|
+
|
|
171
|
+
a = np.power(norms[0], 2)
|
|
172
|
+
b = np.power(norms[1], 2)
|
|
173
|
+
c = np.power(norms[2], 2)
|
|
174
|
+
x = (a-b+c)/(2*a**0.5)
|
|
175
|
+
y = (c-x**2)**0.5
|
|
176
|
+
|
|
177
|
+
vertices[2] = np.array([x,y])
|
|
178
|
+
# similar to the code from polygonize, to get the active triangle
|
|
179
|
+
# but without the orientation specified in the polygonize function
|
|
180
|
+
|
|
181
|
+
a = vertices[1,0] # first point, x
|
|
182
|
+
b = vertices[2,0] # second point, x
|
|
183
|
+
c = vertices[2,1] # second point, y
|
|
184
|
+
|
|
185
|
+
x = a/2
|
|
186
|
+
y = (b**2 + c**2 - a*b)/(2*c)
|
|
187
|
+
cc = np.array([x,y])
|
|
188
|
+
# 2D coordinates of the triangle circocenter
|
|
189
|
+
|
|
190
|
+
v0, v1, v2 = vertices
|
|
191
|
+
|
|
192
|
+
meanpoint1 = np.mean((v0,v1), axis=0)
|
|
193
|
+
meanpoint2 = np.mean((v1,v2), axis=0)
|
|
194
|
+
meanpoint3 = np.mean((v2,v0), axis=0)
|
|
195
|
+
|
|
196
|
+
dir1 = cc - meanpoint1
|
|
197
|
+
dir2 = cc - meanpoint2
|
|
198
|
+
dir3 = cc - meanpoint3
|
|
199
|
+
# 2D direction versors connecting center of side with circumcenter.
|
|
200
|
+
# Now we need to understand if we want these or their negative
|
|
201
|
+
|
|
202
|
+
if np.any([np.all(d == 0) for d in (dir1, dir2, dir3)]):
|
|
203
|
+
# We have a right triangle. To aviod numerical
|
|
204
|
+
# errors, a small perturbation is made.
|
|
205
|
+
# This should not happen, but just in case...
|
|
206
|
+
norms[0] += 1e-5
|
|
207
|
+
dir1, dir2, dir3 = [t[:-1] for t in _get_directions(norms)]
|
|
208
|
+
|
|
209
|
+
angle0_obtuse = (vec_angle(v1-v0, v2-v0) > 90)
|
|
210
|
+
angle1_obtuse = (vec_angle(v0-v1, v2-v1) > 90)
|
|
211
|
+
angle2_obtuse = (vec_angle(v0-v2, v1-v2) > 90)
|
|
212
|
+
|
|
213
|
+
dir1 = -dir1 if angle2_obtuse else dir1
|
|
214
|
+
dir2 = -dir2 if angle0_obtuse else dir2
|
|
215
|
+
dir3 = -dir3 if angle1_obtuse else dir3
|
|
216
|
+
# invert the versors sign of circumcenter if
|
|
217
|
+
# one angle is obtuse, because then
|
|
218
|
+
# circumcenter is outside the triangle
|
|
219
|
+
|
|
220
|
+
dir1 = norm(np.concatenate((dir1, [0])))
|
|
221
|
+
dir2 = norm(np.concatenate((dir2, [0])))
|
|
222
|
+
dir3 = norm(np.concatenate((dir3, [0])))
|
|
223
|
+
|
|
224
|
+
return np.vstack((dir1, dir2, dir3))
|
|
225
|
+
|
|
226
|
+
def _adjust_directions(embedder, directions, constrained_indices, triangle_vectors, pivots, conf_ids):
|
|
227
|
+
'''
|
|
228
|
+
For trimolecular TSs, correct molecules pre-alignment. That is, after the initial estimate
|
|
229
|
+
based on pivot triangle circocentrum, systematically rotate each molecule around its pivot
|
|
230
|
+
by fixed increments and look for the arrangement with the smallest deviation from orbital
|
|
231
|
+
parallel interaction. This optimizes the obtainment of poses with the correct inter-reactive
|
|
232
|
+
atoms distances.
|
|
233
|
+
|
|
234
|
+
'''
|
|
235
|
+
assert directions.shape[0] == 3
|
|
236
|
+
|
|
237
|
+
mols = deepcopy(embedder.objects)
|
|
238
|
+
p0, p1, p2 = [end - start for start, end in triangle_vectors]
|
|
239
|
+
p0_mean, p1_mean, p2_mean = [np.mean((end, start), axis=0) for start, end in triangle_vectors]
|
|
240
|
+
|
|
241
|
+
############### get triangle vertices
|
|
242
|
+
|
|
243
|
+
vertices = np.zeros((3,2))
|
|
244
|
+
vertices[1] = np.array([norms[0],0])
|
|
245
|
+
|
|
246
|
+
a = np.power(norms[0], 2)
|
|
247
|
+
b = np.power(norms[1], 2)
|
|
248
|
+
c = np.power(norms[2], 2)
|
|
249
|
+
x = (a-b+c)/(2*a**0.5)
|
|
250
|
+
y = (c-x**2)**0.5
|
|
251
|
+
|
|
252
|
+
vertices[2] = np.array([x,y])
|
|
253
|
+
# similar to the code from polygonize, to get the active triangle
|
|
254
|
+
# but without the orientation specified in the polygonize function
|
|
255
|
+
|
|
256
|
+
a = vertices[1,0] # first point, x
|
|
257
|
+
b = vertices[2,0] # second point, x
|
|
258
|
+
c = vertices[2,1] # second point, y
|
|
259
|
+
|
|
260
|
+
x = a/2
|
|
261
|
+
y = (b**2 + c**2 - a*b)/(2*c)
|
|
262
|
+
# cc = np.array([x,y])
|
|
263
|
+
# 2D coordinates of the triangle circocenter
|
|
264
|
+
|
|
265
|
+
v0, v1, v2 = vertices
|
|
266
|
+
|
|
267
|
+
v0 = np.concatenate((v0, [0]))
|
|
268
|
+
v1 = np.concatenate((v1, [0]))
|
|
269
|
+
v2 = np.concatenate((v2, [0]))
|
|
270
|
+
|
|
271
|
+
############### set up mols -> pos + rot
|
|
272
|
+
|
|
273
|
+
for i in (0,1,2):
|
|
274
|
+
|
|
275
|
+
start, end = triangle_vectors[i]
|
|
276
|
+
|
|
277
|
+
mol_direction = pivots[i].meanpoint - np.mean(embedder.objects[i].atomcoords[conf_ids[i]][embedder.objects[i].reactive_indices], axis=0)
|
|
278
|
+
if np.all(mol_direction == 0.):
|
|
279
|
+
mol_direction = pivots[i].meanpoint
|
|
280
|
+
|
|
281
|
+
mols[i].rotation = align_vec_pair(np.array([end-start, directions[i]]),
|
|
282
|
+
np.array([pivots[i].pivot, mol_direction]))
|
|
283
|
+
mols[i].position = np.mean(triangle_vectors[i], axis=0) - mols[i].rotation @ pivots[i].meanpoint
|
|
284
|
+
|
|
285
|
+
############### set up pairings between reactive atoms
|
|
286
|
+
|
|
287
|
+
pairings = [[None, None] for _ in constrained_indices]
|
|
288
|
+
for i, c in enumerate(constrained_indices):
|
|
289
|
+
for m, mol in enumerate(embedder.objects):
|
|
290
|
+
for index, r_atom in mol.reactive_atoms_classes_dict[0].items():
|
|
291
|
+
if r_atom.cumnum == c[0]:
|
|
292
|
+
pairings[i][0] = (m, index)
|
|
293
|
+
if r_atom.cumnum == c[1]:
|
|
294
|
+
pairings[i][1] = (m, index)
|
|
295
|
+
|
|
296
|
+
r = np.zeros((3,3), dtype=int)
|
|
297
|
+
|
|
298
|
+
for first, second in pairings:
|
|
299
|
+
mol_index = first[0]
|
|
300
|
+
partner_index = second[0]
|
|
301
|
+
reactive_index = first[1]
|
|
302
|
+
r[mol_index, partner_index] = reactive_index
|
|
303
|
+
|
|
304
|
+
mol_index = second[0]
|
|
305
|
+
partner_index = first[0]
|
|
306
|
+
reactive_index = second[1]
|
|
307
|
+
r[mol_index, partner_index] = reactive_index
|
|
308
|
+
|
|
309
|
+
# r[0,1] is the reactive_index of molecule 0 that faces molecule 1 and so on
|
|
310
|
+
# diagonal of r (r[0,0], r[1,1], r[2,2]) is just unused
|
|
311
|
+
|
|
312
|
+
############### calculate reactive atoms positions
|
|
313
|
+
|
|
314
|
+
mol0, mol1, mol2 = mols
|
|
315
|
+
|
|
316
|
+
a01 = mol0.rotation @ mol0.atomcoords[0][r[0,1]] + mol0.position
|
|
317
|
+
a02 = mol0.rotation @ mol0.atomcoords[0][r[0,2]] + mol0.position
|
|
318
|
+
|
|
319
|
+
a10 = mol1.rotation @ mol1.atomcoords[0][r[1,0]] + mol1.position
|
|
320
|
+
a12 = mol1.rotation @ mol1.atomcoords[0][r[1,2]] + mol1.position
|
|
321
|
+
|
|
322
|
+
a20 = mol2.rotation @ mol2.atomcoords[0][r[2,0]] + mol2.position
|
|
323
|
+
a21 = mol2.rotation @ mol2.atomcoords[0][r[2,1]] + mol2.position
|
|
324
|
+
|
|
325
|
+
############### explore all angles combinations
|
|
326
|
+
|
|
327
|
+
steps = 6
|
|
328
|
+
angle_range = 30
|
|
329
|
+
step_angle = 2*angle_range/steps
|
|
330
|
+
angles_list = cartesian_product(*[range(steps+1) for _ in range(3)]) * step_angle - angle_range
|
|
331
|
+
# Molecules are rotated around the +angle_range/-angle_range range in the given number of steps.
|
|
332
|
+
# Therefore, the angular resolution between candidates is step_angle (10 degrees)
|
|
333
|
+
|
|
334
|
+
candidates = []
|
|
335
|
+
for angles in angles_list:
|
|
336
|
+
|
|
337
|
+
rot0 = rot_mat_from_pointer(p0, angles[0])
|
|
338
|
+
new_a01 = rot0 @ a01
|
|
339
|
+
new_a02 = rot0 @ a02
|
|
340
|
+
d0 = p0_mean - np.mean((new_a01, new_a02), axis=0)
|
|
341
|
+
|
|
342
|
+
rot1 = rot_mat_from_pointer(p1, angles[1])
|
|
343
|
+
new_a10 = rot1 @ a10
|
|
344
|
+
new_a12 = rot1 @ a12
|
|
345
|
+
d1 = p1_mean - np.mean((new_a10, new_a12), axis=0)
|
|
346
|
+
|
|
347
|
+
rot2 = rot_mat_from_pointer(p2, angles[2])
|
|
348
|
+
new_a20 = rot2 @ a20
|
|
349
|
+
new_a21 = rot2 @ a21
|
|
350
|
+
d2 = p2_mean - np.mean((new_a20, new_a21), axis=0)
|
|
351
|
+
|
|
352
|
+
cost = 0
|
|
353
|
+
cost += vec_angle(v0 - new_a02, new_a20 - v0)
|
|
354
|
+
cost += vec_angle(v1 - new_a01, new_a10 - v1)
|
|
355
|
+
cost += vec_angle(v2 - new_a21, new_a12 - v2)
|
|
356
|
+
|
|
357
|
+
candidates.append((cost, angles, (d0, d1, d2)))
|
|
358
|
+
|
|
359
|
+
############### choose the one with the best alignment, that is minor cost
|
|
360
|
+
|
|
361
|
+
cost, angles, directions = sorted(candidates, key=lambda x: x[0])[0]
|
|
362
|
+
|
|
363
|
+
return np.array(directions)
|
|
364
|
+
|
|
365
|
+
s = f'\n--> Performing {embedder.embed} embed ({pretty_num(embedder.candidates)} candidates)'
|
|
366
|
+
|
|
367
|
+
embedder.log(s)
|
|
368
|
+
|
|
369
|
+
if not embedder.options.rigid:
|
|
370
|
+
embedder.ase_bent_mols_dict = {}
|
|
371
|
+
# used as molecular cache for ase_bend
|
|
372
|
+
# keys are tuples with: ((identifier, pivot.index, target_pivot_length), obtained with:
|
|
373
|
+
# (np.sum(original_mol.atomcoords[0]), tuple(sorted(pivot.index)), round(threshold,3))
|
|
374
|
+
|
|
375
|
+
# if not embedder.options.let:
|
|
376
|
+
# for mol in embedder.objects:
|
|
377
|
+
# if len(mol.atomcoords) > 10:
|
|
378
|
+
# mol.atomcoords = most_diverse_conformers(10, mol.atomcoords)
|
|
379
|
+
# embedder.log(f'Using only the most diverse 10 conformers of molecule {mol.filename} (override with LET keyword)')
|
|
380
|
+
# Do not keep more than 10 conformations, unless LET keyword is provided
|
|
381
|
+
|
|
382
|
+
conf_number = [len(mol.atomcoords) for mol in embedder.objects]
|
|
383
|
+
conf_indices = cartesian_product(*[np.array(range(i)) for i in conf_number])
|
|
384
|
+
|
|
385
|
+
poses = []
|
|
386
|
+
constrained_indices = []
|
|
387
|
+
for ci, conf_ids in enumerate(conf_indices):
|
|
388
|
+
|
|
389
|
+
pivots_indices = cartesian_product(*[range(len(mol.pivots[conf_ids[i]])) for i, mol in enumerate(embedder.objects)])
|
|
390
|
+
# indices of pivots in each molecule self.pivots[conf] list. For three mols with 2 pivots each: [[0,0,0], [0,0,1], [0,1,0], ...]
|
|
391
|
+
|
|
392
|
+
for p, pi in enumerate(pivots_indices):
|
|
393
|
+
|
|
394
|
+
loadbar(p+ci*(len(pivots_indices)), len(pivots_indices)*len(conf_indices), prefix='Embedding structures ')
|
|
395
|
+
|
|
396
|
+
pivots = [embedder.objects[m].pivots[conf_ids[m]][pi[m]] for m, _ in enumerate(embedder.objects)]
|
|
397
|
+
# getting the active pivot for each molecule for this run
|
|
398
|
+
|
|
399
|
+
norms = np.linalg.norm(np.array([p.pivot for p in pivots]), axis=1)
|
|
400
|
+
# getting the pivots norms to feed into the polygonize function
|
|
401
|
+
|
|
402
|
+
if len(norms) == 2:
|
|
403
|
+
|
|
404
|
+
if abs(norms[0] - norms[1]) < max_norm_delta:
|
|
405
|
+
norms_type = 'digon'
|
|
406
|
+
|
|
407
|
+
else:
|
|
408
|
+
norms_type = 'impossible_digon'
|
|
409
|
+
|
|
410
|
+
else:
|
|
411
|
+
if all([norms[i] < norms[i-1] + norms[i-2] for i in (0,1,2)]):
|
|
412
|
+
norms_type = 'triangle'
|
|
413
|
+
|
|
414
|
+
else:
|
|
415
|
+
norms_type = 'impossible_triangle'
|
|
416
|
+
|
|
417
|
+
if norms_type in ('triangle', 'digon'):
|
|
418
|
+
|
|
419
|
+
polygon_vectors = polygonize(norms)
|
|
420
|
+
|
|
421
|
+
elif norms_type == 'impossible_triangle':
|
|
422
|
+
# Accessed if we cannot build a triangle with the given norms.
|
|
423
|
+
# Try to bend the structure if it was close or just skip this triangle and go on.
|
|
424
|
+
|
|
425
|
+
deltas = [norms[i] - (norms[i-1] + norms[i-2]) for i in range(3)]
|
|
426
|
+
|
|
427
|
+
rel_delta = max([deltas[i]/norms[i] for i in range(3)])
|
|
428
|
+
# s = 'Rejected triangle, delta was %s, %s of side length' % (round(delta, 3), str(round(100*rel_delta, 3)) + ' %')
|
|
429
|
+
# embedder.log(s, p=False)
|
|
430
|
+
|
|
431
|
+
if rel_delta < 0.2 and not embedder.options.rigid:
|
|
432
|
+
# correct the molecule structure with the longest
|
|
433
|
+
# side if the distances are at most 20% off.
|
|
434
|
+
|
|
435
|
+
index = deltas.index(max(deltas))
|
|
436
|
+
mol = embedder.objects[index]
|
|
437
|
+
|
|
438
|
+
if tuple(sorted(mol.reactive_indices)) not in list(mol.graph.edges):
|
|
439
|
+
# do not try to bend molecules where the two reactive indices are bonded
|
|
440
|
+
|
|
441
|
+
pivot = pivots[index]
|
|
442
|
+
|
|
443
|
+
# ase_view(mol)
|
|
444
|
+
maxval = norms[index-1] + norms[index-2]
|
|
445
|
+
|
|
446
|
+
traj = f'bend_{mol.filename}_p{p}_tgt_{round(0.9*maxval, 3)}' if embedder.options.debug else None
|
|
447
|
+
|
|
448
|
+
bent_mol = ase_bend(embedder,
|
|
449
|
+
mol,
|
|
450
|
+
conf_ids[index],
|
|
451
|
+
pivot,
|
|
452
|
+
0.9*maxval,
|
|
453
|
+
title=f'{mol.rootname} - pivot {p}',
|
|
454
|
+
traj=traj
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
embedder.objects[index] = bent_mol
|
|
458
|
+
|
|
459
|
+
try:
|
|
460
|
+
pivots = [embedder.objects[m].pivots[conf_ids[m]][pi[m]] for m, _ in enumerate(embedder.objects)]
|
|
461
|
+
# updating the active pivot for each molecule for this run
|
|
462
|
+
except IndexError:
|
|
463
|
+
raise Exception((f'The number of pivots for molecule {index} ({bent_mol.filename}) most likely decreased during ' +
|
|
464
|
+
'its bending, causing this error. Adding the RIGID (and maybe also SHRINK) keyword to the ' +
|
|
465
|
+
'input file should solve the issue. I do not think this should ever happen under common ' +
|
|
466
|
+
'circumstances, but if it does, it may be reasonable to print a statement on the log, ' +
|
|
467
|
+
'discard the bent molecule, and then proceed with the embed. If you see this error, ' +
|
|
468
|
+
'please report your input and structures on a GitHub issue. Thank you.'))
|
|
469
|
+
|
|
470
|
+
norms = np.linalg.norm(np.array([p.pivot for p in pivots]), axis=1)
|
|
471
|
+
# updating the pivots norms to feed into the polygonize function
|
|
472
|
+
|
|
473
|
+
try:
|
|
474
|
+
polygon_vectors = polygonize(norms)
|
|
475
|
+
# repeating the failed polygon creation. If it fails again, skip these pivots
|
|
476
|
+
|
|
477
|
+
except TriangleError:
|
|
478
|
+
continue
|
|
479
|
+
|
|
480
|
+
else:
|
|
481
|
+
continue
|
|
482
|
+
|
|
483
|
+
else:
|
|
484
|
+
continue
|
|
485
|
+
|
|
486
|
+
else: # norms type == 'impossible_digon', that is sides are too different in length
|
|
487
|
+
|
|
488
|
+
if not embedder.options.rigid:
|
|
489
|
+
|
|
490
|
+
if embedder.embed == 'chelotropic':
|
|
491
|
+
target_length = min(norms)
|
|
492
|
+
|
|
493
|
+
else:
|
|
494
|
+
maxgap = 3 # in Angstrom
|
|
495
|
+
gap = abs(norms[0]-norms[1])
|
|
496
|
+
r = 0.3 + 0.5*(gap/maxgap)
|
|
497
|
+
r = np.clip(5, 0.5, 0.8)
|
|
498
|
+
|
|
499
|
+
# r is the ratio for calculating target_length based
|
|
500
|
+
# on the gap that deformations will need to cover.
|
|
501
|
+
# It ranges from 0.5 to 0.8 and is shifted more toward
|
|
502
|
+
# the shorter norm as the gap rises. For gaps of more
|
|
503
|
+
# than maxgap Angstroms, the target length is very close
|
|
504
|
+
# to the shortest molecule, and only the molecule
|
|
505
|
+
# with the longest pivot is bent.
|
|
506
|
+
|
|
507
|
+
target_length = min(norms)*r + max(norms)*(1-r)
|
|
508
|
+
|
|
509
|
+
for i, mol in enumerate(deepcopy(embedder.objects)):
|
|
510
|
+
|
|
511
|
+
if len(mol.reactive_indices) > 1:
|
|
512
|
+
# do not try to bend molecules that react with a single atom
|
|
513
|
+
|
|
514
|
+
if tuple(sorted(mol.reactive_indices)) not in list(mol.graph.edges):
|
|
515
|
+
# do not try to bend molecules where the two reactive indices are bonded
|
|
516
|
+
|
|
517
|
+
traj = f'bend_{mol.filename}_p{p}_tgt_{round(target_length, 3)}' if embedder.options.debug else None
|
|
518
|
+
|
|
519
|
+
bent_mol = ase_bend(embedder,
|
|
520
|
+
mol,
|
|
521
|
+
conf_ids[i],
|
|
522
|
+
pivots[i],
|
|
523
|
+
target_length,
|
|
524
|
+
title=f'{mol.rootname} - pivot {p}',
|
|
525
|
+
traj=traj
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
# ase_view(bent_mol)
|
|
529
|
+
embedder.objects[i] = bent_mol
|
|
530
|
+
|
|
531
|
+
# Repeating the previous polygonization steps with the bent molecules
|
|
532
|
+
|
|
533
|
+
pivots = [embedder.objects[m].pivots[conf_ids[m]][pi[m]] for m, _ in enumerate(embedder.objects)]
|
|
534
|
+
# updating the active pivot for each molecule for this run
|
|
535
|
+
|
|
536
|
+
norms = np.linalg.norm(np.array([p.pivot for p in pivots]), axis=1)
|
|
537
|
+
# updating the pivots norms to feed into the polygonize function
|
|
538
|
+
|
|
539
|
+
polygon_vectors = polygonize(norms)
|
|
540
|
+
# repeating the failed polygon creation
|
|
541
|
+
|
|
542
|
+
else:
|
|
543
|
+
continue # do not embed digons with too different lengths if RIGID
|
|
544
|
+
|
|
545
|
+
directions = _get_directions(norms)
|
|
546
|
+
# directions to orient the molecules toward, orthogonal to each vec_pair
|
|
547
|
+
|
|
548
|
+
for v, vecs in enumerate(polygon_vectors):
|
|
549
|
+
# getting vertices to embed molecules with and iterating over start/end points
|
|
550
|
+
|
|
551
|
+
ids = _get_cyclical_reactive_indices(embedder, pivots, v)
|
|
552
|
+
# get indices of atoms that face each other
|
|
553
|
+
|
|
554
|
+
if not embedder.pairings_table or all((pair in ids) or (pair in embedder.internal_constraints) for pair in embedder.pairings_table.values()):
|
|
555
|
+
# ensure that the active arrangement has all the pairings that the user specified
|
|
556
|
+
|
|
557
|
+
angular_poses = []
|
|
558
|
+
# initialize a container for the poses generated for this combination of conformations,
|
|
559
|
+
# pairing and polygon_vectors orientation. These will be used not to generate poses
|
|
560
|
+
# that are too similar to each other.
|
|
561
|
+
|
|
562
|
+
if len(embedder.objects) == 3:
|
|
563
|
+
|
|
564
|
+
directions = _adjust_directions(embedder, directions, ids, vecs, pivots, conf_ids)
|
|
565
|
+
# For trimolecular TSs, the alignment direction previously get is
|
|
566
|
+
# just a general first approximation that needs to be corrected
|
|
567
|
+
# for the specific case through another algorithm.
|
|
568
|
+
|
|
569
|
+
for angles in embedder.systematic_angles:
|
|
570
|
+
|
|
571
|
+
for i, vec_pair in enumerate(vecs):
|
|
572
|
+
# setting molecular positions and rotations (embedding)
|
|
573
|
+
# i is the molecule index, vecs is a tuple of start and end positions
|
|
574
|
+
# for the pivot vector
|
|
575
|
+
|
|
576
|
+
start, end = vec_pair
|
|
577
|
+
angle = angles[i]
|
|
578
|
+
|
|
579
|
+
reactive_coords = embedder.objects[i].atomcoords[conf_ids[i]][embedder.objects[i].reactive_indices]
|
|
580
|
+
# coordinates for the reactive atoms in this run
|
|
581
|
+
|
|
582
|
+
atomic_pivot_mean = np.mean(reactive_coords, axis=0)
|
|
583
|
+
# mean position of the atoms active in this run
|
|
584
|
+
|
|
585
|
+
mol_direction = pivots[i].meanpoint-atomic_pivot_mean
|
|
586
|
+
if np.all(mol_direction == 0.):
|
|
587
|
+
mol_direction = pivots[i].meanpoint
|
|
588
|
+
# log.write(f'mol {i} - improper pivot? Thread {len(threads)-1}\n')
|
|
589
|
+
|
|
590
|
+
# Direction in which the molecule should be oriented, based on the mean of reactive
|
|
591
|
+
# atom positions and the mean point of the active pivot for the run.
|
|
592
|
+
# If this vector is too small and gets rounded to zero (as it can happen for
|
|
593
|
+
# "antrafacial" vectors), we fallback to the vector starting from the molecule
|
|
594
|
+
# center (mean of atomic positions) and ending in pivot_means[i], so to avoid
|
|
595
|
+
# numeric errors in the next function.
|
|
596
|
+
|
|
597
|
+
alignment_rotation = align_vec_pair(np.array([end-start, directions[i]]),
|
|
598
|
+
np.array([pivots[i].pivot, mol_direction]))
|
|
599
|
+
# this rotation superimposes the molecular orbitals active in this run (pivots[i].pivot
|
|
600
|
+
# goes to end-start) and also aligns the molecules so that they face each other
|
|
601
|
+
# (mol_direction goes to directions[i])
|
|
602
|
+
|
|
603
|
+
if len(reactive_coords) == 2:
|
|
604
|
+
axis_of_step_rotation = alignment_rotation @ (reactive_coords[0]-reactive_coords[1])
|
|
605
|
+
else:
|
|
606
|
+
axis_of_step_rotation = alignment_rotation @ pivots[i].pivot
|
|
607
|
+
# molecules with two reactive atoms are step-rotated around the line connecting
|
|
608
|
+
# the reactive atoms, while single reactive atom mols around their active pivot
|
|
609
|
+
|
|
610
|
+
step_rotation = rot_mat_from_pointer(axis_of_step_rotation, angle)
|
|
611
|
+
# this rotation cycles through all different rotation angles for each molecule
|
|
612
|
+
|
|
613
|
+
center_of_rotation = alignment_rotation @ atomic_pivot_mean
|
|
614
|
+
# center_of_rotation is the mean point between the reactive atoms so
|
|
615
|
+
# as to keep the reactive distances constant
|
|
616
|
+
|
|
617
|
+
embedder.objects[i].rotation = step_rotation @ alignment_rotation
|
|
618
|
+
# overall rotation for the molecule is given by the matrices product
|
|
619
|
+
|
|
620
|
+
pos = np.mean(vec_pair, axis=0) - alignment_rotation @ pivots[i].meanpoint
|
|
621
|
+
embedder.objects[i].position = center_of_rotation - step_rotation @ center_of_rotation + pos
|
|
622
|
+
# overall position is given by superimposing mean of active pivot (connecting orbitals)
|
|
623
|
+
# to mean of vec_pair (defining the target position - the side of a triangle for three molecules)
|
|
624
|
+
|
|
625
|
+
embedded_structure = get_embed(embedder.objects, conf_ids)
|
|
626
|
+
if compenetration_check(embedded_structure, ids=embedder.ids, thresh=embedder.options.clash_thresh):
|
|
627
|
+
if not _rmsd_similarity(embedded_structure, angular_poses, rmsd_thr=1):
|
|
628
|
+
poses.append(embedded_structure)
|
|
629
|
+
angular_poses.append(embedded_structure)
|
|
630
|
+
constrained_indices.append(ids)
|
|
631
|
+
# Save indices to be constrained later in the optimization step
|
|
632
|
+
|
|
633
|
+
loadbar(1, 1, prefix='Embedding structures ')
|
|
634
|
+
|
|
635
|
+
embedder.constrained_indices = np.array(constrained_indices)
|
|
636
|
+
|
|
637
|
+
if not poses:
|
|
638
|
+
s = ('\n--> Cyclical embed did not find any suitable disposition of molecules.\n' +
|
|
639
|
+
' This is probably because one molecule has two reactive centers at a great distance,\n' +
|
|
640
|
+
' preventing the other two molecules from forming a closed, cyclical structure.')
|
|
641
|
+
embedder.log(s, p=False)
|
|
642
|
+
raise ZeroCandidatesError(s)
|
|
643
|
+
|
|
644
|
+
return np.array(poses)
|
|
645
|
+
|
|
646
|
+
def _fast_bimol_rigid_cyclical_embed(embedder, max_norm_delta=10):
|
|
647
|
+
'''
|
|
648
|
+
return threads: return embedded structures, with position and rotation attributes set, ready to be pumped
|
|
649
|
+
into embedder.structures. Algorithm used is the "cyclical" algorithm (see docs).
|
|
650
|
+
'''
|
|
651
|
+
|
|
652
|
+
embedder.log(f'\n--> Performing {embedder.embed} embed ({embedder.candidates} candidates)')
|
|
653
|
+
|
|
654
|
+
conf_number = [len(mol.atomcoords) for mol in embedder.objects]
|
|
655
|
+
conf_indices = cartesian_product(*[np.array(range(i)) for i in conf_number])
|
|
656
|
+
|
|
657
|
+
poses = []
|
|
658
|
+
constrained_indices = []
|
|
659
|
+
for ci, conf_ids in enumerate(conf_indices):
|
|
660
|
+
|
|
661
|
+
pivots_indices = cartesian_product(*[range(len(mol.pivots[conf_ids[i]])) for i, mol in enumerate(embedder.objects)])
|
|
662
|
+
# indices of pivots in each molecule self.pivots[conf] list. For three mols with 2 pivots each: [[0,0,0], [0,0,1], [0,1,0], ...]
|
|
663
|
+
|
|
664
|
+
for p, pi in enumerate(pivots_indices):
|
|
665
|
+
|
|
666
|
+
loadbar(p+ci*(len(pivots_indices)), len(pivots_indices)*len(conf_indices), prefix='Embedding structures ')
|
|
667
|
+
|
|
668
|
+
pivots = [embedder.objects[m].pivots[conf_ids[m]][pi[m]] for m, _ in enumerate(embedder.objects)]
|
|
669
|
+
# getting the active pivot for each molecule for this run
|
|
670
|
+
|
|
671
|
+
norms = np.linalg.norm(np.array([p.pivot for p in pivots]), axis=1)
|
|
672
|
+
# getting the pivots norms to feed into the polygonize function
|
|
673
|
+
|
|
674
|
+
if abs(norms[0] - norms[1]) > max_norm_delta:
|
|
675
|
+
continue
|
|
676
|
+
# skip if norms are too different
|
|
677
|
+
|
|
678
|
+
polygon_vectors = polygonize(norms)
|
|
679
|
+
|
|
680
|
+
directions = np.array([[0, 1,0], [0,-1,0]])
|
|
681
|
+
# directions to orient the molecules toward, orthogonal to each vec_pair
|
|
682
|
+
|
|
683
|
+
for v, vecs in enumerate(polygon_vectors):
|
|
684
|
+
# getting vertices to embed molecules with and iterating over start/end points
|
|
685
|
+
|
|
686
|
+
ids = _get_cyclical_reactive_indices(embedder, pivots, v)
|
|
687
|
+
# get indices of atoms that face each other
|
|
688
|
+
|
|
689
|
+
if not embedder.pairings_table or all((pair in ids) or (pair in embedder.internal_constraints) for pair in embedder.pairings_table.values()):
|
|
690
|
+
# ensure that the active arrangement has all the pairings that the user specified
|
|
691
|
+
|
|
692
|
+
angular_poses = []
|
|
693
|
+
# initialize a container for the poses generated for this combination of conformations,
|
|
694
|
+
# pairing and polygon_vectors orientation. These will be used not to generate poses
|
|
695
|
+
# that are too similar to each other.
|
|
696
|
+
|
|
697
|
+
for angles in embedder.systematic_angles:
|
|
698
|
+
|
|
699
|
+
for i, vec_pair in enumerate(vecs):
|
|
700
|
+
# setting molecular positions and rotations (embedding)
|
|
701
|
+
# i is the molecule index, vecs is a tuple of start and end positions
|
|
702
|
+
# for the pivot vector
|
|
703
|
+
|
|
704
|
+
start, end = vec_pair
|
|
705
|
+
angle = angles[i]
|
|
706
|
+
|
|
707
|
+
reactive_coords = embedder.objects[i].atomcoords[conf_ids[i]][embedder.objects[i].reactive_indices]
|
|
708
|
+
# coordinates for the reactive atoms in this run
|
|
709
|
+
|
|
710
|
+
atomic_pivot_mean = np.mean(reactive_coords, axis=0)
|
|
711
|
+
# mean position of the atoms active in this run
|
|
712
|
+
|
|
713
|
+
mol_direction = pivots[i].meanpoint-atomic_pivot_mean
|
|
714
|
+
if np.all(mol_direction == 0.):
|
|
715
|
+
mol_direction = pivots[i].meanpoint
|
|
716
|
+
# log.write(f'mol {i} - improper pivot? Thread {len(threads)-1}\n')
|
|
717
|
+
|
|
718
|
+
# Direction in which the molecule should be oriented, based on the mean of reactive
|
|
719
|
+
# atom positions and the mean point of the active pivot for the run.
|
|
720
|
+
# If this vector is too small and gets rounded to zero (as it can happen for
|
|
721
|
+
# "antrafacial" vectors), we fallback to the vector starting from the molecule
|
|
722
|
+
# center (mean of atomic positions) and ending in pivot_means[i], so to avoid
|
|
723
|
+
# numeric errors in the next function.
|
|
724
|
+
|
|
725
|
+
alignment_rotation = align_vec_pair(np.array([end-start, directions[i]]),
|
|
726
|
+
np.array([pivots[i].pivot, mol_direction]))
|
|
727
|
+
# this rotation superimposes the molecular orbitals active in this run (pivots[i].pivot
|
|
728
|
+
# goes to end-start) and also aligns the molecules so that they face each other
|
|
729
|
+
# (mol_direction goes to directions[i])
|
|
730
|
+
|
|
731
|
+
if len(reactive_coords) == 2:
|
|
732
|
+
axis_of_step_rotation = alignment_rotation @ (reactive_coords[0]-reactive_coords[1])
|
|
733
|
+
else:
|
|
734
|
+
axis_of_step_rotation = alignment_rotation @ pivots[i].pivot
|
|
735
|
+
# molecules with two reactive atoms are step-rotated around the line connecting
|
|
736
|
+
# the reactive atoms, while single reactive atom mols around their active pivot
|
|
737
|
+
|
|
738
|
+
step_rotation = rot_mat_from_pointer(axis_of_step_rotation, angle)
|
|
739
|
+
# this rotation cycles through all different rotation angles for each molecule
|
|
740
|
+
|
|
741
|
+
center_of_rotation = alignment_rotation @ atomic_pivot_mean
|
|
742
|
+
# center_of_rotation is the mean point between the reactive atoms so
|
|
743
|
+
# as to keep the reactive distances constant
|
|
744
|
+
|
|
745
|
+
embedder.objects[i].rotation = step_rotation @ alignment_rotation
|
|
746
|
+
# overall rotation for the molecule is given by the matrices product
|
|
747
|
+
|
|
748
|
+
pos = np.mean(vec_pair, axis=0) - alignment_rotation @ pivots[i].meanpoint
|
|
749
|
+
embedder.objects[i].position = center_of_rotation - step_rotation @ center_of_rotation + pos
|
|
750
|
+
# overall position is given by superimposing mean of active pivot (connecting orbitals)
|
|
751
|
+
# to mean of vec_pair (defining the target position - the side of a triangle for three molecules)
|
|
752
|
+
|
|
753
|
+
embedded_structure = get_embed(embedder.objects, conf_ids)
|
|
754
|
+
if compenetration_check(embedded_structure, ids=embedder.ids, thresh=embedder.options.clash_thresh):
|
|
755
|
+
if not _rmsd_similarity(embedded_structure, angular_poses, rmsd_thr=1):
|
|
756
|
+
poses.append(embedded_structure)
|
|
757
|
+
angular_poses.append(embedded_structure)
|
|
758
|
+
constrained_indices.append(ids)
|
|
759
|
+
# Save indices to be constrained later in the optimization step
|
|
760
|
+
|
|
761
|
+
loadbar(1, 1, prefix='Embedding structures ')
|
|
762
|
+
|
|
763
|
+
embedder.constrained_indices = np.array(constrained_indices)
|
|
764
|
+
|
|
765
|
+
if not poses:
|
|
766
|
+
s = ('\n--> Cyclical embed did not find any suitable disposition of molecules.\n' +
|
|
767
|
+
' This is probably because one molecule has two reactive centers at a great distance,\n' +
|
|
768
|
+
' preventing the other two molecules from forming a closed, cyclical structure.')
|
|
769
|
+
embedder.log(s, p=False)
|
|
770
|
+
raise ZeroCandidatesError(s)
|
|
771
|
+
|
|
772
|
+
return np.array(poses)
|
|
773
|
+
|
|
774
|
+
def _get_cyclical_reactive_indices(embedder, pivots, n):
|
|
775
|
+
'''
|
|
776
|
+
:params n: index of the n-th disposition of vectors yielded by the polygonize function.
|
|
777
|
+
:return: list of index couples, to be constrained during the partial optimization.
|
|
778
|
+
'''
|
|
779
|
+
|
|
780
|
+
cumulative_pivots_ids = [[p.start_atom.cumnum, p.end_atom.cumnum] for p in pivots]
|
|
781
|
+
|
|
782
|
+
def orient(i,ids,n):
|
|
783
|
+
if swaps[n][i]:
|
|
784
|
+
return list(reversed(ids))
|
|
785
|
+
return ids
|
|
786
|
+
|
|
787
|
+
if len(embedder.objects) == 2:
|
|
788
|
+
|
|
789
|
+
swaps = [(0,0),
|
|
790
|
+
(0,1)]
|
|
791
|
+
|
|
792
|
+
oriented = [orient(i,ids,n) for i, ids in enumerate(cumulative_pivots_ids)]
|
|
793
|
+
couples = [[oriented[0][0], oriented[1][0]], [oriented[0][1], oriented[1][1]]]
|
|
794
|
+
|
|
795
|
+
return couples
|
|
796
|
+
|
|
797
|
+
swaps = [(0,0,0),
|
|
798
|
+
(0,0,1),
|
|
799
|
+
(0,1,0),
|
|
800
|
+
(0,1,1),
|
|
801
|
+
(1,0,0),
|
|
802
|
+
(1,1,0),
|
|
803
|
+
(1,0,1),
|
|
804
|
+
(1,1,1)]
|
|
805
|
+
|
|
806
|
+
oriented = [orient(i,ids,n) for i, ids in enumerate(cumulative_pivots_ids)]
|
|
807
|
+
couples = [[oriented[0][1], oriented[1][0]], [oriented[1][1], oriented[2][0]], [oriented[2][1], oriented[0][0]]]
|
|
808
|
+
couples = [sorted(c) for c in couples]
|
|
809
|
+
|
|
810
|
+
return couples
|
|
811
|
+
|
|
812
|
+
def monomolecular_embed(embedder):
|
|
813
|
+
'''
|
|
814
|
+
return threads: embeds structures by bending molecules, storing them
|
|
815
|
+
in embedder.structures. Algorithm used is the "monomolecular" algorithm (see docs).
|
|
816
|
+
'''
|
|
817
|
+
|
|
818
|
+
assert len(embedder.objects) == 1
|
|
819
|
+
|
|
820
|
+
embedder.log(f'\n--> Performing monomolecular embed ({embedder.candidates} candidates)')
|
|
821
|
+
|
|
822
|
+
mol = embedder.objects[0]
|
|
823
|
+
|
|
824
|
+
embedder.structures = []
|
|
825
|
+
|
|
826
|
+
for c, _ in enumerate(mol.atomcoords):
|
|
827
|
+
for p, pivot in enumerate(mol.pivots[c]):
|
|
828
|
+
|
|
829
|
+
loadbar(p, len(mol.pivots[c]), prefix='Bending structures ')
|
|
830
|
+
|
|
831
|
+
traj = f'bend_{p}_monomol' if embedder.options.debug else None
|
|
832
|
+
|
|
833
|
+
bent_mol = ase_bend(embedder,
|
|
834
|
+
mol,
|
|
835
|
+
c,
|
|
836
|
+
pivot,
|
|
837
|
+
1, # bend until we are within 1 A to
|
|
838
|
+
# the target distance between atoms
|
|
839
|
+
title=f'{mol.rootname} - pivot {p}',
|
|
840
|
+
traj=traj,
|
|
841
|
+
check=False, # avoid returning the non-bent molecule,
|
|
842
|
+
# even if this means having it scrambled
|
|
843
|
+
)
|
|
844
|
+
|
|
845
|
+
for conformer in bent_mol.atomcoords:
|
|
846
|
+
embedder.structures.append(conformer)
|
|
847
|
+
|
|
848
|
+
loadbar(1, 1, prefix='Bending structures ')
|
|
849
|
+
|
|
850
|
+
embedder.structures = np.array(embedder.structures)
|
|
851
|
+
|
|
852
|
+
embedder.atomnos = mol.atomnos
|
|
853
|
+
embedder.energies = np.zeros(len(embedder.structures))
|
|
854
|
+
embedder.exit_status = np.zeros(len(embedder.structures), dtype=bool)
|
|
855
|
+
embedder.graphs = [mol.graph]
|
|
856
|
+
|
|
857
|
+
embedder.constrained_indices = _get_monomolecular_reactive_indices(embedder)
|
|
858
|
+
|
|
859
|
+
return embedder.structures
|
|
860
|
+
|
|
861
|
+
def _get_monomolecular_reactive_indices(embedder):
|
|
862
|
+
'''
|
|
863
|
+
'''
|
|
864
|
+
if embedder.pairings_table:
|
|
865
|
+
return np.array([list(embedder.pairings_table.values())
|
|
866
|
+
for _ in embedder.structures])
|
|
867
|
+
# This option gives the possibility to specify pairings in
|
|
868
|
+
# refine>/REFINE runs, so as to make constrained optimizations
|
|
869
|
+
# accessible.
|
|
870
|
+
|
|
871
|
+
return np.array([[] for _ in embedder.structures])
|
|
872
|
+
|
|
873
|
+
def get_embed(mols, conf_ids):
|
|
874
|
+
'''
|
|
875
|
+
mols: iterable of Hypermolecule objects
|
|
876
|
+
conf_ids: iterable of conformer indices for each mol
|
|
877
|
+
|
|
878
|
+
Returns an np.array with the coordinates
|
|
879
|
+
of every molecule as a concatenated array.
|
|
880
|
+
'''
|
|
881
|
+
return np.concatenate([(mol.rotation @ mol.atomcoords[c].T).T + mol.position for mol, c in zip(mols, conf_ids)])
|