firecode 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. firecode/TEST_NOTEBOOK.ipynb +3940 -0
  2. firecode/__init__.py +0 -0
  3. firecode/__main__.py +118 -0
  4. firecode/_gaussian.py +97 -0
  5. firecode/algebra.py +405 -0
  6. firecode/ase_manipulations.py +879 -0
  7. firecode/atropisomer_module.py +516 -0
  8. firecode/automep.py +130 -0
  9. firecode/calculators/__init__.py +29 -0
  10. firecode/calculators/_gaussian.py +98 -0
  11. firecode/calculators/_mopac.py +242 -0
  12. firecode/calculators/_openbabel.py +154 -0
  13. firecode/calculators/_orca.py +129 -0
  14. firecode/calculators/_xtb.py +786 -0
  15. firecode/concurrent_test.py +119 -0
  16. firecode/embedder.py +2590 -0
  17. firecode/embedder_options.py +577 -0
  18. firecode/embeds.py +881 -0
  19. firecode/errors.py +65 -0
  20. firecode/graph_manipulations.py +333 -0
  21. firecode/hypermolecule_class.py +364 -0
  22. firecode/mep_relaxer.py +199 -0
  23. firecode/modify_settings.py +186 -0
  24. firecode/mprof.py +65 -0
  25. firecode/multiembed.py +148 -0
  26. firecode/nci.py +186 -0
  27. firecode/numba_functions.py +260 -0
  28. firecode/operators.py +776 -0
  29. firecode/optimization_methods.py +609 -0
  30. firecode/parameters.py +84 -0
  31. firecode/pka.py +275 -0
  32. firecode/profiler.py +17 -0
  33. firecode/pruning.py +421 -0
  34. firecode/pt.py +32 -0
  35. firecode/quotes.json +6651 -0
  36. firecode/quotes.py +9 -0
  37. firecode/reactive_atoms_classes.py +666 -0
  38. firecode/references.py +11 -0
  39. firecode/rmsd.py +74 -0
  40. firecode/settings.py +75 -0
  41. firecode/solvents.py +126 -0
  42. firecode/tests/C2F2H4.xyz +10 -0
  43. firecode/tests/C2H4.xyz +8 -0
  44. firecode/tests/CH3Cl.xyz +7 -0
  45. firecode/tests/HCOOH.xyz +7 -0
  46. firecode/tests/HCOOOH.xyz +8 -0
  47. firecode/tests/chelotropic.txt +3 -0
  48. firecode/tests/cyclical.txt +3 -0
  49. firecode/tests/dihedral.txt +2 -0
  50. firecode/tests/string.txt +3 -0
  51. firecode/tests/trimolecular.txt +9 -0
  52. firecode/tests.py +151 -0
  53. firecode/torsion_module.py +1035 -0
  54. firecode/utils.py +541 -0
  55. firecode-1.0.0.dist-info/LICENSE +165 -0
  56. firecode-1.0.0.dist-info/METADATA +321 -0
  57. firecode-1.0.0.dist-info/RECORD +59 -0
  58. firecode-1.0.0.dist-info/WHEEL +5 -0
  59. firecode-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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}'
@@ -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