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,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
|