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/quotes.py ADDED
@@ -0,0 +1,9 @@
1
+ import json
2
+ import os
3
+
4
+ def load_quotes():
5
+ quotes_folder = os.path.dirname(os.path.realpath(__file__))
6
+ quotes_path = os.path.join(quotes_folder, 'quotes.json')
7
+ with open(quotes_path, 'rb') as f:
8
+ quotes = json.loads(f.read())
9
+ return quotes
@@ -0,0 +1,666 @@
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 norm, norm_of, rot_mat_from_pointer, vec_angle
29
+ from firecode.graph_manipulations import neighbors
30
+ from firecode.parameters import orb_dim_dict
31
+ from firecode.pt import pt
32
+
33
+
34
+ class Single:
35
+
36
+ def __repr__(self):
37
+ return 'Single Bond'
38
+
39
+ def init(self, mol, i, update=False, orb_dim=None, conf=0) -> None:
40
+ '''
41
+ '''
42
+ self.index = i
43
+ self.symbol = pt[mol.atomnos[i]].symbol
44
+ neighbors_indices = neighbors(mol.graph, i)
45
+
46
+ self.neighbors_symbols = [pt[mol.atomnos[i]].symbol for i in neighbors_indices]
47
+ self.coord = mol.atomcoords[conf][i]
48
+ self.other = mol.atomcoords[conf][neighbors_indices][0]
49
+
50
+ if not mol.sp3_sigmastar:
51
+ self.orb_vecs = np.array([norm(self.coord - self.other)])
52
+
53
+ else:
54
+ other_reactive_indices = list(mol.reactive_indices)
55
+ other_reactive_indices.remove(i)
56
+ for index in other_reactive_indices:
57
+ if index in neighbors_indices:
58
+ parnter_index = index
59
+ break
60
+ # obtain the reference partner index
61
+
62
+ partner = mol.atomcoords[conf][parnter_index]
63
+ pivot = norm(partner - self.coord)
64
+
65
+ neighbors_of_partner = neighbors(mol.graph, parnter_index)
66
+ neighbors_of_partner.remove(i)
67
+ orb_vec = norm(mol.atomcoords[conf][neighbors_of_partner[0]] - partner)
68
+ orb_vec = orb_vec - orb_vec @ pivot * pivot
69
+
70
+ steps = 3 # number of total orbitals
71
+ self.orb_vecs = np.array([rot_mat_from_pointer(pivot, angle+60) @ orb_vec for angle in range(0,360,int(360/steps))])
72
+ # orbitals are staggered in relation to sp3 substituents
73
+
74
+ self.orb_vers = norm(self.orb_vecs[0])
75
+
76
+ if update:
77
+ if orb_dim is None:
78
+ key = self.symbol + ' ' + str(self).split(' (')[0]
79
+ orb_dim = orb_dim_dict.get(key)
80
+
81
+ if orb_dim is None:
82
+ orb_dim = norm_of(self.coord - self.other)
83
+ print(f'ATTENTION: COULD NOT SETUP REACTIVE ATOM ORBITAL FROM PARAMETERS. We have no parameters for {key}. Using the bonding distance ({round(orb_dim, 3)} A).')
84
+
85
+ self.center = orb_dim * self.orb_vecs + self.coord
86
+
87
+
88
+ class Sp2:
89
+
90
+ def __repr__(self):
91
+ return 'sp2'
92
+
93
+ def init(self, mol, i, update=False, orb_dim=None, conf=0) -> None:
94
+ '''
95
+ '''
96
+ self.index = i
97
+ self.symbol = pt[mol.atomnos[i]].symbol
98
+ neighbors_indices = neighbors(mol.graph, i)
99
+
100
+
101
+
102
+ self.neighbors_symbols = [pt[mol.atomnos[i]].symbol for i in neighbors_indices]
103
+ self.coord = mol.atomcoords[conf][i]
104
+ self.others = mol.atomcoords[conf][neighbors_indices]
105
+
106
+ self.vectors = self.others - self.coord # vectors connecting reactive atom with neighbors
107
+ self.orb_vec = norm(np.mean(np.array([np.cross(norm(self.vectors[0]), norm(self.vectors[1])),
108
+ np.cross(norm(self.vectors[1]), norm(self.vectors[2])),
109
+ np.cross(norm(self.vectors[2]), norm(self.vectors[0]))]), axis=0))
110
+
111
+ self.orb_vecs = np.vstack((self.orb_vec, -self.orb_vec))
112
+
113
+ if update:
114
+ if orb_dim is None:
115
+ key = self.symbol + ' ' + str(self).split(' (')[0]
116
+ orb_dim = orb_dim_dict.get(key)
117
+
118
+ if orb_dim is None:
119
+ orb_dim = orb_dim_dict['Fallback']
120
+ print(f'ATTENTION: COULD NOT SETUP REACTIVE ATOM ORBITAL FROM PARAMETERS. We have no parameters for {key}. Using {orb_dim} A.')
121
+
122
+ self.center = self.orb_vecs * orb_dim
123
+
124
+ self.center += self.coord
125
+
126
+
127
+ class Sp3:
128
+
129
+ def __repr__(self):
130
+ return 'sp3'
131
+
132
+ def init(self, mol, i, update=False, orb_dim=None, conf=0) -> None:
133
+
134
+ self.index = i
135
+ self.symbol = pt[mol.atomnos[i]].symbol
136
+ neighbors_indices = neighbors(mol.graph, i)
137
+ self.neighbors_symbols = [pt[mol.atomnos[i]].symbol for i in neighbors_indices]
138
+ self.coord = mol.atomcoords[conf][i]
139
+ self.others = mol.atomcoords[conf][neighbors_indices]
140
+
141
+ if not mol.sp3_sigmastar:
142
+
143
+ if not hasattr(self, 'leaving_group_index'):
144
+ self.leaving_group_index = None
145
+
146
+ if len([atom for atom in self.neighbors_symbols if atom in ['O', 'N', 'Cl', 'Br', 'I']]) == 1: # if we can tell where is the leaving group
147
+ self.leaving_group_coords = self.others[self.neighbors_symbols.index([atom for atom in self.neighbors_symbols if atom in ['O', 'Cl', 'Br', 'I']][0])]
148
+
149
+ elif len([atom for atom in self.neighbors_symbols if atom not in ['H']]) == 1: # if no clear leaving group but we only have one atom != H
150
+ self.leaving_group_coords = self.others[self.neighbors_symbols.index([atom for atom in self.neighbors_symbols if atom not in ['H']][0])]
151
+
152
+ else: # if we cannot infer, ask user if we didn't have already
153
+ try:
154
+ self.leaving_group_coords = self._set_leaving_group(mol, neighbors_indices)
155
+
156
+ except Exception:
157
+ # if something goes wrong, we fallback to command line input for reactive atom index collection
158
+
159
+ if self.leaving_group_index is None:
160
+
161
+ while True:
162
+
163
+ self.leaving_group_index = input(f'Please insert the index of the leaving group atom bonded to the sp3 reactive atom (index {self.index}) of molecule {mol.rootname} : ')
164
+
165
+ if self.leaving_group_index == '' or self.leaving_group_index.lower().islower():
166
+ pass
167
+
168
+ elif int(self.leaving_group_index) in neighbors_indices:
169
+ self.leaving_group_index = int(self.leaving_group_index)
170
+ break
171
+
172
+ else:
173
+ print(f'Atom {self.leaving_group_index} is not bonded to the sp3 center with index {self.index}.')
174
+
175
+ self.leaving_group_coords = self.others[neighbors_indices.index(self.leaving_group_index)]
176
+
177
+ self.orb_vecs = np.array([self.coord - self.leaving_group_coords])
178
+ self.orb_vers = norm(self.orb_vecs[0])
179
+
180
+ else: # Sigma bond type
181
+
182
+ other_reactive_indices = list(mol.reactive_indices)
183
+ other_reactive_indices.remove(i)
184
+ for index in other_reactive_indices:
185
+ if index in neighbors_indices:
186
+ parnter_index = index
187
+ break
188
+ # obtain the reference partner index
189
+
190
+ pivot = norm(mol.atomcoords[conf][parnter_index] - self.coord)
191
+
192
+ other_neighbors = deepcopy(neighbors_indices)
193
+ other_neighbors.remove(parnter_index)
194
+ orb_vec = norm(mol.atomcoords[conf][other_neighbors[0]] - self.coord)
195
+ orb_vec = orb_vec - orb_vec @ pivot * pivot
196
+
197
+ steps = 3 # number of total orbitals
198
+ self.orb_vecs = np.array([rot_mat_from_pointer(pivot, angle+60) @ orb_vec for angle in range(0,360,int(360/steps))])
199
+ # orbitals are staggered in relation to sp3 substituents
200
+
201
+ self.orb_vers = norm(self.orb_vecs[0])
202
+
203
+ if update:
204
+ if orb_dim is None:
205
+ key = self.symbol + ' ' + str(self).split(' (')[0]
206
+ orb_dim = orb_dim_dict.get(key)
207
+
208
+ if orb_dim is None:
209
+ orb_dim = orb_dim_dict['Fallback']
210
+ print(f'ATTENTION: COULD NOT SETUP REACTIVE ATOM ORBITAL FROM PARAMETERS. We have no parameters for {key}. Using {orb_dim} A.')
211
+
212
+ self.center = np.array([orb_dim * norm(vec) + self.coord for vec in self.orb_vecs])
213
+
214
+ def _set_leaving_group(self, mol, neighbors_indices):
215
+ '''
216
+ Manually set the molecule leaving group from the ASE GUI, imposing
217
+ a constraint on the desired atom.
218
+
219
+ '''
220
+
221
+ if self.leaving_group_index is None:
222
+
223
+ from ase import Atoms
224
+ from ase.gui.gui import GUI
225
+ from ase.gui.images import Images
226
+
227
+ atoms = Atoms(mol.atomnos, positions=mol.atomcoords[0])
228
+
229
+ while True:
230
+ print(('\nPlease, manually select the leaving group atom for molecule %s'
231
+ '\nbonded to the sp3 reactive atom with index %s.'
232
+ '\nRotate with right click and select atoms by clicking.'
233
+ '\nThen go to Tools -> Constraints -> Constrain, and close the GUI.') % (mol.filename, self.index))
234
+
235
+ GUI(images=Images([atoms]), show_bonds=True).run()
236
+
237
+ if atoms.constraints != []:
238
+ if len(list(atoms.constraints[0].get_indices())) == 1:
239
+ if list(atoms.constraints[0].get_indices())[0] in neighbors_indices:
240
+ self.leaving_group_index = list(atoms.constraints[0].get_indices())[0]
241
+ break
242
+ else:
243
+ print('\nSeems that the atom you selected is not bonded to the reactive center or is the reactive atom itself.\nThis is probably an error, please try again.')
244
+ atoms.constraints = []
245
+ else:
246
+ print('\nPlease only select one leaving group atom.')
247
+ atoms.constraints = []
248
+
249
+
250
+ return self.others[neighbors_indices.index(self.leaving_group_index)]
251
+
252
+
253
+ class Ether:
254
+
255
+ def __repr__(self):
256
+ return 'Ether'
257
+
258
+ def init(self, mol, i, update=False, orb_dim=None, conf=0) -> None:
259
+ '''
260
+ '''
261
+ self.index = i
262
+ self.symbol = pt[mol.atomnos[i]].symbol
263
+ neighbors_indices = neighbors(mol.graph, i)
264
+
265
+
266
+
267
+ self.neighbors_symbols = [pt[mol.atomnos[i]].symbol for i in neighbors_indices]
268
+ self.coord = mol.atomcoords[conf][i]
269
+ self.others = mol.atomcoords[conf][neighbors_indices]
270
+
271
+ self.orb_vecs = self.others - self.coord # vectors connecting center to each of the two substituents
272
+
273
+ if update:
274
+ if orb_dim is None:
275
+ key = self.symbol + ' ' + str(self).split(' (')[0]
276
+ orb_dim = orb_dim_dict.get(key)
277
+
278
+ if orb_dim is None:
279
+ orb_dim = orb_dim_dict['Fallback']
280
+ print(f'ATTENTION: COULD NOT SETUP REACTIVE ATOM ORBITAL FROM PARAMETERS. We have no parameters for {key}. Using {orb_dim} A.')
281
+
282
+ self.orb_vecs = orb_dim * np.array([norm(v) for v in self.orb_vecs]) # making both vectors a fixed, defined length
283
+
284
+ orb_mat = rot_mat_from_pointer(np.mean(self.orb_vecs, axis=0), 90) @ rot_mat_from_pointer(np.cross(self.orb_vecs[0], self.orb_vecs[1]), 180)
285
+
286
+ # self.orb_vecs = np.array([orb_mat @ v for v in self.orb_vecs])
287
+ self.orb_vecs = (orb_mat @ self.orb_vecs.T).T
288
+
289
+ self.center = self.orb_vecs + self.coord
290
+ # two vectors defining the position of the two orbital lobes centers
291
+
292
+
293
+ class Ketone:
294
+
295
+ def __repr__(self):
296
+ return f'Ketone ({self.subtype})'
297
+
298
+ def init(self, mol, i, update=False, orb_dim=None, conf=0) -> None:
299
+ '''
300
+ '''
301
+ self.index = i
302
+ self.symbol = pt[mol.atomnos[i]].symbol
303
+ neighbors_indices = neighbors(mol.graph, i)
304
+ self.subtype = 'pre-init'
305
+
306
+
307
+ self.neighbors_symbols = [pt[mol.atomnos[i]].symbol for i in neighbors_indices]
308
+ self.coord = mol.atomcoords[conf][i]
309
+ self.other = mol.atomcoords[conf][neighbors_indices][0]
310
+
311
+ self.vector = self.other - self.coord # vector connecting center to substituent
312
+
313
+ if update:
314
+ if orb_dim is None:
315
+ key = self.symbol + ' ' + str(self).split(' (')[0]
316
+ orb_dim = orb_dim_dict.get(key)
317
+
318
+ if orb_dim is None:
319
+ orb_dim = orb_dim_dict['Fallback']
320
+ print(f'ATTENTION: COULD NOT SETUP REACTIVE ATOM ORBITAL FROM PARAMETERS. We have no parameters for {key}. Using {orb_dim} A.')
321
+
322
+ neighbors_of_neighbor_indices = neighbors(mol.graph, neighbors_indices[0])
323
+ neighbors_of_neighbor_indices.remove(i)
324
+
325
+ self.vector = norm(self.vector)*orb_dim
326
+
327
+ if len(neighbors_of_neighbor_indices) == 1:
328
+ # ketene
329
+
330
+ ketene_sub_indices = neighbors(mol.graph, neighbors_of_neighbor_indices[0])
331
+ ketene_sub_indices.remove(neighbors_indices[0])
332
+
333
+ ketene_sub_coords = mol.atomcoords[conf][ketene_sub_indices[0]]
334
+ n_o_n_coords = mol.atomcoords[conf][neighbors_of_neighbor_indices[0]]
335
+
336
+ # vector connecting ketene R with C (O=C=C(R)R)
337
+ v = (ketene_sub_coords - n_o_n_coords)
338
+
339
+ # this vector is orthogonal to the ketene O=C=C and coplanar with the ketene
340
+ pointer = v - ((v @ norm(self.vector)) * self.vector)
341
+ pointer = norm(pointer) * orb_dim
342
+
343
+ self.center = np.array([rot_mat_from_pointer(self.vector, 90*step) @ pointer for step in range(4)])
344
+
345
+ self.subtype = 'p+p'
346
+
347
+ elif len(neighbors_of_neighbor_indices) == 2:
348
+ # if it is a normal ketone (or an enolate), n orbital lobes must be coplanar with
349
+ # atoms connecting to ketone C atom, or p lobes must be placed accordingly
350
+
351
+ a1 = mol.atomcoords[conf][neighbors_of_neighbor_indices[0]]
352
+ a2 = mol.atomcoords[conf][neighbors_of_neighbor_indices[1]]
353
+ pivot = norm(np.cross(a1 - self.coord, a2 - self.coord))
354
+
355
+ if mol.sigmatropic[conf]:
356
+ # two p lobes
357
+ self.center = np.concatenate(([pivot*orb_dim], [-pivot*orb_dim]))
358
+ self.subtype = 'p'
359
+
360
+ else:
361
+ #two n lobes
362
+ self.center = np.array([rot_mat_from_pointer(pivot, angle) @ self.vector for angle in (120,240)])
363
+ self.subtype = 'sp2'
364
+
365
+ elif len(neighbors_of_neighbor_indices) == 3:
366
+ # alkoxide, sulfonamide
367
+
368
+ v1, v2, v3 = mol.atomcoords[conf][neighbors_of_neighbor_indices] - self.coord
369
+ v1, v2, v3 = norm(v1), norm(v2), norm(v3)
370
+ v1, v2, v3 = v1*orb_dim, v2*orb_dim, v3*orb_dim
371
+ pivot = norm(np.cross(self.vector, v1))
372
+
373
+ self.center = np.array([rot_mat_from_pointer(pivot, 180) @ v for v in (v1, v2, v3)])
374
+ self.subtype = 'trilobe'
375
+
376
+ self.orb_vecs = np.array([norm(center) for center in self.center])
377
+ # unit vectors connecting reactive atom coord with orbital centers
378
+
379
+ self.center += self.coord
380
+ # two vectors defining the position of the two orbital lobes centers
381
+
382
+
383
+ class Imine:
384
+
385
+ def __repr__(self):
386
+ return 'Imine'
387
+
388
+ def init(self, mol, i, update=False, orb_dim=None, conf=0) -> None:
389
+ '''
390
+ '''
391
+ self.index = i
392
+ self.symbol = pt[mol.atomnos[i]].symbol
393
+ neighbors_indices = neighbors(mol.graph, i)
394
+
395
+
396
+
397
+ self.neighbors_symbols = [pt[mol.atomnos[i]].symbol for i in neighbors_indices]
398
+ self.coord = mol.atomcoords[conf][i]
399
+ self.others = mol.atomcoords[conf][neighbors_indices]
400
+
401
+ self.vectors = self.others - self.coord # vector connecting center to substituent
402
+
403
+ if update:
404
+ if orb_dim is None:
405
+ key = self.symbol + ' ' + str(self).split(' (')[0]
406
+ orb_dim = orb_dim_dict.get(key)
407
+
408
+ if orb_dim is None:
409
+ orb_dim = orb_dim_dict['Fallback']
410
+ print(f'ATTENTION: COULD NOT SETUP REACTIVE ATOM ORBITAL FROM PARAMETERS. We have no parameters for {key}. Using {orb_dim} A.')
411
+
412
+ if mol.sigmatropic[conf]:
413
+ # two p lobes
414
+ p_lobe = norm(np.cross(self.vectors[0], self.vectors[1]))*orb_dim
415
+ self.orb_vecs = np.concatenate(([p_lobe], [-p_lobe]))
416
+
417
+ else:
418
+ # lone pair lobe
419
+ self.orb_vecs = np.array([-norm(np.mean([norm(v) for v in self.vectors], axis=0))*orb_dim])
420
+
421
+ self.center = self.orb_vecs + self.coord
422
+ # two vectors defining the position of the two orbital lobes centers
423
+
424
+
425
+ class Sp_or_carbene:
426
+
427
+ def __repr__(self):
428
+ return self.type
429
+
430
+ def init(self, mol, i, update=False, orb_dim=None, conf=0) -> None:
431
+
432
+ self.index = i
433
+ self.symbol = pt[mol.atomnos[i]].symbol
434
+ neighbors_indices = neighbors(mol.graph, i)
435
+
436
+ self.neighbors_symbols = [pt[mol.atomnos[i]].symbol for i in neighbors_indices]
437
+
438
+ self.coord = mol.atomcoords[conf][i]
439
+ self.others = mol.atomcoords[conf][neighbors_indices]
440
+
441
+ self.vectors = self.others - self.coord # vector connecting center to substituent
442
+
443
+
444
+ angle = vec_angle(norm(self.others[0] - self.coord),
445
+ norm(self.others[1] - self.coord))
446
+
447
+ if np.abs(angle - 180) < 5:
448
+ self.type = 'sp'
449
+
450
+ else:
451
+ self.type = 'bent carbene'
452
+
453
+ self.allene = False
454
+ self.ketene = False
455
+ if self.type == 'sp' and all([s == 'C' for s in self.neighbors_symbols]):
456
+
457
+ neighbors_of_neighbors_indices = (neighbors(mol.graph, neighbors_indices[0]),
458
+ neighbors(mol.graph, neighbors_indices[1]))
459
+
460
+ neighbors_of_neighbors_indices[0].remove(i)
461
+ neighbors_of_neighbors_indices[1].remove(i)
462
+
463
+ if (len(side1) == len(side2) == 2 for side1, side2 in neighbors_of_neighbors_indices):
464
+ self.allene = True
465
+
466
+ elif self.type == 'sp' and sorted(self.neighbors_symbols) in (['C', 'O'], ['C', 'S']):
467
+
468
+ self.ketene = True
469
+
470
+ neighbors_of_neighbors_indices = (neighbors(mol.graph, neighbors_indices[0]),
471
+ neighbors(mol.graph, neighbors_indices[1]))
472
+
473
+ neighbors_of_neighbors_indices[0].remove(i)
474
+ neighbors_of_neighbors_indices[1].remove(i)
475
+
476
+ if len(neighbors_of_neighbors_indices[0]) == 2:
477
+ substituent = mol.atomcoords[conf][neighbors_of_neighbors_indices[0][0]]
478
+ ketene_atom = mol.atomcoords[conf][neighbors_indices[0]]
479
+ self.ketene_ref = substituent - ketene_atom
480
+
481
+ elif len(neighbors_of_neighbors_indices[1]) == 2:
482
+ substituent = mol.atomcoords[conf][neighbors_of_neighbors_indices[1][0]]
483
+ ketene_atom = mol.atomcoords[conf][neighbors_indices[1]]
484
+ self.ketene_ref = substituent - ketene_atom
485
+
486
+ else:
487
+ self.ketene = False
488
+
489
+ if update:
490
+ if orb_dim is None:
491
+ key = self.symbol + ' ' + self.type
492
+ orb_dim = orb_dim_dict.get(key)
493
+
494
+ if orb_dim is None:
495
+ orb_dim = orb_dim_dict['Fallback']
496
+ print(f'ATTENTION: COULD NOT SETUP REACTIVE ATOM ORBITAL FROM PARAMETERS. We have no parameters for {key}. Using {orb_dim} A.')
497
+
498
+ if self.type == 'sp':
499
+
500
+ v = np.random.rand(3)
501
+ pivot1 = v - ((v @ norm(self.vectors[0])) * self.vectors[0])
502
+
503
+ if self.allene or self.ketene:
504
+ # if we have an allene or ketene, pivot1 is aligned to
505
+ # one substituent so that the resulting positions
506
+ # for the four orbital centers make chemical sense.
507
+
508
+ axis = norm(self.others[0] - self.others[1])
509
+ # versor connecting reactive atom neighbors
510
+
511
+ if self.allene:
512
+ ref = (mol.atomcoords[conf][neighbors_of_neighbors_indices[0][0]] -
513
+ mol.atomcoords[conf][neighbors_indices[0]])
514
+ else:
515
+ ref = self.ketene_ref
516
+
517
+ pivot1 = ref - ref @ axis * axis
518
+ # projection of ref orthogonal to axis (vector rejection)
519
+
520
+
521
+ pivot2 = norm(np.cross(pivot1, self.vectors[0]))
522
+
523
+ self.orb_vecs = np.array([rot_mat_from_pointer(pivot2, 90) @
524
+ rot_mat_from_pointer(pivot1, angle) @
525
+ norm(self.vectors[0]) for angle in (0, 90, 180, 270)]) * orb_dim
526
+
527
+ self.center = self.orb_vecs + self.coord
528
+ # four vectors defining the position of the four orbital lobes centers
529
+
530
+
531
+
532
+ else: # bent carbene case: three centers, sp2+p
533
+
534
+ self.orb_vecs = np.array([-norm(np.mean([norm(v) for v in self.vectors], axis=0))*orb_dim])
535
+ # one sp2 center first
536
+
537
+ p_vec = np.cross(norm(self.vectors[0]), norm(self.vectors[1]))
538
+ p_vecs = np.array([norm(p_vec)*orb_dim, -norm(p_vec)*orb_dim])
539
+ self.orb_vecs = np.concatenate((self.orb_vecs, p_vecs))
540
+ # adding two p centers
541
+
542
+ self.center = self.orb_vecs + self.coord
543
+ # three vectors defining the position of the two p lobes and main sp2 lobe centers
544
+
545
+
546
+ class Metal:
547
+
548
+ def __repr__(self):
549
+ return 'Metal'
550
+
551
+ def init(self, mol, i, update=False, orb_dim=None, conf=0) -> None:
552
+
553
+ self.index = i
554
+ self.symbol = pt[mol.atomnos[i]].symbol
555
+ neighbors_indices = neighbors(mol.graph, i)
556
+
557
+ self.neighbors_symbols = [pt[mol.atomnos[i]].symbol for i in neighbors_indices]
558
+ self.coord = mol.atomcoords[conf][i]
559
+ self.others = mol.atomcoords[conf][neighbors_indices]
560
+
561
+ self.vectors = self.others - self.coord # vectors connecting reactive atom with neighbors
562
+
563
+ v1 = self.vectors[0]
564
+ # v1 connects first bonded atom to the metal itself
565
+
566
+ neighbor_of_neighbor_index = neighbors(mol.graph, neighbors_indices[0])[0]
567
+ v2 = mol.atomcoords[conf][neighbor_of_neighbor_index] - self.coord
568
+ # v2 connects first neighbor of the first neighbor to the metal itself
569
+
570
+ self.orb_vec = norm(rot_mat_from_pointer(np.cross(v1, v2), 120) @ v1)
571
+ # setting the pointer (orb_vec) so that orbitals are oriented correctly
572
+ # (Lithium enolate in mind)
573
+
574
+ steps = 4 # number of total orbitals
575
+ self.orb_vecs = np.array([rot_mat_from_pointer(v1, angle) @ self.orb_vec for angle in range(0,360,int(360/steps))])
576
+
577
+ if update:
578
+ if orb_dim is None:
579
+ orb_dim = orb_dim_dict[str(self)]
580
+
581
+ self.center = (self.orb_vecs * orb_dim) + self.coord
582
+
583
+ # Keys are made of atom symbol and number of bonds that it makes
584
+ atom_type_dict = {
585
+ 'H1' : Single,
586
+
587
+ 'B3' : Sp2,
588
+ 'B4' : Sp3,
589
+
590
+ 'C1' : Single, # deprotonated terminal alkyne. What if it is a carbylidene? Very rare by the way...
591
+ 'C2' : Sp_or_carbene, # sp if straight, carbene if bent
592
+ 'C3' : Sp2, # double ball
593
+ 'C4' : Sp3, # one ball, on the back of the leaving group. If we can't tell which one it is, we ask user
594
+
595
+ 'N1' : Single,
596
+ 'N2' : Imine, # one ball on free side
597
+ 'N3' : Sp2, # double ball
598
+ 'N4' : Sp3, # leaving group
599
+
600
+ 'O1' : Ketone, # two balls 120° apart. Also for alkoxides, good enough
601
+ 'O2' : Ether, # or alcohol, two balls about 109,5° apart
602
+
603
+ 'P2' : Imine, # one ball on free side
604
+ 'P3' : Sp2, # double ball
605
+ 'P4' : Sp3, # leaving group
606
+
607
+ 'S1' : Ketone,
608
+ 'S2' : Ether,
609
+ 'S3' : Sp2, # Not sure if this can be valid, but it's basically treating it as a bent carbonyl, should work
610
+ # 'S3' : Sulphoxide, # Should we consider this? Or just ok with Sp2()?
611
+ # 'S4' : Sulphone,
612
+
613
+ 'F1' : Single,
614
+ 'Cl1': Single,
615
+ 'Br1': Single,
616
+ 'I1' : Single,
617
+
618
+ ############### Name associations
619
+
620
+ 'Single' : Single,
621
+ 'Sp2' : Sp2,
622
+ 'Sp3' : Sp3,
623
+ 'Ether' : Ether,
624
+ 'Ketone' : Ketone,
625
+ 'Imine' : Imine,
626
+ 'Sp_or_carbene' : Sp_or_carbene,
627
+ 'Metal' : Metal,
628
+
629
+ }
630
+
631
+ metals = (
632
+ 'Li',
633
+ 'Na',
634
+ 'Mg',
635
+ 'K',
636
+ 'Ca',
637
+ 'Ti',
638
+ 'Rb',
639
+ 'Sr',
640
+ 'Cs',
641
+ 'Ba',
642
+ 'Zn',
643
+ )
644
+
645
+ for metal in metals:
646
+ for bonds in range(1,9):
647
+ bonds = str(bonds)
648
+ atom_type_dict[metal+bonds] = Metal
649
+
650
+ def get_atom_type(graph, index, override=None):
651
+ '''
652
+ Returns the appropriate class to represent
653
+ the atom with the given index on the graph.
654
+ If override is not None, returns the class
655
+ with that name.
656
+ '''
657
+ if override is not None:
658
+ return atom_type_dict[override]
659
+
660
+ nb = neighbors(graph, index)
661
+ code = pt[graph.nodes[index]['atomnos']].symbol + str(len(nb))
662
+ try:
663
+ return atom_type_dict[code]
664
+
665
+ except KeyError:
666
+ raise KeyError(f'Orbital type {code} not known (index {index})')
firecode/references.py ADDED
@@ -0,0 +1,11 @@
1
+ # coding=utf-8
2
+
3
+ from firecode.__main__ import __version__
4
+
5
+ references = {
6
+ 'FIRECODE' : f'Tampellini, N. FIRECODE {__version__}, Github - https://github.com/ntampellini/firecode',
7
+ 'GFN-FF' : 'Spicher, S.; Grimme, S. Angew. Chem. Int. Ed. 2020, 59, 15665-15673',
8
+ 'GFN2-XTB': 'Bannwarth, C.; Ehlert, S.; Grimme, S. J. Chem. Theory Comput. 2019, 15, 1652-1671',
9
+ 'CREST': 'Pracht, P.; Bohle, F.; Grimme, S. PCCP 2020, 22, 7169-7192',
10
+ 'ALPB': 'Ehlert, S.; Stahn, M.; Spicher, S.; Grimme, S. J. Chem. Theory Comput. 2021, 17, 4250-4261',
11
+ }