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,786 @@
|
|
|
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 os
|
|
25
|
+
import shutil
|
|
26
|
+
import sys
|
|
27
|
+
from subprocess import DEVNULL, STDOUT, CalledProcessError, check_call
|
|
28
|
+
|
|
29
|
+
import numpy as np
|
|
30
|
+
|
|
31
|
+
from firecode.algebra import norm, norm_of
|
|
32
|
+
from firecode.graph_manipulations import get_sum_graph
|
|
33
|
+
from firecode.utils import clean_directory, read_xyz, write_xyz
|
|
34
|
+
from firecode.calculators.__init__ import NewFolderContext
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def xtb_opt(
|
|
38
|
+
coords,
|
|
39
|
+
atomnos,
|
|
40
|
+
constrained_indices=None,
|
|
41
|
+
constrained_distances=None,
|
|
42
|
+
constrained_dihedrals=None,
|
|
43
|
+
constrained_dih_angles=None,
|
|
44
|
+
method='GFN2-xTB',
|
|
45
|
+
maxiter=500,
|
|
46
|
+
solvent=None,
|
|
47
|
+
charge=0,
|
|
48
|
+
title='temp',
|
|
49
|
+
read_output=True,
|
|
50
|
+
procs=4,
|
|
51
|
+
opt=True,
|
|
52
|
+
conv_thr="tight",
|
|
53
|
+
assert_convergence=False,
|
|
54
|
+
constrain_string=None,
|
|
55
|
+
recursive_stepsize=0.3,
|
|
56
|
+
spring_constant=1,
|
|
57
|
+
|
|
58
|
+
debug=False,
|
|
59
|
+
**kwargs,
|
|
60
|
+
):
|
|
61
|
+
'''
|
|
62
|
+
This function writes an XTB .inp file, runs it with the subprocess
|
|
63
|
+
module and reads its output.
|
|
64
|
+
|
|
65
|
+
coords: array of shape (n,3) with cartesian coordinates for atoms.
|
|
66
|
+
|
|
67
|
+
atomnos: array of atomic numbers for atoms.
|
|
68
|
+
|
|
69
|
+
constrained_indices: array of shape (n,2), with the indices
|
|
70
|
+
of atomic pairs to be constrained.
|
|
71
|
+
|
|
72
|
+
constrained_distances: optional, target distances for the specified
|
|
73
|
+
distance constraints.
|
|
74
|
+
|
|
75
|
+
constrained_dihedrals: quadruplets of atomic indices to constrain.
|
|
76
|
+
|
|
77
|
+
constrained_dih_angles: target dihedral angles for the dihedral constraints.
|
|
78
|
+
|
|
79
|
+
method: string, specifying the theory level to be used.
|
|
80
|
+
|
|
81
|
+
maxiter: maximum number of geometry optimization steps (maxcycle).
|
|
82
|
+
|
|
83
|
+
solvent: solvent to be used in the calculation (ALPB model).
|
|
84
|
+
|
|
85
|
+
charge: charge to be used in the calculation.
|
|
86
|
+
|
|
87
|
+
title: string, used as a file name and job title for the mopac input file.
|
|
88
|
+
|
|
89
|
+
read_output: Whether to read the output file and return anything.
|
|
90
|
+
|
|
91
|
+
procs: number of cores to be used for the calculation.
|
|
92
|
+
|
|
93
|
+
opt: if false, a single point energy calculation is carried.
|
|
94
|
+
|
|
95
|
+
conv_thr: tightness of convergence thresholds. See XTB ReadTheDocs.
|
|
96
|
+
|
|
97
|
+
assert_convergence: wheter to raise an error in case convergence is not
|
|
98
|
+
achieved by xtb.
|
|
99
|
+
|
|
100
|
+
constrain_string: string to be added to the end of the $geom section of
|
|
101
|
+
the input file.
|
|
102
|
+
|
|
103
|
+
recursive_stepsize: magnitude of step in recursive constrained optimizations.
|
|
104
|
+
The smaller, the slower - but potentially safer against scrambling.
|
|
105
|
+
|
|
106
|
+
spring_constant: stiffness of harmonic distance constraint (Hartrees/Bohrs^2)
|
|
107
|
+
|
|
108
|
+
'''
|
|
109
|
+
# create working folder and cd into it
|
|
110
|
+
with NewFolderContext(title, delete_after=(not debug)):
|
|
111
|
+
|
|
112
|
+
if constrained_indices is not None:
|
|
113
|
+
if len(constrained_indices) == 0:
|
|
114
|
+
constrained_indices = None
|
|
115
|
+
|
|
116
|
+
if constrained_distances is not None:
|
|
117
|
+
if len(constrained_distances) == 0:
|
|
118
|
+
constrained_distances = None
|
|
119
|
+
|
|
120
|
+
# recursive
|
|
121
|
+
if constrained_distances is not None:
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
|
|
125
|
+
for i, (target_d, ci) in enumerate(zip(constrained_distances, constrained_indices)):
|
|
126
|
+
|
|
127
|
+
if target_d is None:
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
if len(ci) == 2:
|
|
131
|
+
a, b = ci
|
|
132
|
+
else:
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
d = norm_of(coords[b] - coords[a])
|
|
136
|
+
delta = d - target_d
|
|
137
|
+
|
|
138
|
+
if abs(delta) > recursive_stepsize:
|
|
139
|
+
recursive_c_d = constrained_distances.copy()
|
|
140
|
+
recursive_c_d[i] = target_d + (recursive_stepsize * np.sign(d-target_d))
|
|
141
|
+
# print(f"--------> d is {round(d, 3)}, target d is {round(target_d, 3)}, delta is {round(delta, 3)}, setting new pretarget at {recursive_c_d}")
|
|
142
|
+
coords, _, _ = xtb_opt(
|
|
143
|
+
coords,
|
|
144
|
+
atomnos,
|
|
145
|
+
constrained_indices,
|
|
146
|
+
constrained_distances=recursive_c_d,
|
|
147
|
+
method=method,
|
|
148
|
+
solvent=solvent,
|
|
149
|
+
charge=charge,
|
|
150
|
+
maxiter=50,
|
|
151
|
+
title=title,
|
|
152
|
+
procs=procs,
|
|
153
|
+
conv_thr='loose',
|
|
154
|
+
constrain_string=constrain_string,
|
|
155
|
+
recursive_stepsize=0.3,
|
|
156
|
+
spring_constant=0.25,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
d = norm_of(coords[b] - coords[a])
|
|
160
|
+
delta = d - target_d
|
|
161
|
+
coords[b] -= norm(coords[b] - coords[a]) * delta
|
|
162
|
+
# print(f"--------> moved atoms from {round(d, 3)} A to {round(norm_of(coords[b] - coords[a]), 3)} A")
|
|
163
|
+
|
|
164
|
+
except RecursionError:
|
|
165
|
+
with open(f'{title}_crashed.xyz', 'w') as f:
|
|
166
|
+
write_xyz(coords, atomnos, f, title=title)
|
|
167
|
+
print("Recursion limit reached in constrained optimization - Crashed.")
|
|
168
|
+
sys.exit()
|
|
169
|
+
|
|
170
|
+
with open(f'{title}.xyz', 'w') as f:
|
|
171
|
+
write_xyz(coords, atomnos, f, title=title)
|
|
172
|
+
|
|
173
|
+
# outname = f'{title}_xtbopt.xyz' DOES NOT WORK - XTB ISSUE?
|
|
174
|
+
outname = 'xtbopt.xyz'
|
|
175
|
+
trajname = f'{title}_opt_log.xyz'
|
|
176
|
+
maxiter = maxiter if maxiter is not None else 0
|
|
177
|
+
s = f'$opt\n logfile={trajname}\n output={outname}\n maxcycle={maxiter}\n'
|
|
178
|
+
|
|
179
|
+
if constrained_indices is not None:
|
|
180
|
+
s += f'\n$constrain\n force constant={spring_constant}\n'
|
|
181
|
+
|
|
182
|
+
for (a, b), distance in zip(constrained_indices, constrained_distances):
|
|
183
|
+
|
|
184
|
+
distance = distance or 'auto'
|
|
185
|
+
s += f" distance: {a+1}, {b+1}, {distance}\n"
|
|
186
|
+
|
|
187
|
+
if constrained_dihedrals is not None:
|
|
188
|
+
|
|
189
|
+
assert len(constrained_dihedrals) == len(constrained_dih_angles)
|
|
190
|
+
|
|
191
|
+
if constrained_indices is None:
|
|
192
|
+
s += '\n$constrain\n'
|
|
193
|
+
|
|
194
|
+
for (a, b, c, d), angle in zip(constrained_dihedrals, constrained_dih_angles):
|
|
195
|
+
s += f" dihedral: {a+1}, {b+1}, {c+1}, {d+1}, {angle}\n"
|
|
196
|
+
|
|
197
|
+
if constrain_string is not None:
|
|
198
|
+
s += '\n$constrain\n'
|
|
199
|
+
s += constrain_string
|
|
200
|
+
|
|
201
|
+
if method.upper() in ('GFN-XTB', 'GFNXTB'):
|
|
202
|
+
s += '\n$gfn\n method=1\n'
|
|
203
|
+
|
|
204
|
+
elif method.upper() in ('GFN2-XTB', 'GFN2XTB'):
|
|
205
|
+
s += '\n$gfn\n method=2\n'
|
|
206
|
+
|
|
207
|
+
s += '\n$end'
|
|
208
|
+
|
|
209
|
+
s = ''.join(s)
|
|
210
|
+
with open(f'{title}.inp', 'w') as f:
|
|
211
|
+
f.write(s)
|
|
212
|
+
|
|
213
|
+
flags = '--norestart'
|
|
214
|
+
|
|
215
|
+
if opt:
|
|
216
|
+
flags += f' --opt {conv_thr}'
|
|
217
|
+
# specify convergence tightness
|
|
218
|
+
|
|
219
|
+
if method in ('GFN-FF', 'GFNFF'):
|
|
220
|
+
|
|
221
|
+
flags += ' --gfnff'
|
|
222
|
+
# declaring the use of FF instead of semiempirical
|
|
223
|
+
|
|
224
|
+
if charge != 0:
|
|
225
|
+
flags += f' --chrg {charge}'
|
|
226
|
+
|
|
227
|
+
if procs is not None:
|
|
228
|
+
flags += f' -P {procs}'
|
|
229
|
+
|
|
230
|
+
if solvent is not None:
|
|
231
|
+
|
|
232
|
+
if solvent == 'methanol':
|
|
233
|
+
flags += ' --gbsa methanol'
|
|
234
|
+
|
|
235
|
+
else:
|
|
236
|
+
flags += f' --alpb {solvent}'
|
|
237
|
+
|
|
238
|
+
elif method.upper() in ('GFN-FF', 'GFNFF'):
|
|
239
|
+
flags += ' --alpb ch2cl2'
|
|
240
|
+
# if using the GFN-FF force field, add CH2Cl2 solvation for increased accuracy
|
|
241
|
+
|
|
242
|
+
try:
|
|
243
|
+
with open(f"{title}.out", "w") as f:
|
|
244
|
+
check_call(f'xtb {title}.xyz --input {title}.inp {flags}'.split(), stdout=f, stderr=STDOUT)
|
|
245
|
+
|
|
246
|
+
# sometimes the SCC does not converge: only raise the error if specified
|
|
247
|
+
except CalledProcessError:
|
|
248
|
+
if assert_convergence:
|
|
249
|
+
raise CalledProcessError
|
|
250
|
+
|
|
251
|
+
except KeyboardInterrupt:
|
|
252
|
+
print('KeyboardInterrupt requested by user. Quitting.')
|
|
253
|
+
sys.exit()
|
|
254
|
+
|
|
255
|
+
if spring_constant > 0.25:
|
|
256
|
+
print()
|
|
257
|
+
|
|
258
|
+
if read_output:
|
|
259
|
+
|
|
260
|
+
if opt:
|
|
261
|
+
|
|
262
|
+
if trajname in os.listdir():
|
|
263
|
+
coords, energy = read_from_xtbtraj(trajname)
|
|
264
|
+
|
|
265
|
+
else:
|
|
266
|
+
energy = None
|
|
267
|
+
|
|
268
|
+
clean_directory((f'{title}.inp', f'{title}.xyz', f"{title}.out", trajname, outname))
|
|
269
|
+
|
|
270
|
+
else:
|
|
271
|
+
energy = energy_grepper(f"{title}.out", 'TOTAL ENERGY', 3)
|
|
272
|
+
# clean_directory((f'{title}.inp', f'{title}.xyz', f"{title}.out", trajname, outname))
|
|
273
|
+
|
|
274
|
+
for filename in ('gfnff_topo',
|
|
275
|
+
'charges',
|
|
276
|
+
'wbo',
|
|
277
|
+
'xtbrestart',
|
|
278
|
+
'xtbtopo.mol',
|
|
279
|
+
'.xtboptok',
|
|
280
|
+
'gfnff_adjacency',
|
|
281
|
+
'gfnff_charges',
|
|
282
|
+
):
|
|
283
|
+
try:
|
|
284
|
+
os.remove(filename)
|
|
285
|
+
except FileNotFoundError:
|
|
286
|
+
pass
|
|
287
|
+
|
|
288
|
+
return coords, energy, True
|
|
289
|
+
|
|
290
|
+
def xtb_pre_opt(
|
|
291
|
+
coords,
|
|
292
|
+
atomnos,
|
|
293
|
+
graphs,
|
|
294
|
+
constrained_indices=None,
|
|
295
|
+
constrained_distances=None,
|
|
296
|
+
**kwargs,
|
|
297
|
+
):
|
|
298
|
+
'''
|
|
299
|
+
Wrapper for xtb_opt that preserves the distance of every bond present in each subgraph provided
|
|
300
|
+
|
|
301
|
+
graphs: list of subgraphs that make up coords, in order
|
|
302
|
+
|
|
303
|
+
'''
|
|
304
|
+
sum_graph = get_sum_graph(graphs, extra_edges=constrained_indices)
|
|
305
|
+
|
|
306
|
+
# we have to check through a list this way, as I have not found
|
|
307
|
+
# an analogous way to check through an array for subarrays in a nice way
|
|
308
|
+
list_of_constr_ids = [[a,b] for a, b in constrained_indices] if constrained_indices is not None else []
|
|
309
|
+
|
|
310
|
+
constrain_string = "$constrain\n"
|
|
311
|
+
for constraint in [[a, b] for (a, b) in sum_graph.edges if a!=b]:
|
|
312
|
+
|
|
313
|
+
if constrained_distances is None:
|
|
314
|
+
distance = 'auto'
|
|
315
|
+
|
|
316
|
+
elif constraint in list_of_constr_ids:
|
|
317
|
+
distance = constrained_distances[list_of_constr_ids.index(constraint)]
|
|
318
|
+
|
|
319
|
+
else:
|
|
320
|
+
distance = 'auto'
|
|
321
|
+
|
|
322
|
+
indices_string = str([i+1 for i in constraint]).strip("[").strip("]")
|
|
323
|
+
constrain_string += f" distance: {indices_string}, {distance}\n"
|
|
324
|
+
constrain_string += "\n$end"
|
|
325
|
+
|
|
326
|
+
return xtb_opt(
|
|
327
|
+
coords,
|
|
328
|
+
atomnos,
|
|
329
|
+
constrained_indices=constrained_indices,
|
|
330
|
+
constrained_distances=constrained_distances,
|
|
331
|
+
constrain_string=constrain_string,
|
|
332
|
+
**kwargs,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
def read_from_xtbtraj(filename):
|
|
336
|
+
'''
|
|
337
|
+
Read coordinates from a .xyz trajfile.
|
|
338
|
+
|
|
339
|
+
'''
|
|
340
|
+
with open(filename, 'r') as f:
|
|
341
|
+
lines = f.readlines()
|
|
342
|
+
|
|
343
|
+
# look for the last line containing the flag (iterate in reverse order)
|
|
344
|
+
# and extract the line at which coordinates start
|
|
345
|
+
first_coord_line = len(lines) - next(line_num for line_num, line in enumerate(reversed(lines)) if 'energy:' in line)
|
|
346
|
+
xyzblock = lines[first_coord_line:]
|
|
347
|
+
|
|
348
|
+
coords = np.array([line.split()[1:] for line in xyzblock], dtype=float)
|
|
349
|
+
energy = float(lines[first_coord_line-1].split()[1]) * 627.5096080305927 # Eh to kcal/mol
|
|
350
|
+
|
|
351
|
+
return coords, energy
|
|
352
|
+
|
|
353
|
+
def energy_grepper(filename, signal_string, position):
|
|
354
|
+
'''
|
|
355
|
+
returns a kcal/mol energy from a Eh energy in a textfile.
|
|
356
|
+
'''
|
|
357
|
+
with open(filename, 'r', encoding='utf-8') as f:
|
|
358
|
+
line = f.readline()
|
|
359
|
+
while True:
|
|
360
|
+
line = f.readline()
|
|
361
|
+
if signal_string in line:
|
|
362
|
+
return float(line.split()[position]) * 627.5096080305927 # Eh to kcal/mol
|
|
363
|
+
if not line:
|
|
364
|
+
raise Exception()
|
|
365
|
+
|
|
366
|
+
def xtb_get_free_energy(coords, atomnos, method='GFN2-xTB', solvent=None,
|
|
367
|
+
charge=0, title='temp', sph=False, **kwargs):
|
|
368
|
+
'''
|
|
369
|
+
Calculates free energy with XTB,
|
|
370
|
+
without optimizing the provided structure.
|
|
371
|
+
'''
|
|
372
|
+
|
|
373
|
+
with open(f'{title}.xyz', 'w') as f:
|
|
374
|
+
write_xyz(coords, atomnos, f, title=title)
|
|
375
|
+
|
|
376
|
+
outname = 'xtbopt.xyz'
|
|
377
|
+
trajname = f'{title}_opt_log.xyz'
|
|
378
|
+
s = f'$opt\n logfile={trajname}\n output={outname}\n maxcycle=1\n'
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
if method.upper() in ('GFN-XTB', 'GFNXTB'):
|
|
382
|
+
s += '\n$gfn\n method=1\n'
|
|
383
|
+
|
|
384
|
+
elif method.upper() in ('GFN2-XTB', 'GFN2XTB'):
|
|
385
|
+
s += '\n$gfn\n method=2\n'
|
|
386
|
+
|
|
387
|
+
s += '\n$end'
|
|
388
|
+
|
|
389
|
+
s = ''.join(s)
|
|
390
|
+
with open(f'{title}.inp', 'w') as f:
|
|
391
|
+
f.write(s)
|
|
392
|
+
|
|
393
|
+
if sph:
|
|
394
|
+
flags = '--bhess'
|
|
395
|
+
else:
|
|
396
|
+
flags = '--ohess'
|
|
397
|
+
|
|
398
|
+
if method in ('GFN-FF', 'GFNFF'):
|
|
399
|
+
flags += ' --gfnff'
|
|
400
|
+
# declaring the use of FF instead of semiempirical
|
|
401
|
+
|
|
402
|
+
if charge != 0:
|
|
403
|
+
flags += f' --chrg {charge}'
|
|
404
|
+
|
|
405
|
+
if solvent is not None:
|
|
406
|
+
|
|
407
|
+
if solvent == 'methanol':
|
|
408
|
+
flags += ' --gbsa methanol'
|
|
409
|
+
|
|
410
|
+
else:
|
|
411
|
+
flags += f' --alpb {solvent}'
|
|
412
|
+
|
|
413
|
+
try:
|
|
414
|
+
with open('temp_hess.log', 'w') as outfile:
|
|
415
|
+
check_call(f'xtb --input {title}.inp {title}.xyz {flags}'.split(), stdout=outfile, stderr=STDOUT)
|
|
416
|
+
|
|
417
|
+
except KeyboardInterrupt:
|
|
418
|
+
print('KeyboardInterrupt requested by user. Quitting.')
|
|
419
|
+
sys.exit()
|
|
420
|
+
|
|
421
|
+
try:
|
|
422
|
+
free_energy = read_xtb_free_energy('temp_hess.log')
|
|
423
|
+
|
|
424
|
+
clean_directory()
|
|
425
|
+
for filename in ('gfnff_topo', 'charges', 'wbo', 'xtbrestart', 'xtbtopo.mol', '.xtboptok',
|
|
426
|
+
'hessian', 'g98.out', 'vibspectrum', 'wbo', 'xtbhess.xyz', 'charges', 'temp_hess.log'):
|
|
427
|
+
try:
|
|
428
|
+
os.remove(filename)
|
|
429
|
+
except FileNotFoundError:
|
|
430
|
+
pass
|
|
431
|
+
|
|
432
|
+
return free_energy
|
|
433
|
+
|
|
434
|
+
except FileNotFoundError:
|
|
435
|
+
# return 1E10
|
|
436
|
+
# print(f'temp_hess.log not present here - we are in', os.getcwd())
|
|
437
|
+
print(os.listdir())
|
|
438
|
+
sys.exit()
|
|
439
|
+
|
|
440
|
+
def read_xtb_free_energy(filename):
|
|
441
|
+
'''
|
|
442
|
+
returns free energy in kcal/mol from an XTB
|
|
443
|
+
.xyz result file (xtbopt.xyz)
|
|
444
|
+
'''
|
|
445
|
+
with open(filename, 'r') as f:
|
|
446
|
+
line = f.readline()
|
|
447
|
+
while True:
|
|
448
|
+
line = f.readline()
|
|
449
|
+
if 'TOTAL FREE ENERGY' in line:
|
|
450
|
+
return float(line.split()[4]) * 627.5096080305927 # Eh to kcal/mol
|
|
451
|
+
if not line:
|
|
452
|
+
raise Exception()
|
|
453
|
+
|
|
454
|
+
def xtb_metadyn_augmentation(coords, atomnos, constrained_indices=None, new_structures:int=5, title=0, debug=False):
|
|
455
|
+
'''
|
|
456
|
+
Runs a metadynamics simulation (MTD) through
|
|
457
|
+
the XTB program to obtain new conformations.
|
|
458
|
+
The GFN-FF force field is used.
|
|
459
|
+
'''
|
|
460
|
+
with open('temp.xyz', 'w') as f:
|
|
461
|
+
write_xyz(coords, atomnos, f, title='temp')
|
|
462
|
+
|
|
463
|
+
s = (
|
|
464
|
+
'$md\n'
|
|
465
|
+
' time=%s\n' % (new_structures) +
|
|
466
|
+
' step=1\n'
|
|
467
|
+
' temp=300\n'
|
|
468
|
+
'$end\n'
|
|
469
|
+
'$metadyn\n'
|
|
470
|
+
' save=%s\n' % (new_structures) +
|
|
471
|
+
'$end'
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
if constrained_indices is not None:
|
|
475
|
+
s += '\n$constrain\n'
|
|
476
|
+
for a, b in constrained_indices:
|
|
477
|
+
s += ' distance: %s, %s, %s\n' % (a+1, b+1, round(norm_of(coords[a]-coords[b]), 5))
|
|
478
|
+
|
|
479
|
+
s = ''.join(s)
|
|
480
|
+
with open('temp.inp', 'w') as f:
|
|
481
|
+
f.write(s)
|
|
482
|
+
|
|
483
|
+
try:
|
|
484
|
+
check_call(f'xtb --md --input temp.inp temp.xyz --gfnff > Structure{title}_MTD.log 2>&1'.split(), stdout=DEVNULL, stderr=STDOUT)
|
|
485
|
+
|
|
486
|
+
except KeyboardInterrupt:
|
|
487
|
+
print('KeyboardInterrupt requested by user. Quitting.')
|
|
488
|
+
sys.exit()
|
|
489
|
+
|
|
490
|
+
structures = [coords]
|
|
491
|
+
for n in range(1,new_structures):
|
|
492
|
+
name = 'scoord.'+str(n)
|
|
493
|
+
structures.append(parse_xtb_out(name))
|
|
494
|
+
os.remove(name)
|
|
495
|
+
|
|
496
|
+
for filename in ('gfnff_topo', 'xtbmdoc', 'mdrestart'):
|
|
497
|
+
try:
|
|
498
|
+
os.remove(filename)
|
|
499
|
+
except FileNotFoundError:
|
|
500
|
+
pass
|
|
501
|
+
|
|
502
|
+
# if debug:
|
|
503
|
+
os.rename('xtb.trj', f'Structure{title}_MTD_traj.xyz')
|
|
504
|
+
|
|
505
|
+
# else:
|
|
506
|
+
# os.remove('xtb.traj')
|
|
507
|
+
|
|
508
|
+
structures = np.array(structures)
|
|
509
|
+
|
|
510
|
+
return structures
|
|
511
|
+
|
|
512
|
+
def parse_xtb_out(filename):
|
|
513
|
+
'''
|
|
514
|
+
'''
|
|
515
|
+
with open(filename, 'r') as f:
|
|
516
|
+
lines = f.readlines()
|
|
517
|
+
|
|
518
|
+
coords = np.zeros((len(lines)-3,3))
|
|
519
|
+
|
|
520
|
+
for _l, line in enumerate(lines[1:-2]):
|
|
521
|
+
coords[_l] = line.split()[:-1]
|
|
522
|
+
|
|
523
|
+
return coords * 0.529177249 # Bohrs to Angstroms
|
|
524
|
+
|
|
525
|
+
def crest_mtd_search(
|
|
526
|
+
coords,
|
|
527
|
+
atomnos,
|
|
528
|
+
constrained_indices=None,
|
|
529
|
+
constrained_distances=None,
|
|
530
|
+
constrained_dihedrals=None,
|
|
531
|
+
constrained_dih_angles=None,
|
|
532
|
+
method='GFN2-XTB//GFN-FF',
|
|
533
|
+
solvent='CH2Cl2',
|
|
534
|
+
charge=0,
|
|
535
|
+
kcal=None,
|
|
536
|
+
ncimode=False,
|
|
537
|
+
title='temp',
|
|
538
|
+
procs=4,
|
|
539
|
+
threads=1,
|
|
540
|
+
):
|
|
541
|
+
'''
|
|
542
|
+
This function runs a crest metadynamic conformational search and
|
|
543
|
+
returns its output.
|
|
544
|
+
|
|
545
|
+
coords: array of shape (n,3) with cartesian coordinates for atoms.
|
|
546
|
+
|
|
547
|
+
atomnos: array of atomic numbers for atoms.
|
|
548
|
+
|
|
549
|
+
constrained_indices: array of shape (n,2), with the indices
|
|
550
|
+
of atomic pairs to be constrained.
|
|
551
|
+
|
|
552
|
+
constrained_distances: optional, target distances for the specified
|
|
553
|
+
distance constraints.
|
|
554
|
+
|
|
555
|
+
constrained_dihedrals: quadruplets of atomic indices to constrain.
|
|
556
|
+
|
|
557
|
+
constrained_dih_angles: target dihedral angles for the dihedral constraints.
|
|
558
|
+
|
|
559
|
+
method: string, specifying the theory level to be used.
|
|
560
|
+
|
|
561
|
+
solvent: solvent to be used in the calculation (ALPB model).
|
|
562
|
+
|
|
563
|
+
charge: charge to be used in the calculation.
|
|
564
|
+
|
|
565
|
+
title: string, used as a file name and job title for the mopac input file.
|
|
566
|
+
|
|
567
|
+
procs: number of cores to be used for the calculation.
|
|
568
|
+
|
|
569
|
+
threads: number of parallel threads to be used by the process.
|
|
570
|
+
|
|
571
|
+
'''
|
|
572
|
+
|
|
573
|
+
# Remove title directory, if already present
|
|
574
|
+
if title in os.listdir():
|
|
575
|
+
shutil.rmtree(os.path.join(os.getcwd(), title))
|
|
576
|
+
|
|
577
|
+
# make title directory and cd into it
|
|
578
|
+
os.mkdir(title)
|
|
579
|
+
os.chdir(os.path.join(os.getcwd(), title))
|
|
580
|
+
|
|
581
|
+
if constrained_indices is not None:
|
|
582
|
+
if len(constrained_indices) == 0:
|
|
583
|
+
constrained_indices = None
|
|
584
|
+
|
|
585
|
+
if constrained_distances is not None:
|
|
586
|
+
if len(constrained_distances) == 0:
|
|
587
|
+
constrained_distances = None
|
|
588
|
+
|
|
589
|
+
with open(f'{title}.xyz', 'w') as f:
|
|
590
|
+
write_xyz(coords, atomnos, f, title=title)
|
|
591
|
+
|
|
592
|
+
s = '$opt\n '
|
|
593
|
+
|
|
594
|
+
if constrained_indices is not None:
|
|
595
|
+
s += '\n$constrain\n'
|
|
596
|
+
# s += ' atoms: '
|
|
597
|
+
# for i in np.unique(np.array(constrained_indices).flatten()):
|
|
598
|
+
# s += f"{i+1},"
|
|
599
|
+
|
|
600
|
+
for (c1, c2), cd in zip(constrained_indices, constrained_distances):
|
|
601
|
+
cd = "auto" if cd is None else cd
|
|
602
|
+
s += f" distance: {c1+1}, {c2+1}, {cd}\n"
|
|
603
|
+
|
|
604
|
+
if constrained_dihedrals is not None:
|
|
605
|
+
assert len(constrained_dihedrals) == len(constrained_dih_angles)
|
|
606
|
+
s += '\n$constrain\n' if constrained_indices is None else ''
|
|
607
|
+
for (a, b, c, d), angle in zip(constrained_dihedrals, constrained_dih_angles):
|
|
608
|
+
s += f" dihedral: {a+1}, {b+1}, {c+1}, {d+1}, {angle}\n"
|
|
609
|
+
|
|
610
|
+
s += "\n$metadyn\n atoms: "
|
|
611
|
+
|
|
612
|
+
constrained_atoms_cumulative = set()
|
|
613
|
+
if constrained_indices is not None:
|
|
614
|
+
for c1, c2 in constrained_indices:
|
|
615
|
+
constrained_atoms_cumulative.add(c1)
|
|
616
|
+
constrained_atoms_cumulative.add(c2)
|
|
617
|
+
|
|
618
|
+
if constrained_dihedrals is not None:
|
|
619
|
+
for c1, c2, c3, c4 in constrained_dihedrals:
|
|
620
|
+
constrained_atoms_cumulative.add(c1)
|
|
621
|
+
constrained_atoms_cumulative.add(c2)
|
|
622
|
+
constrained_atoms_cumulative.add(c3)
|
|
623
|
+
constrained_atoms_cumulative.add(c4)
|
|
624
|
+
|
|
625
|
+
# write atoms that need to be moved during metadynamics (all but constrained)
|
|
626
|
+
active_ids = np.array([i+1 for i, _ in enumerate(atomnos) if i not in constrained_atoms_cumulative])
|
|
627
|
+
|
|
628
|
+
while len(active_ids) > 2:
|
|
629
|
+
i = next((i for i, _ in enumerate(active_ids[:-2]) if active_ids[i+1]-active_ids[i]>1), len(active_ids)-1)
|
|
630
|
+
if active_ids[0] == active_ids[i]:
|
|
631
|
+
s += f"{active_ids[0]},"
|
|
632
|
+
else:
|
|
633
|
+
s += f"{active_ids[0]}-{active_ids[i]},"
|
|
634
|
+
active_ids = active_ids[i+1:]
|
|
635
|
+
|
|
636
|
+
# remove final comma
|
|
637
|
+
s = s[:-1]
|
|
638
|
+
s += '\n$end'
|
|
639
|
+
|
|
640
|
+
s = ''.join(s)
|
|
641
|
+
with open(f'{title}.inp', 'w') as f:
|
|
642
|
+
f.write(s)
|
|
643
|
+
|
|
644
|
+
# avoid restarting the run
|
|
645
|
+
flags = '--norestart'
|
|
646
|
+
|
|
647
|
+
# add method flag
|
|
648
|
+
if method.upper() in ('GFN-FF', 'GFNFF'):
|
|
649
|
+
flags += ' --gfnff'
|
|
650
|
+
# declaring the use of FF instead of semiempirical
|
|
651
|
+
|
|
652
|
+
elif method.upper() in ('GFN2-XTB', 'GFN2'):
|
|
653
|
+
flags += ' --gfn2'
|
|
654
|
+
|
|
655
|
+
elif method.upper() in ('GFN2-XTB//GFN-FF', 'GFN2//GFNFF'):
|
|
656
|
+
flags += ' --gfn2//gfnff'
|
|
657
|
+
|
|
658
|
+
# adding other options
|
|
659
|
+
if charge != 0:
|
|
660
|
+
flags += f' --chrg {charge}'
|
|
661
|
+
|
|
662
|
+
if procs is not None:
|
|
663
|
+
flags += f' -P {procs}'
|
|
664
|
+
|
|
665
|
+
if threads is not None:
|
|
666
|
+
flags += f' -T {threads}'
|
|
667
|
+
|
|
668
|
+
if solvent is not None:
|
|
669
|
+
|
|
670
|
+
if solvent == 'methanol':
|
|
671
|
+
flags += ' --gbsa methanol'
|
|
672
|
+
|
|
673
|
+
else:
|
|
674
|
+
flags += f' --alpb {solvent}'
|
|
675
|
+
|
|
676
|
+
if kcal is None:
|
|
677
|
+
kcal = 10
|
|
678
|
+
flags += f' --ewin {kcal}'
|
|
679
|
+
|
|
680
|
+
if ncimode:
|
|
681
|
+
flags += ' --nci'
|
|
682
|
+
|
|
683
|
+
flags += ' --noreftopo'
|
|
684
|
+
|
|
685
|
+
try:
|
|
686
|
+
with open(f"{title}.out", "w") as f:
|
|
687
|
+
check_call(f'crest {title}.xyz --cinp {title}.inp {flags}'.split(), stdout=f, stderr=STDOUT)
|
|
688
|
+
|
|
689
|
+
except KeyboardInterrupt:
|
|
690
|
+
print('KeyboardInterrupt requested by user. Quitting.')
|
|
691
|
+
sys.exit()
|
|
692
|
+
|
|
693
|
+
# if CREST crashes, cd into the parent folder before propagating the error
|
|
694
|
+
except CalledProcessError:
|
|
695
|
+
os.chdir(os.path.dirname(os.getcwd()))
|
|
696
|
+
raise CalledProcessError
|
|
697
|
+
|
|
698
|
+
new_coords = read_xyz('crest_conformers.xyz').atomcoords
|
|
699
|
+
|
|
700
|
+
# clean_directory((f'{title}.inp', f'{title}.xyz', f"{title}.out"))
|
|
701
|
+
|
|
702
|
+
for filename in ('gfnff_topo',
|
|
703
|
+
'charges',
|
|
704
|
+
'wbo',
|
|
705
|
+
'xtbrestart',
|
|
706
|
+
'xtbtopo.mol',
|
|
707
|
+
'.xtboptok',
|
|
708
|
+
'gfnff_adjacency',
|
|
709
|
+
'gfnff_charges',
|
|
710
|
+
):
|
|
711
|
+
try:
|
|
712
|
+
os.remove(filename)
|
|
713
|
+
except FileNotFoundError:
|
|
714
|
+
pass
|
|
715
|
+
|
|
716
|
+
os.chdir(os.path.dirname(os.getcwd()))
|
|
717
|
+
# shutil.rmtree(os.path.join(os.getcwd(), title))
|
|
718
|
+
|
|
719
|
+
return new_coords
|
|
720
|
+
|
|
721
|
+
def xtb_gsolv(coords, atomnos, model='alpb', charge=0, solvent='ch2cl2', title='temp', assert_convergence=True):
|
|
722
|
+
'''
|
|
723
|
+
Returns the solvation free energy in kcal/mol, as computed by XTB.
|
|
724
|
+
Single-point energy calculation carried out with GFN-FF.
|
|
725
|
+
|
|
726
|
+
'''
|
|
727
|
+
|
|
728
|
+
# create working folder and cd into it
|
|
729
|
+
if title in os.listdir():
|
|
730
|
+
shutil.rmtree(os.path.join(os.getcwd(), title))
|
|
731
|
+
|
|
732
|
+
os.mkdir(title)
|
|
733
|
+
os.chdir(os.path.join(os.getcwd(), title))
|
|
734
|
+
|
|
735
|
+
with open(f'{title}.xyz', 'w') as f:
|
|
736
|
+
write_xyz(coords, atomnos, f, title=title)
|
|
737
|
+
|
|
738
|
+
# outname = f'{title}_xtbopt.xyz' DOES NOT WORK - XTB ISSUE?
|
|
739
|
+
outname = 'xtbopt.xyz'
|
|
740
|
+
flags = '--norestart'
|
|
741
|
+
|
|
742
|
+
# declaring the use of FF instead of semiempirical
|
|
743
|
+
flags += ' --gfnff'
|
|
744
|
+
|
|
745
|
+
if charge != 0:
|
|
746
|
+
flags += f' --chrg {charge}'
|
|
747
|
+
|
|
748
|
+
flags += f' --{model} {solvent}'
|
|
749
|
+
|
|
750
|
+
try:
|
|
751
|
+
with open(f"{title}.out", "w") as f:
|
|
752
|
+
check_call(f'xtb {title}.xyz {flags}'.split(), stdout=f, stderr=STDOUT)
|
|
753
|
+
|
|
754
|
+
# sometimes the SCC does not converge: only raise the error if specified
|
|
755
|
+
except CalledProcessError:
|
|
756
|
+
if assert_convergence:
|
|
757
|
+
raise CalledProcessError
|
|
758
|
+
|
|
759
|
+
except KeyboardInterrupt:
|
|
760
|
+
print('KeyboardInterrupt requested by user. Quitting.')
|
|
761
|
+
sys.exit()
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
else:
|
|
765
|
+
gsolv = energy_grepper(f"{title}.out", '-> Gsolv', 3)
|
|
766
|
+
clean_directory((f'{title}.inp', f'{title}.xyz', f"{title}.out", outname))
|
|
767
|
+
|
|
768
|
+
for filename in ('gfnff_topo',
|
|
769
|
+
'charges',
|
|
770
|
+
'wbo',
|
|
771
|
+
'xtbrestart',
|
|
772
|
+
'xtbtopo.mol',
|
|
773
|
+
'.xtboptok',
|
|
774
|
+
'gfnff_adjacency',
|
|
775
|
+
'gfnff_charges',
|
|
776
|
+
):
|
|
777
|
+
try:
|
|
778
|
+
os.remove(filename)
|
|
779
|
+
except FileNotFoundError:
|
|
780
|
+
pass
|
|
781
|
+
|
|
782
|
+
# get out of working folder and delete it
|
|
783
|
+
os.chdir(os.path.dirname(os.getcwd()))
|
|
784
|
+
shutil.rmtree(os.path.join(os.getcwd(), title))
|
|
785
|
+
|
|
786
|
+
return gsolv
|