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,516 @@
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 time import time
25
+
26
+ import numpy as np
27
+ from ase import Atoms
28
+ from ase.constraints import FixInternals
29
+ from ase.optimize import LBFGS
30
+ from networkx.algorithms.components.connected import connected_components
31
+ from networkx.algorithms.shortest_paths.generic import shortest_path
32
+
33
+ from firecode.algebra import dihedral
34
+ from firecode.ase_manipulations import ase_neb, ase_saddle, get_ase_calc
35
+ from firecode.errors import ZeroCandidatesError
36
+ from firecode.hypermolecule_class import graphize
37
+ from firecode.optimization_methods import optimize
38
+ from firecode.pruning import prune_by_rmsd
39
+ from firecode.utils import (align_structures, clean_directory, loadbar,
40
+ molecule_check, time_to_string, write_xyz)
41
+
42
+
43
+ def ase_torsion_TSs(embedder,
44
+ coords,
45
+ atomnos,
46
+ indices,
47
+ threshold_kcal=5,
48
+ title='temp',
49
+ optimization=True,
50
+ logfile=None,
51
+ bernytraj=None,
52
+ plot=False):
53
+ '''
54
+ Automated dihedral scan. Runs two preliminary scans
55
+ (clockwise, anticlockwise) in 10 degrees increments,
56
+ then peaks above 'kcal_thresh' are re-scanned accurately
57
+ in 1 degree increments.
58
+
59
+ '''
60
+
61
+ assert len(indices) == 4
62
+ # cyclical = False
63
+
64
+ ts_structures, energies = [], []
65
+
66
+ graph = graphize(coords, atomnos)
67
+ i1, i2, i3, i4 = indices
68
+
69
+ if all([len(shortest_path(graph, start, end)) == 2 for start, end in zip(indices[0:-1], indices[1:])]):
70
+ graph.remove_edge(i2, i3)
71
+ subgraphs = connected_components(graph)
72
+
73
+ for subgraph in subgraphs:
74
+ if i3 in subgraph:
75
+ indices_to_be_moved = subgraph - {i3}
76
+ break
77
+
78
+ if i1 in indices_to_be_moved:
79
+
80
+ # cyclical = True
81
+ indices_to_be_moved = [i4]
82
+ # if molecule is cyclical, just move the fourth atom and
83
+ # let the rest of the structure relax
84
+
85
+ s = 'The specified dihedral angle is comprised within a cycle. Switching to safe dihedral scan (moving only last index).'
86
+ print(s)
87
+ if logfile is not None:
88
+ logfile.write(s+'\n')
89
+
90
+ else:
91
+
92
+ if not embedder.options.let:
93
+ raise SystemExit('The specified dihedral angle is made up of non-contiguous atoms. To prevent errors, the\n' +
94
+ 'run has been stopped. Override this behavior with the LET keyword.')
95
+
96
+ # if user did not provide four contiguous indices,
97
+ # and did that on purpose, just move the fourth atom and
98
+ # let the rest of the structure relax
99
+ indices_to_be_moved = [i4]
100
+ # cyclical = True
101
+
102
+ s = 'The specified dihedral angle is made up of non-contiguous atoms.\nThis might cause some unexpected results.'
103
+ print(s)
104
+ if logfile is not None:
105
+ logfile.write(s+'\n')
106
+
107
+
108
+ # routine = ((10, 18, '_clockwise'), (-10, 18, '_counterclockwise')) if cyclical else ((10, 36, ''),)
109
+ routine = ((10, 36, '_clockwise'), (-10, 36, '_counterclockwise'))
110
+
111
+
112
+ for degrees, steps, direction in routine:
113
+
114
+ print()
115
+ if logfile is not None:
116
+ logfile.write('\n')
117
+
118
+ structures, energies = ase_dih_scan(embedder,
119
+ coords,
120
+ atomnos,
121
+ indices=indices,
122
+ degrees=degrees,
123
+ steps=steps,
124
+ relaxed=optimization,
125
+ indices_to_be_moved=indices_to_be_moved,
126
+ title='Preliminary scan' + ((' (clockwise)' if direction == '_clockwise' \
127
+ else ' (counterclockwise)') if direction != '' else ''),
128
+ logfile=logfile)
129
+
130
+ min_e = min(energies)
131
+ rel_energies = [e-min_e for e in energies]
132
+
133
+ tag = '_relaxed' if optimization else '_rigid'
134
+
135
+ with open(title + tag + direction + '_scan.xyz', 'w') as outfile:
136
+ for s, structure in enumerate(align_structures(np.array(structures), indices[:-1])):
137
+ write_xyz(structure, atomnos, outfile, title=f'Scan point {s+1}/{len(structures)} - Rel. E = {round(rel_energies[s], 3)} kcal/mol')
138
+
139
+ if plot:
140
+ import matplotlib.pyplot as plt
141
+
142
+ plt.figure()
143
+
144
+ x1 = [dihedral(structure[indices]) for structure in structures]
145
+ y1 = [e-min_e for e in energies]
146
+
147
+ for i, (x_, y_) in enumerate(get_plot_segments(x1, y1, max_step=abs(degrees)+1)):
148
+
149
+ plt.plot(x_,
150
+ y_,
151
+ '-',
152
+ color='tab:blue',
153
+ label=('Preliminary SCAN'+direction) if i == 0 else None,
154
+ linewidth=3,
155
+ alpha=0.50)
156
+
157
+ peaks_indices = atropisomer_peaks(energies, min_thr=min_e+threshold_kcal, max_thr=min_e+75)
158
+
159
+ if peaks_indices:
160
+
161
+ s = 's' if len(peaks_indices) > 1 else ''
162
+ print(f'Found {len(peaks_indices)} peak{s}. Performing accurate scan{s}.\n')
163
+ if logfile is not None:
164
+ logfile.write(f'Found {len(peaks_indices)} peak{s}. Performing accurate scan{s}.\n\n')
165
+
166
+
167
+ for p, peak in enumerate(peaks_indices):
168
+
169
+ sub_structures, sub_energies = ase_dih_scan(embedder,
170
+ structures[peak-1],
171
+ atomnos,
172
+ indices=indices,
173
+ degrees=degrees/10, #1° or -1°
174
+ steps=20,
175
+ relaxed=optimization,
176
+ ad_libitum=True, # goes on until the hill is crossed
177
+ indices_to_be_moved=indices_to_be_moved,
178
+ title=f'Accurate scan {p+1}/{len(peaks_indices)}',
179
+ logfile=logfile)
180
+
181
+ if logfile is not None:
182
+ logfile.write('\n')
183
+
184
+ if plot:
185
+ x2 = [dihedral(structure[indices]) for structure in sub_structures]
186
+ y2 = [e-min_e for e in sub_energies]
187
+
188
+ for i, (x_, y_) in enumerate(get_plot_segments(x2, y2, max_step=abs(degrees/10)+1)):
189
+
190
+ plt.plot(x_,
191
+ y_,
192
+ '-o',
193
+ color='tab:red',
194
+ label='Accurate SCAN' if (p == 0 and i == 0) else None,
195
+ markersize=1,
196
+ linewidth=2,
197
+ alpha=0.5)
198
+
199
+ sub_peaks_indices = atropisomer_peaks(sub_energies, min_thr=threshold_kcal+min_e, max_thr=min_e+75)
200
+
201
+ if sub_peaks_indices:
202
+
203
+ s = 's' if len(sub_peaks_indices) > 1 else ''
204
+ msg = f'Found {len(sub_peaks_indices)} sub-peak{s}.'
205
+
206
+ if embedder.options.saddle or embedder.options.neb:
207
+ if embedder.options.saddle:
208
+ tag = 'saddle'
209
+ else:
210
+ tag = 'NEB TS'
211
+
212
+ msg += f'Performing {tag} optimization{s}.'
213
+
214
+ print(msg)
215
+
216
+ if logfile is not None:
217
+ logfile.write(s+'\n')
218
+
219
+ for s, sub_peak in enumerate(sub_peaks_indices):
220
+
221
+ if plot:
222
+ x = dihedral(sub_structures[sub_peak][indices])
223
+ y = sub_energies[sub_peak]-min_e
224
+ plt.plot(x, y, color='gold', marker='o', label='Maxima' if p == 0 else None, markersize=3)
225
+
226
+ if embedder.options.saddle:
227
+
228
+ loadbar_title = f' > Saddle opt on sub-peak {s+1}/{len(sub_peaks_indices)}'
229
+ # loadbar(s+1, len(sub_peaks_indices), loadbar_title+' '*(29-len(loadbar_title)))
230
+ print(loadbar_title)
231
+
232
+ optimized_geom, energy, _ = ase_saddle(embedder,
233
+ sub_structures[sub_peak],
234
+ atomnos,
235
+ title=f'Saddle opt - peak {p+1}, sub-peak {s+1}',
236
+ logfile=logfile,
237
+ traj=bernytraj+f'_{p+1}_{s+1}.traj' if bernytraj is not None else None)
238
+
239
+ if molecule_check(coords, optimized_geom, atomnos):
240
+ ts_structures.append(optimized_geom)
241
+ energies.append(energy)
242
+
243
+ elif embedder.options.neb:
244
+
245
+ loadbar_title = f' > NEB TS opt on sub-peak {s+1}/{len(sub_peaks_indices)}, {direction[1:]}'
246
+ drctn = 'clkws' if direction == '_clockwise' else 'ccws'
247
+
248
+ print(loadbar_title)
249
+
250
+ optimized_geom, energy, success = ase_neb(embedder,
251
+ sub_structures[sub_peak-2],
252
+ sub_structures[(sub_peak+1)%len(sub_structures)],
253
+ atomnos,
254
+ n_images=5,
255
+ title=f'{title}_NEB_peak_{p+1}_sub-peak_{s+1}_{drctn}',
256
+ logfunction=embedder.log)
257
+
258
+ if success and molecule_check(coords, optimized_geom, atomnos):
259
+ ts_structures.append(optimized_geom)
260
+ energies.append(energy)
261
+
262
+ else:
263
+ ts_structures.append(sub_structures[sub_peak])
264
+ energies.append(sub_energies[sub_peak])
265
+
266
+ print()
267
+
268
+ else:
269
+ print('No suitable sub-peaks found.\n')
270
+ if logfile is not None:
271
+ logfile.write('No suitable sub-peaks found.\n\n')
272
+ else:
273
+ print('No suitable peaks found.\n')
274
+ if logfile is not None:
275
+ logfile.write('No suitable peaks found.\n\n')
276
+
277
+ if plot:
278
+ plt.legend()
279
+ plt.xlabel(f'Dihedral Angle {tuple(indices)}')
280
+ plt.ylabel('Energy (kcal/mol)')
281
+ # with open(f'{title}{direction}_plt.pickle', 'wb') as _f:
282
+ # pickle.dump(fig, _f)
283
+ plt.savefig(f'{title}{direction}_plt.svg')
284
+
285
+ ts_structures = np.array(ts_structures)
286
+
287
+ clean_directory()
288
+
289
+ return ts_structures, energies
290
+
291
+ def atropisomer_peaks(data, min_thr, max_thr):
292
+ '''
293
+ data: iterable
294
+ min_thr: peaks must be values greater than min_thr
295
+ max_thr: peaks must be values smaller than max_thr
296
+ return: list of peak indices
297
+ '''
298
+ _l = len(data)
299
+ peaks = [i for i in range(_l-2) if (
300
+
301
+ data[i-1] < data[i] >= data[i+1] and
302
+ # peaks have neighbors that are smaller than them
303
+
304
+ max_thr > data[i] > min_thr and
305
+ # discard peaks that are too small or too big
306
+
307
+ # abs(data[i] - min((data[i-1], data[i+1]))) > 2
308
+ data[i] == max(data[i-2:i+3])
309
+ # discard peaks that are not the highest within close nieghbors
310
+ )]
311
+
312
+ return peaks
313
+
314
+ def ase_dih_scan(embedder,
315
+ coords,
316
+ atomnos,
317
+ indices,
318
+ degrees=10,
319
+ steps=36,
320
+ relaxed=True,
321
+ ad_libitum=False,
322
+ indices_to_be_moved=None,
323
+ title='temp scan',
324
+ logfile=None):
325
+ '''
326
+ Performs a dihedral scan via the ASE library
327
+ if ad libitum, steps is the minimum number of performed steps
328
+ '''
329
+ assert len(indices) == 4
330
+
331
+ if ad_libitum:
332
+ if not relaxed:
333
+ raise Exception('The ad_libitum keyword is only available for relaxed scans.')
334
+
335
+ atoms = Atoms(atomnos, positions=coords)
336
+ structures, energies = [], []
337
+
338
+ atoms.calc = get_ase_calc(embedder)
339
+
340
+ if indices_to_be_moved is None:
341
+ indices_to_be_moved = range(len(atomnos))
342
+
343
+ mask = np.array([i in indices_to_be_moved for i, _ in enumerate(atomnos)], dtype=bool)
344
+
345
+ t_start = time()
346
+
347
+ if logfile is not None:
348
+ logfile.write(f' > {title}\n')
349
+
350
+ for scan_step in range(1000):
351
+
352
+ loadbar_title = f'{title} - step {scan_step+1}'
353
+ if ad_libitum:
354
+ print(loadbar_title, end='\r')
355
+ else:
356
+ loadbar_title += '/'+str(steps)
357
+ loadbar(scan_step+1, steps, loadbar_title+' '*(29-len(loadbar_title)))
358
+
359
+ if logfile is not None:
360
+ t_start_step = time()
361
+
362
+ if relaxed:
363
+ atoms.set_constraint(FixInternals(dihedrals_deg=[[atoms.get_dihedral(*indices), indices]]))
364
+
365
+ with LBFGS(atoms, maxstep=0.2, logfile=None, trajectory=None) as opt:
366
+
367
+ try:
368
+ opt.run(fmax=0.05, steps=500)
369
+ exit_str = 'converged'
370
+
371
+ except ValueError: # Shake did not converge
372
+ exit_str = 'crashed'
373
+
374
+ iterations = opt.nsteps
375
+
376
+
377
+ energies.append(atoms.get_total_energy() * 23.06054194532933) # eV to kcal/mol
378
+
379
+ if logfile is not None:
380
+ elapsed = time() - t_start_step
381
+ s = '/' + str(steps) if not ad_libitum else ''
382
+ logfile.write(f' Step {scan_step+1}{s} - {exit_str} - {iterations} iterations ({time_to_string(elapsed)})\n')
383
+
384
+ structures.append(atoms.get_positions())
385
+
386
+ atoms.rotate_dihedral(*indices, angle=degrees, mask=mask)
387
+
388
+ if exit_str == 'crashed':
389
+ break
390
+
391
+ elif scan_step+1 >= steps:
392
+ if ad_libitum:
393
+ if any((
394
+ (max(energies) - energies[-1]) > 1,
395
+ (max(energies) - energies[-1]) > max(energies)-energies[0],
396
+ (energies[-1] - min(energies)) > 50
397
+ )):
398
+
399
+ # ad_libitum stops when one of these conditions is met:
400
+ # - we surpassed and are below the maximum of at least 1 kcal/mol
401
+ # - we surpassed maximum and are below starting point
402
+ # - current step energy is more than 50 kcal/mol above starting point
403
+
404
+ print(loadbar_title)
405
+ break
406
+ else:
407
+ break
408
+
409
+ structures = np.array(structures)
410
+
411
+ clean_directory()
412
+
413
+ if logfile is not None:
414
+ elapsed = time() - t_start
415
+ logfile.write(f'{title} - completed ({time_to_string(elapsed)})\n')
416
+
417
+ return align_structures(structures, indices), energies
418
+
419
+ def get_plot_segments(x, y, max_step=2):
420
+ '''
421
+ Returns a zip object with x, y segments.
422
+ A single segment has x values with separation
423
+ smaller than max_step.
424
+ '''
425
+ x, y = zip(*sorted(zip(x, y), key=lambda t: t[0]))
426
+
427
+ x_slices, y_slices = [], []
428
+ for i, n in enumerate(x):
429
+ if abs(x[i-1]-n) > max_step:
430
+ x_slices.append([])
431
+ y_slices.append([])
432
+
433
+ x_slices[-1].append(n)
434
+ y_slices[-1].append(y[i])
435
+
436
+ return zip(x_slices, y_slices)
437
+
438
+ def dihedral_scan(embedder):
439
+ '''
440
+ Automated dihedral scan. Runs two preliminary scans
441
+ (clockwise, anticlockwise) in 10 degrees increments,
442
+ then peaks above 'kcal_thresh' are re-scanned accurately
443
+ in 1 degree increments.
444
+
445
+ '''
446
+
447
+ if 'kcal' not in embedder.kw_line.lower():
448
+ # set to 5 if user did not specify a value
449
+ embedder.options.kcal_thresh = 5
450
+
451
+ mol = embedder.objects[0]
452
+ embedder.structures, embedder.energies = [], []
453
+
454
+
455
+ embedder.log(f'\n--> {mol.filename} - performing a scan of dihedral angle with indices {mol.reactive_indices}\n')
456
+
457
+ for c, coords in enumerate(mol.atomcoords):
458
+
459
+ embedder.log(f'\n--> Pre-optimizing input structure{"s" if len(mol.atomcoords) > 1 else ""} '
460
+ f'({embedder.options.theory_level} via {embedder.options.calculator})')
461
+
462
+ embedder.log(f'--> Performing relaxed scans (conformer {c+1}/{len(mol.atomcoords)})')
463
+
464
+ new_coords, ground_energy, success = optimize(
465
+ coords,
466
+ mol.atomnos,
467
+ embedder.options.calculator,
468
+ method=embedder.options.theory_level,
469
+ procs=embedder.procs,
470
+ solvent=embedder.options.solvent
471
+ )
472
+
473
+ if not success:
474
+ embedder.log(f'Pre-optimization failed - Skipped conformer {c+1}', p=False)
475
+ continue
476
+
477
+ structures, energies = ase_torsion_TSs(embedder,
478
+ new_coords,
479
+ mol.atomnos,
480
+ mol.reactive_indices,
481
+ threshold_kcal=embedder.options.kcal_thresh,
482
+ title=mol.rootname+f'_conf_{c+1}',
483
+ optimization=embedder.options.optimization,
484
+ logfile=embedder.logfile,
485
+ bernytraj=mol.rootname + '_berny' if embedder.options.debug else None,
486
+ plot=True)
487
+
488
+ for structure, energy in zip(structures, energies):
489
+ embedder.structures.append(structure)
490
+ embedder.energies.append(energy)
491
+
492
+ embedder.structures = np.array(embedder.structures)
493
+ embedder.energies = np.array(embedder.energies)
494
+ embedder.atomnos = mol.atomnos
495
+
496
+ if len(embedder.structures) == 0:
497
+ s = ('\n--> Dihedral scan did not find any suitable maxima above the set threshold\n'
498
+ f' ({embedder.options.kcal_thresh} kcal/mol) during the scan procedure. Observe the\n'
499
+ ' generated energy plot and try lowering the threshold value (KCAL keyword).')
500
+ embedder.log(s)
501
+ raise ZeroCandidatesError()
502
+
503
+ # remove similar structures (RMSD)
504
+ embedder.structures, mask = prune_by_rmsd(embedder.structures, mol.atomnos, max_rmsd=embedder.options.rmsd, debugfunction=embedder.debuglog)
505
+ embedder.energies = embedder.energies[mask]
506
+ if 0 in mask:
507
+ embedder.log(f'Discarded {int(len([b for b in mask if not b]))} candidates for RMSD similarity ({len([b for b in mask if b])} left)')
508
+
509
+ # sort structures based on energy
510
+ embedder.energies, embedder.structures = zip(*sorted(zip(embedder.energies, embedder.structures), key=lambda x: x[0]))
511
+ embedder.structures = np.array(embedder.structures)
512
+ embedder.energies = np.array(embedder.energies)
513
+
514
+ # write output and exit
515
+ embedder.write_structures('maxima', indices=mol.reactive_indices, relative=True, extra='(barrier height)', align='moi')
516
+ embedder.normal_termination()
firecode/automep.py ADDED
@@ -0,0 +1,130 @@
1
+ import numpy as np
2
+ from ase import Atoms
3
+ from networkx import cycle_basis
4
+
5
+ from firecode.algebra import dihedral, norm_of
6
+ from firecode.calculators._xtb import xtb_opt, xtb_pre_opt
7
+ from firecode.graph_manipulations import neighbors
8
+ from firecode.mep_relaxer import interpolate_structures
9
+ from firecode.utils import align_structures, write_xyz
10
+
11
+
12
+ def automep(embedder, n_images=9):
13
+
14
+ assert embedder.options.calculator == "XTB"
15
+ assert len(embedder.objects) == 2, "Provide two molecules as start/endpoints."
16
+
17
+ mol = embedder.objects[0]
18
+ coords = mol.atomcoords[0]
19
+
20
+ # Get cycle indices between 7 and 9
21
+ # graph = graphize(coords, mol.atomnos)
22
+ cycles = [_l for _l in cycle_basis(mol.graph) if len(_l) in (7, 8, 9)]
23
+ assert len(cycles) == 1, "Automep only works for 7/8/9-membered ring flips at the moment"
24
+
25
+ embedder.log(f'--> AutoMEP - Building MEP for {len(cycles[0])}-membered ring inversion')
26
+ embedder.log(f' Preoptimizing starting point at {embedder.options.calculator}/{embedder.options.theory_level}({embedder.options.solvent}) level')
27
+
28
+ print(" - Optimizing starting point...", end="\r")
29
+ coords, _, _ = xtb_opt(
30
+ coords,
31
+ mol.atomnos,
32
+ method=embedder.options.theory_level,
33
+ solvent=embedder.options.solvent,
34
+ procs=embedder.procs
35
+ )
36
+
37
+ dihedrals = cycle_to_dihedrals(cycles[0])
38
+ exocyclic = get_exocyclic_dihedrals(mol.graph, cycles[0])
39
+
40
+ # start_angles = np.array([dihedral(coords[d]) for d in dihedrals+exocyclic])
41
+ target_angles = np.array([0 for _ in dihedrals] + [180 for _ in exocyclic])
42
+ print(" - Optimizing planar TS guess...", end="\r")
43
+ ts_guess, _, _ = xtb_opt(
44
+ coords,
45
+ mol.atomnos,
46
+ constrained_dihedrals=dihedrals+exocyclic,
47
+ constrained_dih_angles=target_angles,
48
+ method=embedder.options.theory_level,
49
+ solvent=embedder.options.solvent,
50
+ procs=embedder.procs
51
+ )
52
+ # multipliers = np.linspace(1, -1, n_images)
53
+
54
+ # mep_angles = [(start_angles * m + target_angles * (1-m)) % 360 for m in multipliers]
55
+ # mep = []
56
+ # for i, m_a in enumerate(mep_angles):
57
+ # t_start = time.perf_counter()
58
+ # coords, _, _ = xtb_opt(coords,
59
+ # mol.atomnos,
60
+ # constrained_dihedrals=dihedrals+exocyclic,
61
+ # constrained_dih_angles=m_a,
62
+ # method=embedder.options.theory_level,
63
+ # solvent=embedder.options.solvent,
64
+ # procs=embedder.procs)
65
+ # embedder.log(f' - optimized image {i+1}/{len(mep_angles)} ({round(time.perf_counter()-t_start, 3)} s)')
66
+ # mep.append(coords)
67
+
68
+ mep = interpolate_structures(align_structures(np.array([coords,
69
+ ts_guess,
70
+ embedder.objects[1].atomcoords[0]])),
71
+ mol.atomnos,
72
+ n=n_images,
73
+ method='linear')
74
+
75
+ constrained_indices = [[a, b] for (a, b) in mol.graph.edges if a!=b]
76
+ constrained_distances = [norm_of(coords[a]-coords[b]) for (a, b) in constrained_indices]
77
+
78
+ for g, geom in enumerate(mep):
79
+ if g not in (0, n_images-1):
80
+ print(f" - Relaxing image {g+1}/{n_images}...", end="\r")
81
+ positions = geom.get_positions()
82
+ opt_geom, _, _ = xtb_pre_opt(
83
+ positions,
84
+ mol.atomnos,
85
+ graphs=[mol.graph],
86
+ constrained_indices=constrained_indices,
87
+ constrained_distances=constrained_distances,
88
+ constrained_dihedrals=dihedrals+exocyclic,
89
+ constrained_dih_angles=[dihedral(positions[quadruplet]) for quadruplet in dihedrals+exocyclic],
90
+ method=embedder.options.theory_level,
91
+ solvent=embedder.options.solvent,
92
+ procs=embedder.procs
93
+ )
94
+ mep[g] = Atoms(mol.atomnos, positions=opt_geom)
95
+
96
+ mep_array = np.array([c.get_positions() for c in mep], dtype=float)
97
+ mep_array = align_structures(mep_array)
98
+ with open(f"{mol.rootname}_automep.xyz", "w") as f:
99
+ for c in mep_array:
100
+ write_xyz(c, mol.atomnos, f)
101
+
102
+ embedder.log(f"\n--> Saved autogenerated MEP as {mol.rootname}_automep.xyz\n")
103
+
104
+ return f"{mol.rootname}_automep.xyz"
105
+
106
+ def get_exocyclic_dihedrals(graph, cycle):
107
+ '''
108
+ '''
109
+ exo_dihs = []
110
+ for index in cycle:
111
+ for exo_id in neighbors(graph, index):
112
+ if exo_id not in cycle:
113
+ dummy1 = next(i for i in cycle if i not in (exo_id, index) and i in neighbors(graph, index))
114
+ dummy2 = next(i for i in cycle if i not in (exo_id, index, dummy1) and i in neighbors(graph, dummy1))
115
+ exo_dihs.append([exo_id, index, dummy1, dummy2])
116
+
117
+ return exo_dihs
118
+
119
+ def cycle_to_dihedrals(cycle):
120
+ '''
121
+ '''
122
+ dihedrals = []
123
+ for i in range(len(cycle)):
124
+
125
+ a = cycle[i % len(cycle)]
126
+ b = cycle[(i+1) % len(cycle)]
127
+ c = cycle[(i+2) % len(cycle)]
128
+ d = cycle[(i+3) % len(cycle)]
129
+ dihedrals.append([a, b, c, d])
130
+ return dihedrals
@@ -0,0 +1,29 @@
1
+ import os
2
+ import shutil
3
+
4
+ class NewFolderContext:
5
+ '''
6
+ Context manager: creates a new directory and moves into it on entry.
7
+ On exit, moves out of the directory and deletes it if instructed to do so.
8
+
9
+ '''
10
+
11
+ def __init__(self, new_folder_name, delete_after=True):
12
+ self.new_folder_name = new_folder_name
13
+ self.delete_after = delete_after
14
+
15
+ def __enter__(self):
16
+ # create working folder and cd into it
17
+ if self.new_folder_name in os.listdir():
18
+ shutil.rmtree(os.path.join(os.getcwd(), self.new_folder_name))
19
+
20
+ os.mkdir(self.new_folder_name)
21
+ os.chdir(os.path.join(os.getcwd(), self.new_folder_name))
22
+
23
+ def __exit__(self, *args):
24
+ # get out of working folder
25
+ os.chdir(os.path.dirname(os.getcwd()))
26
+
27
+ # and eventually delete it
28
+ if self.delete_after:
29
+ shutil.rmtree(os.path.join(os.getcwd(), self.new_folder_name))