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,364 @@
|
|
|
1
|
+
# coding=utf-8
|
|
2
|
+
'''
|
|
3
|
+
|
|
4
|
+
FIRECODE: Filtering Refiner and Embedder for Conformationally Dense Ensembles
|
|
5
|
+
Copyright (C) 2021 Nicolò Tampellini
|
|
6
|
+
|
|
7
|
+
This program is free software: you can redistribute it and/or modify
|
|
8
|
+
it under the terms of the GNU General Public License as published by
|
|
9
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
10
|
+
(at your option) any later version.
|
|
11
|
+
|
|
12
|
+
This program is distributed in the hope that it will be useful,
|
|
13
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
GNU General Public License for more details.
|
|
16
|
+
|
|
17
|
+
'''
|
|
18
|
+
|
|
19
|
+
import os
|
|
20
|
+
import warnings
|
|
21
|
+
from copy import deepcopy
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
from numpy.linalg import LinAlgError
|
|
25
|
+
|
|
26
|
+
from firecode.algebra import get_inertia_moments, norm_of
|
|
27
|
+
from firecode.errors import CCReadError, NoOrbitalError
|
|
28
|
+
from firecode.graph_manipulations import (graphize, is_sigmatropic, is_vicinal,
|
|
29
|
+
neighbors)
|
|
30
|
+
from firecode.pt import pt
|
|
31
|
+
from firecode.reactive_atoms_classes import get_atom_type
|
|
32
|
+
from firecode.rmsd import get_alignment_matrix
|
|
33
|
+
from firecode.utils import flatten, read_xyz, smi_to_3d
|
|
34
|
+
|
|
35
|
+
warnings.simplefilter("ignore", UserWarning)
|
|
36
|
+
|
|
37
|
+
def align_by_moi(structures, atomnos, **kwargs):
|
|
38
|
+
'''
|
|
39
|
+
Aligns molecules of a structure array (shape is (n_structures, n_atoms, 3))
|
|
40
|
+
to the first one, based on the the moments of inertia vectors.
|
|
41
|
+
Returns the aligned array.
|
|
42
|
+
|
|
43
|
+
'''
|
|
44
|
+
|
|
45
|
+
reference, *targets = structures
|
|
46
|
+
|
|
47
|
+
masses = np.array([pt[a].mass for a in atomnos])
|
|
48
|
+
|
|
49
|
+
# center all the structures at the origin
|
|
50
|
+
reference -= np.mean(reference, axis=0)
|
|
51
|
+
for t, target in enumerate(targets):
|
|
52
|
+
targets[t] -= np.mean(target, axis=0)
|
|
53
|
+
|
|
54
|
+
# initialize output array
|
|
55
|
+
output = np.zeros(structures.shape)
|
|
56
|
+
output[0] = reference
|
|
57
|
+
|
|
58
|
+
# reference vectors
|
|
59
|
+
ref_moi_vecs = np.eye(3)
|
|
60
|
+
(ref_moi_vecs[0,0],
|
|
61
|
+
ref_moi_vecs[1,1],
|
|
62
|
+
ref_moi_vecs[2,2]) = get_inertia_moments(reference, masses)
|
|
63
|
+
|
|
64
|
+
for t, target in enumerate(targets):
|
|
65
|
+
|
|
66
|
+
tgt_moi_vecs = np.eye(3)
|
|
67
|
+
(tgt_moi_vecs[0,0],
|
|
68
|
+
tgt_moi_vecs[1,1],
|
|
69
|
+
tgt_moi_vecs[2,2]) = get_inertia_moments(target, masses)
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
matrix = get_alignment_matrix(ref_moi_vecs, tgt_moi_vecs)
|
|
73
|
+
|
|
74
|
+
except LinAlgError:
|
|
75
|
+
# it is actually possible for the kabsch alg not to converge
|
|
76
|
+
matrix = np.eye(3)
|
|
77
|
+
|
|
78
|
+
# output[t+1] = np.array([matrix @ vector for vector in target])
|
|
79
|
+
output[t+1] = (matrix @ target.T).T
|
|
80
|
+
|
|
81
|
+
return output
|
|
82
|
+
|
|
83
|
+
class Hypermolecule:
|
|
84
|
+
'''
|
|
85
|
+
Molecule class to be used within firecode.
|
|
86
|
+
'''
|
|
87
|
+
|
|
88
|
+
def __repr__(self):
|
|
89
|
+
r = self.rootname
|
|
90
|
+
if hasattr(self, 'reactive_atoms_classes_dict'):
|
|
91
|
+
r += f' {[str(atom) for atom in self.reactive_atoms_classes_dict[0].values()]}'
|
|
92
|
+
return r
|
|
93
|
+
|
|
94
|
+
def __init__(self, filename, reactive_indices=None, debug_logfunction=None):
|
|
95
|
+
'''
|
|
96
|
+
Initializing class properties: reading conformational ensemble file, aligning
|
|
97
|
+
conformers to first and centering them in origin.
|
|
98
|
+
|
|
99
|
+
:params filename: Input file name. Can be anything, .xyz preferred
|
|
100
|
+
:params reactive_indices: Index of atoms that will link during the desired reaction.
|
|
101
|
+
May be either int or list of int.
|
|
102
|
+
'''
|
|
103
|
+
|
|
104
|
+
if not os.path.isfile(filename):
|
|
105
|
+
if '.' in filename:
|
|
106
|
+
raise SyntaxError((f'Molecule {filename} cannot be read. Please check your syntax.'))
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
filename = smi_to_3d(filename, "generated_3D_coords")
|
|
110
|
+
print(f"--> Embedded SMILES string to 3D structure, saved as {filename}")
|
|
111
|
+
except Exception:
|
|
112
|
+
raise SyntaxError((f'The program is trying to read something that is not a valid molecule input ({filename}). ' +
|
|
113
|
+
'If this looks like a keyword, it is probably faulted by a syntax error.'))
|
|
114
|
+
|
|
115
|
+
self.rootname = filename.split('.')[0]
|
|
116
|
+
self.filename = filename
|
|
117
|
+
self.debug_logfunction = debug_logfunction
|
|
118
|
+
|
|
119
|
+
if isinstance(reactive_indices, np.ndarray):
|
|
120
|
+
self.reactive_indices = reactive_indices
|
|
121
|
+
else:
|
|
122
|
+
self.reactive_indices = np.array(reactive_indices) if isinstance(reactive_indices, (tuple, list)) else ()
|
|
123
|
+
|
|
124
|
+
ccread_object = read_xyz(filename)
|
|
125
|
+
|
|
126
|
+
if ccread_object is None:
|
|
127
|
+
raise CCReadError(f'Cannot read file {filename}')
|
|
128
|
+
|
|
129
|
+
coordinates = np.array(ccread_object.atomcoords)
|
|
130
|
+
|
|
131
|
+
self.atomnos = ccread_object.atomnos
|
|
132
|
+
self.position = np.array([0,0,0], dtype=float) # used in Embedder class
|
|
133
|
+
self.rotation = np.identity(3) # used in Embedder class - rotation matrix
|
|
134
|
+
|
|
135
|
+
assert all([len(coordinates[i])==len(coordinates[0]) for i in range(1, len(coordinates))]), 'Ensembles must have constant atom number.'
|
|
136
|
+
# Checking that ensemble has constant length
|
|
137
|
+
if self.debug_logfunction is not None:
|
|
138
|
+
debug_logfunction(f'DEBUG: Hypermolecule Class __init__ ({filename}) - Initializing object {filename}, read {len(coordinates)} structures with {len(coordinates[0])} atoms')
|
|
139
|
+
|
|
140
|
+
self.centroid = np.sum(np.sum(coordinates, axis=0), axis=0) / (len(coordinates) * len(coordinates[0]))
|
|
141
|
+
|
|
142
|
+
self.atomcoords = coordinates - self.centroid
|
|
143
|
+
self.graph = graphize(self.atomcoords[0], self.atomnos)
|
|
144
|
+
# show_graph(self)
|
|
145
|
+
|
|
146
|
+
self.atoms = np.array([atom for structure in self.atomcoords for atom in structure]) # single list with all atomic positions
|
|
147
|
+
|
|
148
|
+
def compute_orbitals(self, override=None):
|
|
149
|
+
'''
|
|
150
|
+
Computes orbital positions for atoms in self.reactive_atoms
|
|
151
|
+
'''
|
|
152
|
+
|
|
153
|
+
if self.reactive_indices is None:
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
self.sp3_sigmastar, self.sigmatropic = None, None
|
|
157
|
+
|
|
158
|
+
self._inspect_reactive_atoms(override=override)
|
|
159
|
+
# sets reactive atoms properties
|
|
160
|
+
|
|
161
|
+
# self.atomcoords = align_structures(self.atomcoords, self.get_alignment_indices())
|
|
162
|
+
self.sigmatropic = [is_sigmatropic(self, c) for c, _ in enumerate(self.atomcoords)]
|
|
163
|
+
self.sp3_sigmastar = is_vicinal(self)
|
|
164
|
+
|
|
165
|
+
for c, _ in enumerate(self.atomcoords):
|
|
166
|
+
for index, reactive_atom in self.reactive_atoms_classes_dict[c].items():
|
|
167
|
+
reactive_atom.init(self, index, update=True, conf=c)
|
|
168
|
+
# update properties into reactive_atom class.
|
|
169
|
+
# Since now we have mol.sigmatropic and mol.sigmastar,
|
|
170
|
+
# We can update, that is set the reactive_atom.center attribute
|
|
171
|
+
|
|
172
|
+
def _set_reactive_indices(self, filename):
|
|
173
|
+
'''
|
|
174
|
+
Manually set the molecule reactive atoms from the ASE GUI, imposing
|
|
175
|
+
constraints on the desired atoms.
|
|
176
|
+
|
|
177
|
+
'''
|
|
178
|
+
from ase import Atoms
|
|
179
|
+
from ase.gui.gui import GUI
|
|
180
|
+
from ase.gui.images import Images
|
|
181
|
+
|
|
182
|
+
data = read_xyz(filename)
|
|
183
|
+
coords = data.atomcoords[0]
|
|
184
|
+
labels = ''.join([pt[i].symbol for i in data.atomnos])
|
|
185
|
+
|
|
186
|
+
atoms = Atoms(labels, positions=coords)
|
|
187
|
+
|
|
188
|
+
while atoms.constraints == []:
|
|
189
|
+
print(('\nPlease, manually select the reactive atom(s) for molecule %s.'
|
|
190
|
+
'\nRotate with right click and select atoms by clicking. Multiple selections can be done by Ctrl+Click.'
|
|
191
|
+
'\nWith desired atom(s) selected, go to Tools -> Constraints -> Constrain, then close the GUI.') % (filename))
|
|
192
|
+
|
|
193
|
+
GUI(images=Images([atoms]), show_bonds=True).run()
|
|
194
|
+
|
|
195
|
+
return list(atoms.constraints[0].get_indices())
|
|
196
|
+
|
|
197
|
+
def get_alignment_indices(self):
|
|
198
|
+
'''
|
|
199
|
+
Return the indices to align the molecule to, given a list of
|
|
200
|
+
atoms that should be reacting. List is composed by reactive atoms
|
|
201
|
+
plus adjacent atoms.
|
|
202
|
+
:param coords: coordinates of a single molecule
|
|
203
|
+
:param reactive atoms: int or list of ints
|
|
204
|
+
:return: list of indices
|
|
205
|
+
'''
|
|
206
|
+
if len(self.reactive_indices) == 0:
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
indices = set()
|
|
210
|
+
for atom in self.reactive_indices:
|
|
211
|
+
indices |= set(list([(a, b) for a, b in self.graph.adjacency()][atom][1].keys()))
|
|
212
|
+
|
|
213
|
+
if self.debug_logfunction is not None:
|
|
214
|
+
self.debug_logfunction(f'DEBUG: Hypermolecule.get_aligment_indices {self.filename} - Alignment indices are {list(indices)})')
|
|
215
|
+
|
|
216
|
+
return list(indices)
|
|
217
|
+
|
|
218
|
+
def _inspect_reactive_atoms(self, override=None):
|
|
219
|
+
'''
|
|
220
|
+
Control the type of reactive atoms and sets the class attribute self.reactive_atoms_classes_dict
|
|
221
|
+
'''
|
|
222
|
+
self.reactive_atoms_classes_dict = {c:{} for c, _ in enumerate(self.atomcoords)}
|
|
223
|
+
|
|
224
|
+
for c, _ in enumerate(self.atomcoords):
|
|
225
|
+
for index in self.reactive_indices:
|
|
226
|
+
symbol = pt[self.atomnos[index]].symbol
|
|
227
|
+
|
|
228
|
+
atom_type = get_atom_type(self.graph, index, override=override)()
|
|
229
|
+
|
|
230
|
+
# setting the reactive_atom class type
|
|
231
|
+
atom_type.init(self, index, conf=c)
|
|
232
|
+
|
|
233
|
+
# understanding the type of reactive atom in order to align the ensemble correctly and build the correct pseudo-orbitals
|
|
234
|
+
self.reactive_atoms_classes_dict[c][index] = atom_type
|
|
235
|
+
|
|
236
|
+
if self.debug_logfunction is not None:
|
|
237
|
+
self.debug_logfunction(f'DEBUG: Hypermolecule._inspect_reactive_atoms {self.filename} - Reactive atom {index+1} is a {symbol} atom of {atom_type} type. It is bonded to {len(neighbors(self.graph, index))} neighbor(s): {atom_type.neighbors_symbols}')
|
|
238
|
+
|
|
239
|
+
def _scale_orbs(self, value):
|
|
240
|
+
'''
|
|
241
|
+
Scale each orbital dimension according to value.
|
|
242
|
+
'''
|
|
243
|
+
for c, _ in enumerate(self.atomcoords):
|
|
244
|
+
for index, atom in self.reactive_atoms_classes_dict[c].items():
|
|
245
|
+
orb_dim = norm_of(atom.center[0]-atom.coord)
|
|
246
|
+
atom.init(self, index, update=True, orb_dim=orb_dim*value, conf=c)
|
|
247
|
+
|
|
248
|
+
def get_r_atoms(self, c):
|
|
249
|
+
'''
|
|
250
|
+
c: conformer number
|
|
251
|
+
'''
|
|
252
|
+
return list(self.reactive_atoms_classes_dict[c].values())
|
|
253
|
+
|
|
254
|
+
def get_centers(self, c):
|
|
255
|
+
'''
|
|
256
|
+
c: conformer number
|
|
257
|
+
'''
|
|
258
|
+
return np.array([[v for v in atom.center] for atom in self.get_r_atoms(c)])
|
|
259
|
+
|
|
260
|
+
# def calc_positioned_conformers(self):
|
|
261
|
+
# self.positioned_conformers = np.array([[self.rotation @ v + self.position for v in conformer] for conformer in self.atomcoords])
|
|
262
|
+
|
|
263
|
+
def _compute_hypermolecule(self):
|
|
264
|
+
'''
|
|
265
|
+
'''
|
|
266
|
+
|
|
267
|
+
self.energies = [0 for _ in self.atomcoords]
|
|
268
|
+
|
|
269
|
+
self.hypermolecule_atomnos = []
|
|
270
|
+
clusters = {i:{} for i, _ in enumerate(self.atomnos)} # {atom_index:{cluster_number:[position,times_found]}}
|
|
271
|
+
for i, atom_number in enumerate(self.atomnos):
|
|
272
|
+
atoms_arrangement = [conformer[i] for conformer in self.atomcoords]
|
|
273
|
+
cluster_number = 0
|
|
274
|
+
clusters[i][cluster_number] = [atoms_arrangement[0], 1] # first structure has rel E = 0 so its weight is surely 1
|
|
275
|
+
self.hypermolecule_atomnos.append(atom_number)
|
|
276
|
+
radii = pt[atom_number].covalent_radius
|
|
277
|
+
for j, atom in enumerate(atoms_arrangement[1:]):
|
|
278
|
+
|
|
279
|
+
weight = np.exp(-self.energies[j+1] * 503.2475342795285 / self.T)
|
|
280
|
+
# print(f'Atom {i} in conf {j+1} weight is {weight} - rel. E was {self.energies[j+1]}')
|
|
281
|
+
|
|
282
|
+
for cluster_number, reference in deepcopy(clusters[i]).items():
|
|
283
|
+
if norm_of(atom - reference[0]) < radii:
|
|
284
|
+
clusters[i][cluster_number][1] += weight
|
|
285
|
+
else:
|
|
286
|
+
clusters[i][max(clusters[i].keys())+1] = [atom, weight]
|
|
287
|
+
self.hypermolecule_atomnos.append(atom_number)
|
|
288
|
+
|
|
289
|
+
self.weights = [[] for _ in self.atomnos]
|
|
290
|
+
self.hypermolecule = []
|
|
291
|
+
|
|
292
|
+
for i, _ in enumerate(self.atomnos):
|
|
293
|
+
for _, data in clusters[i].items():
|
|
294
|
+
self.weights[i].append(data[1])
|
|
295
|
+
self.hypermolecule.append(data[0])
|
|
296
|
+
|
|
297
|
+
self.hypermolecule = np.asarray(self.hypermolecule)
|
|
298
|
+
self.weights = np.array(self.weights).flatten()
|
|
299
|
+
self.weights = np.array([weights / np.sum(weights) for weights in self.weights])
|
|
300
|
+
self.weights = flatten(self.weights)
|
|
301
|
+
|
|
302
|
+
self.dimensions = (max([coord[0] for coord in self.hypermolecule]) - min([coord[0] for coord in self.hypermolecule]),
|
|
303
|
+
max([coord[1] for coord in self.hypermolecule]) - min([coord[1] for coord in self.hypermolecule]),
|
|
304
|
+
max([coord[2] for coord in self.hypermolecule]) - min([coord[2] for coord in self.hypermolecule]))
|
|
305
|
+
|
|
306
|
+
def write_hypermolecule(self):
|
|
307
|
+
'''
|
|
308
|
+
'''
|
|
309
|
+
|
|
310
|
+
hyp_name = self.rootname + '_hypermolecule.xyz'
|
|
311
|
+
with open(hyp_name, 'w') as f:
|
|
312
|
+
for c, _ in enumerate(self.atomcoords):
|
|
313
|
+
f.write(str(sum([len(atom.center) for atom in self.reactive_atoms_classes_dict[c].values()]) + len(self.atomcoords[0])))
|
|
314
|
+
f.write(f'\FIRECODE Hypermolecule {c} for {self.rootname} - reactive indices {self.reactive_indices}\n')
|
|
315
|
+
orbs =np.vstack([atom_type.center for atom_type in self.reactive_atoms_classes_dict[c].values()]).ravel()
|
|
316
|
+
orbs = orbs.reshape((int(len(orbs)/3), 3))
|
|
317
|
+
for i, atom in enumerate(self.atomcoords[c]):
|
|
318
|
+
f.write('%-5s %-8s %-8s %-8s\n' % (pt[self.atomnos[i]].symbol, round(atom[0], 6), round(atom[1], 6), round(atom[2], 6)))
|
|
319
|
+
for orb in orbs:
|
|
320
|
+
f.write('%-5s %-8s %-8s %-8s\n' % ('X', round(orb[0], 6), round(orb[1], 6), round(orb[2], 6)))
|
|
321
|
+
|
|
322
|
+
def get_orbital_length(self, index):
|
|
323
|
+
'''
|
|
324
|
+
index: reactive atom index
|
|
325
|
+
'''
|
|
326
|
+
if index not in self.reactive_indices:
|
|
327
|
+
raise NoOrbitalError(f'Index provided must be a molecule reactive index ({index}, {self.filename})')
|
|
328
|
+
|
|
329
|
+
r_atom = self.reactive_atoms_classes_dict[0][index]
|
|
330
|
+
return norm_of(r_atom.center[0] - r_atom.coord)
|
|
331
|
+
|
|
332
|
+
class Pivot:
|
|
333
|
+
'''
|
|
334
|
+
(Cyclical embed)
|
|
335
|
+
Pivot object: vector connecting two lobes of a
|
|
336
|
+
molecule, starting from v1 (first reactive atom in
|
|
337
|
+
mol.reacitve_atoms_classes_dict) and ending on v2.
|
|
338
|
+
|
|
339
|
+
For molecules involved in chelotropic reactions,
|
|
340
|
+
that is molecules that undergo a cyclical embed
|
|
341
|
+
while having only one reactive atom, pivots are
|
|
342
|
+
built on that single atom.
|
|
343
|
+
'''
|
|
344
|
+
def __init__(self, c1, c2, a1, a2, index1, index2):
|
|
345
|
+
'''
|
|
346
|
+
c: centers (orbital centers)
|
|
347
|
+
v: vectors (orbital vectors, non-normalized)
|
|
348
|
+
i: indices (of coordinates, in mol.center)
|
|
349
|
+
'''
|
|
350
|
+
self.start = c1
|
|
351
|
+
self.end = c2
|
|
352
|
+
|
|
353
|
+
self.start_atom = a1
|
|
354
|
+
self.end_atom = a2
|
|
355
|
+
|
|
356
|
+
self.pivot = c2 - c1
|
|
357
|
+
self.meanpoint = np.mean((c1, c2), axis=0)
|
|
358
|
+
self.index = (index1, index2)
|
|
359
|
+
# the pivot starts from the index1-th
|
|
360
|
+
# center of the first reactive atom
|
|
361
|
+
# and to the index2-th center of the second
|
|
362
|
+
|
|
363
|
+
def __repr__(self):
|
|
364
|
+
return f'Pivot object - index {self.index}, norm {round(norm_of(self.pivot), 3)}, meanpoint {self.meanpoint}'
|
firecode/mep_relaxer.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import warnings
|
|
3
|
+
|
|
4
|
+
import matplotlib.pyplot as plt
|
|
5
|
+
import numpy as np
|
|
6
|
+
from ase import Atoms
|
|
7
|
+
from ase.calculators.calculator import CalculationFailed
|
|
8
|
+
from ase.dyneb import DyNEB
|
|
9
|
+
from ase.optimize import LBFGS
|
|
10
|
+
|
|
11
|
+
from firecode.ase_manipulations import (PreventScramblingConstraint, ase_dump,
|
|
12
|
+
get_ase_calc)
|
|
13
|
+
from firecode.utils import align_structures, time_to_string
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def ase_mep_relax(
|
|
17
|
+
embedder,
|
|
18
|
+
structures,
|
|
19
|
+
atomnos,
|
|
20
|
+
n_images=None,
|
|
21
|
+
maxiter=200,
|
|
22
|
+
title='temp',
|
|
23
|
+
optimizer=LBFGS,
|
|
24
|
+
logfunction=None,
|
|
25
|
+
write_plot=False,
|
|
26
|
+
verbose_print=False,
|
|
27
|
+
safe=False,
|
|
28
|
+
):
|
|
29
|
+
'''
|
|
30
|
+
embedder: firecode embedder object
|
|
31
|
+
structures: array of coordinates to be used as starting points
|
|
32
|
+
atomnos: 1-d array of atomic numbers
|
|
33
|
+
n_images: total number of optimized images connecting reag/prods
|
|
34
|
+
maxiter: maximum number of ensemble optimization steps
|
|
35
|
+
title: name used to write the final MEP as a .xyz file
|
|
36
|
+
optimizer: ASE optimizer to be used
|
|
37
|
+
logfunction: filename to dump the optimization data to. If None, no file is written.
|
|
38
|
+
write_plot: bool, prints a matplotlib plot with energy information
|
|
39
|
+
|
|
40
|
+
return: 3- element tuple with coodinates of the MEP, energy of the structures in
|
|
41
|
+
kcal/mol and a boolean value indicating success.
|
|
42
|
+
'''
|
|
43
|
+
|
|
44
|
+
if n_images is None:
|
|
45
|
+
n_images = 10
|
|
46
|
+
|
|
47
|
+
if len(structures) < n_images:
|
|
48
|
+
# images = interpolate_structures(align_structures(structures), atomnos, n=n_images)
|
|
49
|
+
|
|
50
|
+
# # If any molecule exploded, try linear interpolation
|
|
51
|
+
# if any([True in np.isnan(image.get_positions()) for image in images]) or (
|
|
52
|
+
# np.max([image.get_positions() for image in images]) > 100):
|
|
53
|
+
|
|
54
|
+
# if logfunction is not None:
|
|
55
|
+
# logfunction(f'\n--> IDPP interpolation of structures failed, falling back to linear interpolation.')
|
|
56
|
+
|
|
57
|
+
images = interpolate_structures(align_structures(structures), atomnos, n=n_images, method='linear')
|
|
58
|
+
|
|
59
|
+
if logfunction is not None:
|
|
60
|
+
logfunction(f'\n--> Interpolation of structures successful ({len(images)} images)')
|
|
61
|
+
|
|
62
|
+
else:
|
|
63
|
+
images = [Atoms(atomnos, positions=coords) for coords in align_structures(structures)]
|
|
64
|
+
|
|
65
|
+
ase_dump('interpolated_MEP_guess.xyz', images, atomnos)
|
|
66
|
+
|
|
67
|
+
neb = DyNEB(images,
|
|
68
|
+
k=0.1,
|
|
69
|
+
fmax=0.05,
|
|
70
|
+
climb=False,
|
|
71
|
+
# parallel=True,
|
|
72
|
+
remove_rotation_and_translation=True,
|
|
73
|
+
method='aseneb',
|
|
74
|
+
scale_fmax=1,
|
|
75
|
+
allow_shared_calculator=True,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Set calculators for all images
|
|
79
|
+
for _, image in enumerate(images):
|
|
80
|
+
image.calc = get_ase_calc(embedder)
|
|
81
|
+
|
|
82
|
+
if safe:
|
|
83
|
+
bond_constr = PreventScramblingConstraint(embedder.objects[0].graph, image)
|
|
84
|
+
image.set_constraint([bond_constr])
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
t_start = time.perf_counter()
|
|
88
|
+
|
|
89
|
+
# Set the optimizer and optimize
|
|
90
|
+
try:
|
|
91
|
+
|
|
92
|
+
with warnings.catch_warnings():
|
|
93
|
+
warnings.simplefilter('ignore')
|
|
94
|
+
# ignore runtime warnings from the NEB module:
|
|
95
|
+
# if something went wrong, we will deal with it later
|
|
96
|
+
|
|
97
|
+
with optimizer(neb, maxstep=0.1, logfile=None if not verbose_print else 'mep_relax_opt.log') as opt:
|
|
98
|
+
|
|
99
|
+
if logfunction is not None:
|
|
100
|
+
logfunction(f'--> Running MEP relaxation through ASE ({embedder.options.theory_level} via {embedder.options.calculator})')
|
|
101
|
+
|
|
102
|
+
for ss in range(1, maxiter//10+1):
|
|
103
|
+
opt.run(fmax=0.05, steps=maxiter//10*ss)
|
|
104
|
+
|
|
105
|
+
if logfunction is not None:
|
|
106
|
+
logfunction(f'--> Ran {maxiter//10*ss} steps, wrote partially optimized traj to {title}_MEP.xyz')
|
|
107
|
+
|
|
108
|
+
ase_dump(f'{title}_MEP.xyz', images, atomnos, [image.get_total_energy() * 23.06054194532933 for image in images])
|
|
109
|
+
|
|
110
|
+
iterations = opt.nsteps
|
|
111
|
+
exit_status = 'CONVERGED' if iterations < maxiter-1 else 'MAX ITER'
|
|
112
|
+
|
|
113
|
+
except (CalculationFailed):
|
|
114
|
+
if logfunction is not None:
|
|
115
|
+
logfunction(f' - MEP relax for {title} CRASHED ({time_to_string(time.perf_counter()-t_start)})\n')
|
|
116
|
+
try:
|
|
117
|
+
ase_dump(f'{title}_MEP_crashed.xyz', neb.images, atomnos)
|
|
118
|
+
except Exception():
|
|
119
|
+
pass
|
|
120
|
+
return None, None, False
|
|
121
|
+
|
|
122
|
+
except KeyboardInterrupt:
|
|
123
|
+
exit_status = 'ABORTED BY USER'
|
|
124
|
+
|
|
125
|
+
if logfunction is not None:
|
|
126
|
+
logfunction(f' - NEB for {title} {exit_status} ({time_to_string(time.perf_counter()-t_start)})\n')
|
|
127
|
+
|
|
128
|
+
energies = [image.get_total_energy() * 23.06054194532933 for image in images] # eV to kcal/mol
|
|
129
|
+
|
|
130
|
+
ase_dump(f'{title}_MEP.xyz', images, atomnos, energies)
|
|
131
|
+
# Save the converged MEP (minimum energy path) to an .xyz file
|
|
132
|
+
|
|
133
|
+
if write_plot:
|
|
134
|
+
|
|
135
|
+
plt.figure()
|
|
136
|
+
plt.plot(
|
|
137
|
+
range(1,len(images)+1),
|
|
138
|
+
np.array(energies)-min(energies),
|
|
139
|
+
color='tab:blue',
|
|
140
|
+
label='Image energies',
|
|
141
|
+
linewidth=3,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
plt.legend()
|
|
145
|
+
plt.title(title)
|
|
146
|
+
plt.xlabel('Image number')
|
|
147
|
+
plt.ylabel('Rel. E. (kcal/mol)')
|
|
148
|
+
plt.savefig(f'{title.replace(" ", "_")}_plt.svg')
|
|
149
|
+
|
|
150
|
+
mep = np.array([image.get_positions() for image in images])
|
|
151
|
+
|
|
152
|
+
return mep, energies, exit_status
|
|
153
|
+
|
|
154
|
+
def interpolate_structures(structures, atomnos, n, method='idpp'):
|
|
155
|
+
'''
|
|
156
|
+
Return n interpolated structures from the
|
|
157
|
+
first to the last present in structures
|
|
158
|
+
as a list of ASE image objects.
|
|
159
|
+
|
|
160
|
+
'''
|
|
161
|
+
if len(structures) == 2:
|
|
162
|
+
images = [None for _ in range(n)]
|
|
163
|
+
images[0] = Atoms(atomnos, positions=structures[0])
|
|
164
|
+
images[-1] = Atoms(atomnos, positions=structures[-1])
|
|
165
|
+
group_ranges = [(0,n-1)]
|
|
166
|
+
|
|
167
|
+
else:
|
|
168
|
+
# calculate the expansion ratio between what we have and what we want
|
|
169
|
+
ratio = n/len(structures)
|
|
170
|
+
|
|
171
|
+
# calculate where original structures will be mapped in the final set
|
|
172
|
+
mappings = [round(i*ratio) for i, _ in enumerate(structures)]
|
|
173
|
+
mappings[-1] = len(structures)
|
|
174
|
+
|
|
175
|
+
# initialize output container with initial structures mapped in
|
|
176
|
+
images = [Atoms(atomnos, positions=structures[mappings.index(i)])
|
|
177
|
+
if i in mappings else None for i in range(n)]
|
|
178
|
+
images[-1] = Atoms(atomnos, positions=structures[-1])
|
|
179
|
+
|
|
180
|
+
# calculate ranges to fill
|
|
181
|
+
group_ranges = [(mappings[i], mappings[i+1]) for i, _ in enumerate(mappings[:-1])
|
|
182
|
+
if mappings[i+1] - mappings[i] > 1]
|
|
183
|
+
group_ranges.append((max(mappings), n-1))
|
|
184
|
+
|
|
185
|
+
# fill them by interpolating nearby images
|
|
186
|
+
for (ref_1, ref_2) in group_ranges:
|
|
187
|
+
|
|
188
|
+
struc_ref1 = images[ref_1]
|
|
189
|
+
struc_ref2 = images[ref_2]
|
|
190
|
+
|
|
191
|
+
images_temp = [struc_ref1] + [struc_ref1.copy() for _ in range(ref_2-ref_1-1)] + [struc_ref2]
|
|
192
|
+
interp_temp = DyNEB(images_temp)
|
|
193
|
+
interp_temp.interpolate(method=method)
|
|
194
|
+
|
|
195
|
+
# replace previous blanks with interpolated structures
|
|
196
|
+
for i in range(ref_1+1, ref_2):
|
|
197
|
+
images[i] = interp_temp.images[i-ref_1]
|
|
198
|
+
|
|
199
|
+
return images
|