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.
Files changed (59) hide show
  1. firecode/TEST_NOTEBOOK.ipynb +3940 -0
  2. firecode/__init__.py +0 -0
  3. firecode/__main__.py +118 -0
  4. firecode/_gaussian.py +97 -0
  5. firecode/algebra.py +405 -0
  6. firecode/ase_manipulations.py +879 -0
  7. firecode/atropisomer_module.py +516 -0
  8. firecode/automep.py +130 -0
  9. firecode/calculators/__init__.py +29 -0
  10. firecode/calculators/_gaussian.py +98 -0
  11. firecode/calculators/_mopac.py +242 -0
  12. firecode/calculators/_openbabel.py +154 -0
  13. firecode/calculators/_orca.py +129 -0
  14. firecode/calculators/_xtb.py +786 -0
  15. firecode/concurrent_test.py +119 -0
  16. firecode/embedder.py +2590 -0
  17. firecode/embedder_options.py +577 -0
  18. firecode/embeds.py +881 -0
  19. firecode/errors.py +65 -0
  20. firecode/graph_manipulations.py +333 -0
  21. firecode/hypermolecule_class.py +364 -0
  22. firecode/mep_relaxer.py +199 -0
  23. firecode/modify_settings.py +186 -0
  24. firecode/mprof.py +65 -0
  25. firecode/multiembed.py +148 -0
  26. firecode/nci.py +186 -0
  27. firecode/numba_functions.py +260 -0
  28. firecode/operators.py +776 -0
  29. firecode/optimization_methods.py +609 -0
  30. firecode/parameters.py +84 -0
  31. firecode/pka.py +275 -0
  32. firecode/profiler.py +17 -0
  33. firecode/pruning.py +421 -0
  34. firecode/pt.py +32 -0
  35. firecode/quotes.json +6651 -0
  36. firecode/quotes.py +9 -0
  37. firecode/reactive_atoms_classes.py +666 -0
  38. firecode/references.py +11 -0
  39. firecode/rmsd.py +74 -0
  40. firecode/settings.py +75 -0
  41. firecode/solvents.py +126 -0
  42. firecode/tests/C2F2H4.xyz +10 -0
  43. firecode/tests/C2H4.xyz +8 -0
  44. firecode/tests/CH3Cl.xyz +7 -0
  45. firecode/tests/HCOOH.xyz +7 -0
  46. firecode/tests/HCOOOH.xyz +8 -0
  47. firecode/tests/chelotropic.txt +3 -0
  48. firecode/tests/cyclical.txt +3 -0
  49. firecode/tests/dihedral.txt +2 -0
  50. firecode/tests/string.txt +3 -0
  51. firecode/tests/trimolecular.txt +9 -0
  52. firecode/tests.py +151 -0
  53. firecode/torsion_module.py +1035 -0
  54. firecode/utils.py +541 -0
  55. firecode-1.0.0.dist-info/LICENSE +165 -0
  56. firecode-1.0.0.dist-info/METADATA +321 -0
  57. firecode-1.0.0.dist-info/RECORD +59 -0
  58. firecode-1.0.0.dist-info/WHEEL +5 -0
  59. 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)])