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
@@ -0,0 +1,609 @@
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
+ import time
25
+ from copy import deepcopy
26
+
27
+ import numpy as np
28
+ from scipy.spatial.transform import Rotation as R
29
+
30
+ from firecode.algebra import norm, norm_of
31
+ from firecode.ase_manipulations import ase_neb, ase_popt
32
+ from firecode.calculators._gaussian import gaussian_opt
33
+ from firecode.calculators._mopac import mopac_opt
34
+ from firecode.calculators._orca import orca_opt
35
+ from firecode.calculators._xtb import xtb_opt
36
+ from firecode.pruning import prune_by_rmsd
37
+ from firecode.settings import DEFAULT_LEVELS
38
+ from firecode.utils import (loadbar, molecule_check, pt, scramble_check,
39
+ time_to_string, write_xyz)
40
+
41
+
42
+ class Opt_func_dispatcher:
43
+
44
+ def __init__(self):
45
+
46
+ self.opt_funcs_dict = {
47
+ 'MOPAC':mopac_opt,
48
+ 'ORCA':orca_opt,
49
+ 'GAUSSIAN':gaussian_opt,
50
+ 'XTB':xtb_opt,
51
+ }
52
+
53
+ def load_aimnet2_calc(self, theory_level, logfunction=print):
54
+
55
+ try:
56
+ from aimnet2_firecode.interface import aimnet2_opt, get_aimnet2_calc
57
+
58
+ except ImportError:
59
+ raise Exception(('Cannot import AIMNet2 python bindings for FIRECODE. Install them with:\n'
60
+ '>>> pip install aimnet2-firecode'))
61
+
62
+ self.opt_funcs_dict['AIMNET2'] = aimnet2_opt
63
+ self.aimnet2_model = get_aimnet2_calc(theory_level, logfunction=logfunction)
64
+
65
+ def optimize(
66
+ coords,
67
+ atomnos,
68
+ calculator,
69
+ method=None,
70
+ maxiter=None,
71
+ conv_thr="tight",
72
+ constrained_indices=None,
73
+ constrained_distances=None,
74
+ mols_graphs=None,
75
+ procs=1,
76
+ solvent=None,
77
+ charge=0,
78
+ max_newbonds=0,
79
+ title='temp',
80
+ check=True,
81
+ logfunction=None,
82
+
83
+ dispatcher=None,
84
+ **kwargs,
85
+ ):
86
+ '''
87
+ Performs a geometry [partial] optimization (OPT/POPT) with MOPAC, ORCA, Gaussian or XTB at $method level,
88
+ constraining the distance between the specified atom pairs, if any. Moreover, if $check, performs a check on atomic
89
+ pairs distances to ensure that the optimization has preserved molecular identities and no atom scrambling occurred.
90
+
91
+ :params calculator: Calculator to be used. ('MOPAC', 'ORCA', 'GAUSSIAN', 'XTB', 'AIMNET2')
92
+ :params coords: list of coordinates for each atom in the TS
93
+ :params atomnos: list of atomic numbers for each atom in the TS
94
+ :params mols_graphs: list of molecule.graph objects, containing connectivity information for each molecule
95
+ :params constrained_indices: indices of constrained atoms in the TS geometry, if this is one
96
+ :params method: Level of theory to be used in geometry optimization. Default if UFF.
97
+
98
+ :return opt_coords: optimized structure
99
+ :return energy: absolute energy of structure, in kcal/mol
100
+ :return not_scrambled: bool, indicating if the optimization shifted up some bonds (except the constrained ones)
101
+ '''
102
+
103
+ if dispatcher is None:
104
+ dispatcher = Opt_func_dispatcher()
105
+
106
+ if calculator == 'AIMNET2':
107
+ dispatcher.load_aimnet2_calc(method)
108
+
109
+ if mols_graphs is not None:
110
+ _l = [len(graph.nodes) for graph in mols_graphs]
111
+ assert len(coords) == sum(_l), f'{len(coords)} coordinates are specified but graphs have {_l} = {sum(_l)} nodes'
112
+
113
+ if method is None:
114
+ method = DEFAULT_LEVELS[calculator]
115
+
116
+ if constrained_distances is not None:
117
+ assert len(constrained_distances) == len(constrained_indices), f'len(cd) = {len(constrained_distances)} != len(ci) = {len(constrained_indices)}'
118
+
119
+ constrained_indices = np.array(()) if constrained_indices is None else constrained_indices
120
+
121
+ # opt_func = opt_funcs_dict[calculator]
122
+ opt_func = dispatcher.opt_funcs_dict[calculator]
123
+
124
+ t_start = time.perf_counter()
125
+
126
+ # success checks that calculation had a normal termination
127
+ opt_coords, energy, success = opt_func(coords,
128
+ atomnos,
129
+ constrained_indices=constrained_indices,
130
+ constrained_distances=constrained_distances,
131
+ method=method,
132
+ procs=procs,
133
+ solvent=solvent,
134
+ maxiter=maxiter,
135
+ conv_thr=conv_thr,
136
+ title=title,
137
+ charge=charge,
138
+
139
+ ase_calc = dispatcher.aimnet2_model if calculator == 'AIMNET2' else None,
140
+ **kwargs)
141
+
142
+ elapsed = time.perf_counter() - t_start
143
+
144
+ if success:
145
+ if check:
146
+ # check boolean ensures that no scrambling occurred during the optimization
147
+ if mols_graphs is not None:
148
+ success = scramble_check(opt_coords, atomnos, constrained_indices, mols_graphs, max_newbonds=max_newbonds)
149
+ else:
150
+ success = molecule_check(coords, opt_coords, atomnos, max_newbonds=max_newbonds)
151
+
152
+ if logfunction is not None:
153
+ if success:
154
+ logfunction(f' - {title} - REFINED {time_to_string(elapsed)}')
155
+ else:
156
+ logfunction(f' - {title} - SCRAMBLED {time_to_string(elapsed)}')
157
+
158
+ return opt_coords, energy, success
159
+
160
+ if logfunction is not None:
161
+ logfunction(f' - {title} - CRASHED')
162
+
163
+ return coords, energy, False
164
+
165
+ def hyperNEB(embedder, coords, atomnos, ids, constrained_indices, title='temp'):
166
+ '''
167
+ Turn a geometry close to TS to a proper TS by getting
168
+ reagents and products and running a climbing image NEB calculation through ASE.
169
+ '''
170
+
171
+ reagents = get_reagent(embedder, coords, atomnos, ids, constrained_indices, method=embedder.options.theory_level)
172
+ products = get_product(embedder, coords, atomnos, ids, constrained_indices, method=embedder.options.theory_level)
173
+ # get reagents and products for this reaction
174
+
175
+ reagents -= np.mean(reagents, axis=0)
176
+ products -= np.mean(products, axis=0)
177
+ # centering both structures on the centroid of reactive atoms
178
+
179
+ aligment_rotation = R.align_vectors(reagents, products)
180
+ # products = np.array([aligment_rotation @ v for v in products])
181
+ products = (aligment_rotation @ products.T).T
182
+ # rotating the two structures to minimize differences
183
+
184
+ ts_coords, ts_energy, energies, success = ase_neb(embedder, reagents, products, atomnos, title=title)
185
+ # Use these structures plus the TS guess to run a NEB calculation through ASE
186
+
187
+ return ts_coords, ts_energy, energies, success
188
+
189
+ def get_product(embedder, coords, atomnos, ids, constrained_indices, method='PM7'):
190
+ '''
191
+ Part of the automatic NEB implementation.
192
+ Returns a structure that presumably is the association reaction product
193
+ ([cyclo]additions reactions in mind)
194
+ '''
195
+
196
+ opt_func = embedder.dispatcher.opt_funcs_dict[embedder.options.calculator]
197
+
198
+ bond_factor = 1.2
199
+ # multiple of sum of covalent radii for two atoms.
200
+ # If two atoms are closer than this times their sum
201
+ # of c_radii, they are considered to converge to
202
+ # products when their geometry is optimized.
203
+
204
+ step_size = 0.1
205
+ # in Angstroms
206
+
207
+ if len(ids) == 2:
208
+
209
+ mol1_center = np.mean([coords[a] for a, _ in constrained_indices], axis=0)
210
+ mol2_center = np.mean([coords[b] for _, b in constrained_indices], axis=0)
211
+ motion = norm(mol2_center - mol1_center)
212
+ # norm of the motion that, when applied to mol1,
213
+ # superimposes its reactive atoms to the ones of mol2
214
+
215
+ threshold_dists = [bond_factor*(pt[atomnos[a]].covalent_radius +
216
+ pt[atomnos[b]].covalent_radius) for a, b in constrained_indices]
217
+
218
+ reactive_dists = [norm_of(coords[a] - coords[b]) for a, b in constrained_indices]
219
+ # distances between reactive atoms
220
+
221
+ while not np.all([reactive_dists[i] < threshold_dists[i] for i, _ in enumerate(constrained_indices)]):
222
+ # print('Reactive distances are', reactive_dists)
223
+
224
+ coords[:ids[0]] += motion*step_size
225
+
226
+ coords, _, _ = opt_func(coords, atomnos, constrained_indices, method=method)
227
+
228
+ reactive_dists = [norm_of(coords[a] - coords[b]) for a, b in constrained_indices]
229
+
230
+ newcoords, _, _ = opt_func(coords, atomnos, method=method)
231
+ # finally, when structures are close enough, do a free optimization to get the reaction product
232
+
233
+ new_reactive_dists = [norm_of(newcoords[a] - newcoords[b]) for a, b in constrained_indices]
234
+
235
+ if np.all([new_reactive_dists[i] < threshold_dists[i] for i, _ in enumerate(constrained_indices)]):
236
+ # return the freely optimized structure only if the reagents did not repel each other
237
+ # during the optimization, otherwise return the last coords, where partners were close
238
+ return newcoords
239
+
240
+ return coords
241
+
242
+ # trimolecular TSs: the approach is to bring the first pair of reactive
243
+ # atoms closer until optimization bounds the molecules together
244
+
245
+ index_to_be_moved = constrained_indices[0,0]
246
+ reference = constrained_indices[0,1]
247
+ moving_molecule_index = next(i for i,n in enumerate(np.cumsum(ids)) if index_to_be_moved < n)
248
+ bounds = [0] + [n+1 for n in np.cumsum(ids)]
249
+ moving_molecule_slice = slice(bounds[moving_molecule_index], bounds[moving_molecule_index+1])
250
+ threshold_dist = bond_factor*(pt[atomnos[constrained_indices[0,0]]].covalent_radius +
251
+ pt[atomnos[constrained_indices[0,1]]].covalent_radius)
252
+
253
+ motion = (coords[reference] - coords[index_to_be_moved])
254
+ # vector from the atom to be moved to the target reactive atom
255
+
256
+ while norm_of(motion) > threshold_dist:
257
+ # check if the reactive atoms are sufficiently close to converge to products
258
+
259
+ for i, atom in enumerate(coords[moving_molecule_slice]):
260
+ dist = norm_of(atom - coords[index_to_be_moved])
261
+ # for any atom in the molecule, distance from the reactive atom
262
+
263
+ atom_step = step_size*np.exp(-0.5*dist)
264
+ coords[moving_molecule_slice][i] += norm(motion)*atom_step
265
+ # the more they are close, the more they are moved
266
+
267
+ # print('Reactive dist -', norm_of(motion))
268
+ coords, _, _ = opt_func(coords, atomnos, constrained_indices, method=method)
269
+ # when all atoms are moved, optimize the geometry with the previous constraints
270
+
271
+ motion = (coords[reference] - coords[index_to_be_moved])
272
+
273
+ newcoords, _, _ = opt_func(coords, atomnos, method=method)
274
+ # finally, when structures are close enough, do a free optimization to get the reaction product
275
+
276
+ new_reactive_dist = norm_of(newcoords[constrained_indices[0,0]] - newcoords[constrained_indices[0,0]])
277
+
278
+ if new_reactive_dist < threshold_dist:
279
+ # return the freely optimized structure only if the reagents did not repel each other
280
+ # during the optimization, otherwise return the last coords, where partners were close
281
+ return newcoords
282
+
283
+ return coords
284
+
285
+ def get_reagent(embedder, coords, atomnos, ids, constrained_indices, method='PM7'):
286
+ '''
287
+ Part of the automatic NEB implementation.
288
+ Returns a structure that presumably is the association reaction reagent.
289
+ ([cyclo]additions reactions in mind)
290
+ '''
291
+
292
+ opt_func = embedder.dispatcher.opt_funcs_dict[embedder.options.calculator]
293
+
294
+ bond_factor = 1.5
295
+ # multiple of sum of covalent radii for two atoms.
296
+ # Putting reactive atoms at this times their bonding
297
+ # distance and performing a constrained optimization
298
+ # is the way to get a good guess for reagents structure.
299
+
300
+ if len(ids) == 2:
301
+
302
+ mol1_center = np.mean([coords[a] for a, _ in constrained_indices], axis=0)
303
+ mol2_center = np.mean([coords[b] for _, b in constrained_indices], axis=0)
304
+ motion = norm(mol2_center - mol1_center)
305
+ # norm of the motion that, when applied to mol1,
306
+ # superimposes its reactive centers to the ones of mol2
307
+
308
+ threshold_dists = [bond_factor*(pt[atomnos[a]].covalent_radius + pt[atomnos[b]].covalent_radius) for a, b in constrained_indices]
309
+
310
+ reactive_dists = [norm_of(coords[a] - coords[b]) for a, b in constrained_indices]
311
+ # distances between reactive atoms
312
+
313
+ coords[:ids[0]] -= norm(motion)*(np.mean(threshold_dists) - np.mean(reactive_dists))
314
+ # move reactive atoms away from each other just enough
315
+
316
+ coords, _, _ = opt_func(coords, atomnos, constrained_indices=constrained_indices, method=method)
317
+ # optimize the structure but keeping the reactive atoms distanced
318
+
319
+ return coords
320
+
321
+ # trimolecular TSs: the approach is to bring the first pair of reactive
322
+ # atoms apart just enough to get a good approximation for reagents
323
+
324
+ index_to_be_moved = constrained_indices[0,0]
325
+ reference = constrained_indices[0,1]
326
+ moving_molecule_index = next(i for i,n in enumerate(np.cumsum(ids)) if index_to_be_moved < n)
327
+ bounds = [0] + [n+1 for n in np.cumsum(ids)]
328
+ moving_molecule_slice = slice(bounds[moving_molecule_index], bounds[moving_molecule_index+1])
329
+ threshold_dist = bond_factor*(pt[atomnos[constrained_indices[0,0]]].covalent_radius +
330
+ pt[atomnos[constrained_indices[0,1]]].covalent_radius)
331
+
332
+ motion = (coords[reference] - coords[index_to_be_moved])
333
+ # vector from the atom to be moved to the target reactive atom
334
+
335
+ displacement = norm(motion)*(threshold_dist-norm_of(motion))
336
+ # vector to be applied to the reactive atom to push it far just enough
337
+
338
+ for i, atom in enumerate(coords[moving_molecule_slice]):
339
+ dist = norm_of(atom - coords[index_to_be_moved])
340
+ # for any atom in the molecule, distance from the reactive atom
341
+
342
+ coords[moving_molecule_slice][i] -= displacement*np.exp(-0.5*dist)
343
+ # the closer they are to the reactive atom, the further they are moved
344
+
345
+ coords, _, _ = opt_func(coords, atomnos, constrained_indices=np.array([constrained_indices[0]]), method=method)
346
+ # when all atoms are moved, optimize the geometry with only the first of the previous constraints
347
+
348
+ newcoords, _, _ = opt_func(coords, atomnos, method=method)
349
+ # finally, when structures are close enough, do a free optimization to get the reaction product
350
+
351
+ new_reactive_dist = norm_of(newcoords[constrained_indices[0,0]] - newcoords[constrained_indices[0,0]])
352
+
353
+ if new_reactive_dist > threshold_dist:
354
+ # return the freely optimized structure only if the reagents did not approached back each other
355
+ # during the optimization, otherwise return the last coords, where partners were further away
356
+ return newcoords
357
+
358
+ return coords
359
+
360
+ def opt_linear_scan(embedder, coords, atomnos, scan_indices, constrained_indices, step_size=0.02, safe=False, title='temp', logfile=None, xyztraj=None):
361
+ '''
362
+ Runs a linear scan along the specified linear coordinate.
363
+ The highest energy structure that passes sanity checks is returned.
364
+
365
+ embedder
366
+ coords
367
+ atomnos
368
+ scan_indices
369
+ constrained_indices
370
+ step_size
371
+ safe
372
+ title
373
+ logfile
374
+ xyztraj
375
+ '''
376
+ assert [i in constrained_indices.ravel() for i in scan_indices]
377
+
378
+ i1, i2 = scan_indices
379
+ far_thr = 2 * sum([pt[atomnos[i]].covalent_radius for i in scan_indices])
380
+ t_start = time.perf_counter()
381
+ total_iter = 0
382
+
383
+ _, energy, _ = optimize(coords,
384
+ atomnos,
385
+ embedder.options.calculator,
386
+ embedder.options.theory_level,
387
+ constrained_indices=constrained_indices,
388
+ mols_graphs=embedder.graphs,
389
+ procs=embedder.procs,
390
+ max_newbonds=embedder.options.max_newbonds,
391
+ )
392
+
393
+ direction = coords[i1] - coords[i2]
394
+ base_dist = norm_of(direction)
395
+ energies, geometries = [energy], [coords]
396
+
397
+ for sign in (1, -1):
398
+ # getting closer for sign == 1, further apart for -1
399
+ active_coords = deepcopy(coords)
400
+ dist = base_dist
401
+
402
+ if scan_peak_present(energies):
403
+ break
404
+
405
+ for iterations in range(75):
406
+
407
+ if safe: # use ASE optimization function - more reliable, but locks all interatomic dists
408
+
409
+ targets = [norm_of(active_coords[a]-active_coords[b]) - step_size
410
+ if (a in scan_indices and b in scan_indices)
411
+ else norm_of(active_coords[a]-active_coords[b])
412
+ for a, b in constrained_indices]
413
+
414
+ active_coords, energy, success = ase_popt(embedder,
415
+ active_coords,
416
+ atomnos,
417
+ constrained_indices,
418
+ targets=targets,
419
+ safe=True,
420
+ )
421
+
422
+ else: # use faster raw optimization function, might scramble more often than the ASE one
423
+
424
+ active_coords[i2] += sign * norm(direction) * step_size
425
+ active_coords, energy, success = optimize(active_coords,
426
+ atomnos,
427
+ embedder.options.calculator,
428
+ embedder.options.theory_level,
429
+ constrained_indices=constrained_indices,
430
+ mols_graphs=embedder.graphs,
431
+ procs=embedder.procs,
432
+ max_newbonds=embedder.options.max_newbonds,
433
+ )
434
+
435
+ if not success:
436
+ if logfile is not None and iterations == 0:
437
+ logfile.write(f' - {title} CRASHED at first step\n')
438
+
439
+ if embedder.options.debug:
440
+ with open(title+'_SCRAMBLED.xyz', 'a') as f:
441
+ write_xyz(active_coords, atomnos, f, title=title+(
442
+ f' d({i1}-{i2}) = {round(dist, 3)} A, Rel. E = {round(energy-energies[0], 3)} kcal/mol'))
443
+
444
+ break
445
+
446
+ direction = active_coords[i1] - active_coords[i2]
447
+ dist = norm_of(direction)
448
+
449
+ total_iter += 1
450
+ geometries.append(active_coords)
451
+ energies.append(energy)
452
+
453
+ if xyztraj is not None:
454
+ with open(xyztraj, 'a') as f:
455
+ write_xyz(active_coords, atomnos, f, title=title+(
456
+ f' d({i1}-{i2}) = {round(dist, 3)} A, Rel. E = {round(energy-energies[0], 3)} kcal/mol'))
457
+
458
+ if (dist < 1.2 and sign == 1) or (
459
+ dist > far_thr and sign == -1) or (
460
+ scan_peak_present(energies)
461
+ ):
462
+ break
463
+
464
+ distances = [norm_of(g[i1]-g[i2]) for g in geometries]
465
+ best_distance = distances[energies.index(max(energies))]
466
+
467
+ distances_delta = [abs(d-best_distance) for d in distances]
468
+ closest_geom = geometries[distances_delta.index(min(distances_delta))]
469
+ closest_dist = distances[distances_delta.index(min(distances_delta))]
470
+
471
+ direction = closest_geom[i1] - closest_geom[i2]
472
+ closest_geom[i1] += norm(direction) * (best_distance-closest_dist)
473
+
474
+ final_geom, final_energy, _ = optimize(closest_geom,
475
+ atomnos,
476
+ embedder.options.calculator,
477
+ embedder.options.theory_level,
478
+ constrained_indices=constrained_indices,
479
+ mols_graphs=embedder.graphs,
480
+ procs=embedder.procs,
481
+ max_newbonds=embedder.options.max_newbonds,
482
+ check=False,
483
+ )
484
+
485
+ if embedder.options.debug:
486
+
487
+ if embedder.options.debug:
488
+ with open(xyztraj, 'a') as f:
489
+ write_xyz(active_coords, atomnos, f, title=title+(
490
+ f' FINAL - d({i1}-{i2}) = {round(norm_of(final_geom[i1]-final_geom[i2]), 3)} A,'
491
+ f' Rel. E = {round(final_energy-energies[0], 3)} kcal/mol'))
492
+
493
+ import matplotlib.pyplot as plt
494
+
495
+ plt.figure()
496
+
497
+ distances = [norm_of(geom[i1]-geom[i2]) for geom in geometries]
498
+ distances, sorted_energies = zip(*sorted(zip(distances, energies), key=lambda x: x[0]))
499
+
500
+ plt.plot(distances,
501
+ [s-energies[0] for s in sorted_energies],
502
+ '-o',
503
+ color='tab:red',
504
+ label=f'Linear SCAN ({i1}-{i2})',
505
+ linewidth=3,
506
+ alpha=0.5)
507
+
508
+ plt.plot(norm_of(coords[i1]-coords[i2]),
509
+ 0,
510
+ marker='o',
511
+ color='tab:blue',
512
+ label='Starting point (0 kcal/mol)',
513
+ markersize=5,
514
+ )
515
+
516
+ plt.plot(best_distance,
517
+ final_energy-energies[0],
518
+ marker='o',
519
+ color='black',
520
+ label='Interpolated best distance, actual energy',
521
+ markersize=5)
522
+
523
+ plt.legend()
524
+ plt.title(title)
525
+ plt.xlabel(f'Interatomic distance {tuple(scan_indices)}')
526
+ plt.ylabel('Energy Rel. to starting point (kcal/mol)')
527
+ plt.savefig(f'{title.replace(" ", "_")}_plt.svg')
528
+
529
+ if logfile is not None:
530
+ logfile.write(f' - {title} COMPLETED {total_iter} steps ({time_to_string(time.perf_counter()-t_start)})\n')
531
+
532
+ return final_geom, final_energy, True
533
+
534
+ def scan_peak_present(energies) -> bool:
535
+ '''
536
+ Returns True if the maximum value of the list
537
+ occurs in the middle of it, that is not in first,
538
+ second, second to last or last positions
539
+ '''
540
+ if energies.index(max(energies)) in range(2,len(energies)-1):
541
+ return True
542
+ return False
543
+
544
+ def fitness_check(coords, constraints, targets, threshold) -> bool:
545
+ '''
546
+ Returns True if the strucure respects
547
+ the imposed pairings specified in constraints.
548
+ targets: target distances for each constraint
549
+ threshold: cumulative threshold to reject a structure (A)
550
+
551
+ '''
552
+ error = 0
553
+ for (a, b), target in zip(constraints, targets):
554
+ if target is not None:
555
+ error += (norm_of(coords[a]-coords[b]) - target)
556
+
557
+ return error < threshold
558
+
559
+ def _refine_structures(structures,
560
+ atomnos,
561
+ calculator,
562
+ method,
563
+ procs,
564
+ constrained_indices=None,
565
+ constrained_distances=None,
566
+ solvent=None,
567
+ loadstring='',
568
+ logfunction=None):
569
+ '''
570
+ Refine a set of structures - optimize them and remove similar
571
+ ones and high energy ones (>20 kcal/mol above lowest)
572
+ '''
573
+ energies = []
574
+ for i, conformer in enumerate(deepcopy(structures)):
575
+
576
+ loadbar(i, len(structures), f'{loadstring} {i+1}/{len(structures)} ')
577
+
578
+ opt_coords, energy, success = optimize(
579
+ conformer,
580
+ atomnos,
581
+ calculator,
582
+ constrained_indices=constrained_indices,
583
+ constrained_distances=constrained_distances,
584
+ method=method,
585
+ procs=procs,
586
+ solvent=solvent,
587
+ title=f'Structure_{i+1}',
588
+ logfunction=logfunction,
589
+ check=False, # a change in bonding topology is possible and should not be prevented
590
+ )
591
+
592
+ if success:
593
+ structures[i] = opt_coords
594
+ energies.append(energy)
595
+ else:
596
+ energies.append(1E10)
597
+
598
+ loadbar(len(structures), len(structures), f'{loadstring} {len(structures)}/{len(structures)} ')
599
+ energies = np.array(energies)
600
+
601
+ # remove similar ones
602
+ structures, mask = prune_by_rmsd(structures, atomnos)
603
+ energies = energies[mask]
604
+
605
+ # remove high energy ones
606
+ mask = (energies - np.min(energies)) < 20
607
+ structures, energies = structures[mask], energies[mask]
608
+
609
+ return structures, energies
firecode/parameters.py ADDED
@@ -0,0 +1,84 @@
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
+ orb_dim_dict = {
25
+ 'H Single Bond' : 0.85,
26
+ 'C Single Bond' : 1,
27
+ 'O Single Bond' : 1,
28
+ 'N Single Bond' : 1,
29
+ 'F Single Bond' : 1,
30
+ 'Cl Single Bond' : 1.5,
31
+ 'Br Single Bond' : 1.5,
32
+ 'I Single Bond' : 2,
33
+
34
+ 'C sp' : 1,
35
+ 'N sp' : 1,
36
+
37
+ 'B sp2' : 0.8,
38
+ 'C sp2' : 1.1,
39
+ 'N sp2' : 1,
40
+
41
+ 'B sp3' : 1,
42
+ 'C sp3' : 1,
43
+ 'Br sp3' : 1,
44
+
45
+ 'O Ether' : 1,
46
+ 'S Ether' : 1,
47
+
48
+ 'O Ketone': 0.85,
49
+ 'S Ketone': 1,
50
+
51
+ 'N Imine' : 1,
52
+
53
+ 'C bent carbene' : 1,
54
+
55
+ 'Metal' : 2.5,
56
+
57
+ 'Fallback' : 1
58
+ }
59
+ # Half-lenght of the transition state bonding distance involving a given atom
60
+
61
+ nci_dict={
62
+ # tag in alphabetical order (i.e. 'IN' and not 'NI')
63
+ # maximum distance for a given non-covalent interaction
64
+
65
+ # Hydrogen Bonds
66
+ 'HO' :(2.2,'O-H hydrogen bond'),
67
+ 'HN' :(2.2,'N-H hydrogen bond'),
68
+
69
+ # Aromatics and Stacking
70
+ 'HPh' :(2.8,'H-Ar non-conventional hydrogen bond'), # taken from https://doi.org/10.1039/C1CP20404A
71
+ 'PhPh':(3.8, 'pi-stacking interaction'), # guessed from https://doi.org/10.1039/C2SC20045G
72
+
73
+ # Halogens
74
+ 'FF' :(3.5,'F-F interaction'),
75
+ # 'FPh' :(0.0, 'F-Ar halogen-bonding interaction'),
76
+ # 'ClPh':(0.0, 'Cl-Ar halogen-bonding interaction'),
77
+ # 'BrPh':(0.0, 'Br-Ar halogen-bonding interaction'),
78
+ # 'IPh' :(0.0, 'I-Ar halogen-bonding interaction'),
79
+ # 'IN' :(0.0, 'I-N halogen-bonding interaction'),
80
+ # 'IO' :(0.0, 'I-O halogen-bonding interaction'),
81
+ # 'BrN' :(0.0, 'Br-N halogen-bonding interaction'),
82
+ # 'BrO' :(0.0, 'Br-O halogen-bonding interaction'),
83
+ }
84
+ # non covalent interaction threshold and types for atomic pairs