firecode 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- firecode/TEST_NOTEBOOK.ipynb +3940 -0
- firecode/__init__.py +0 -0
- firecode/__main__.py +118 -0
- firecode/_gaussian.py +97 -0
- firecode/algebra.py +405 -0
- firecode/ase_manipulations.py +879 -0
- firecode/atropisomer_module.py +516 -0
- firecode/automep.py +130 -0
- firecode/calculators/__init__.py +29 -0
- firecode/calculators/_gaussian.py +98 -0
- firecode/calculators/_mopac.py +242 -0
- firecode/calculators/_openbabel.py +154 -0
- firecode/calculators/_orca.py +129 -0
- firecode/calculators/_xtb.py +786 -0
- firecode/concurrent_test.py +119 -0
- firecode/embedder.py +2590 -0
- firecode/embedder_options.py +577 -0
- firecode/embeds.py +881 -0
- firecode/errors.py +65 -0
- firecode/graph_manipulations.py +333 -0
- firecode/hypermolecule_class.py +364 -0
- firecode/mep_relaxer.py +199 -0
- firecode/modify_settings.py +186 -0
- firecode/mprof.py +65 -0
- firecode/multiembed.py +148 -0
- firecode/nci.py +186 -0
- firecode/numba_functions.py +260 -0
- firecode/operators.py +776 -0
- firecode/optimization_methods.py +609 -0
- firecode/parameters.py +84 -0
- firecode/pka.py +275 -0
- firecode/profiler.py +17 -0
- firecode/pruning.py +421 -0
- firecode/pt.py +32 -0
- firecode/quotes.json +6651 -0
- firecode/quotes.py +9 -0
- firecode/reactive_atoms_classes.py +666 -0
- firecode/references.py +11 -0
- firecode/rmsd.py +74 -0
- firecode/settings.py +75 -0
- firecode/solvents.py +126 -0
- firecode/tests/C2F2H4.xyz +10 -0
- firecode/tests/C2H4.xyz +8 -0
- firecode/tests/CH3Cl.xyz +7 -0
- firecode/tests/HCOOH.xyz +7 -0
- firecode/tests/HCOOOH.xyz +8 -0
- firecode/tests/chelotropic.txt +3 -0
- firecode/tests/cyclical.txt +3 -0
- firecode/tests/dihedral.txt +2 -0
- firecode/tests/string.txt +3 -0
- firecode/tests/trimolecular.txt +9 -0
- firecode/tests.py +151 -0
- firecode/torsion_module.py +1035 -0
- firecode/utils.py +541 -0
- firecode-1.0.0.dist-info/LICENSE +165 -0
- firecode-1.0.0.dist-info/METADATA +321 -0
- firecode-1.0.0.dist-info/RECORD +59 -0
- firecode-1.0.0.dist-info/WHEEL +5 -0
- firecode-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -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))
|