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,879 @@
|
|
|
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 time
|
|
26
|
+
import warnings
|
|
27
|
+
from copy import deepcopy
|
|
28
|
+
|
|
29
|
+
import matplotlib.pyplot as plt
|
|
30
|
+
import numpy as np
|
|
31
|
+
from ase import Atoms
|
|
32
|
+
from ase.calculators.calculator import (CalculationFailed,
|
|
33
|
+
PropertyNotImplementedError)
|
|
34
|
+
from ase.calculators.gaussian import Gaussian
|
|
35
|
+
from ase.calculators.mopac import MOPAC
|
|
36
|
+
from ase.calculators.orca import ORCA
|
|
37
|
+
from ase.constraints import FixInternals
|
|
38
|
+
from ase.dyneb import DyNEB
|
|
39
|
+
from ase.optimize import BFGS, LBFGS
|
|
40
|
+
from ase.vibrations import Vibrations
|
|
41
|
+
from sella import Sella
|
|
42
|
+
|
|
43
|
+
from firecode.algebra import norm, norm_of
|
|
44
|
+
from firecode.graph_manipulations import findPaths, graphize, neighbors
|
|
45
|
+
from firecode.rmsd import get_alignment_matrix
|
|
46
|
+
from firecode.settings import COMMANDS, MEM_GB
|
|
47
|
+
from firecode.solvents import get_solvent_line
|
|
48
|
+
from firecode.utils import (HiddenPrints, align_structures, clean_directory,
|
|
49
|
+
get_double_bonds_indices, molecule_check,
|
|
50
|
+
scramble_check, time_to_string, write_xyz)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Spring:
|
|
54
|
+
'''
|
|
55
|
+
ASE Custom Constraint Class
|
|
56
|
+
Adds an harmonic force between a pair of atoms.
|
|
57
|
+
Spring constant is very high to achieve tight convergence,
|
|
58
|
+
but maximum force is dampened so as not to ruin structures.
|
|
59
|
+
'''
|
|
60
|
+
def __init__(self, i1, i2, d_eq, k=100):
|
|
61
|
+
self.i1, self.i2 = i1, i2
|
|
62
|
+
self.d_eq = d_eq
|
|
63
|
+
self.k = k
|
|
64
|
+
|
|
65
|
+
def adjust_positions(self, atoms, newpositions):
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
def adjust_forces(self, atoms, forces):
|
|
69
|
+
|
|
70
|
+
direction = atoms.positions[self.i2] - atoms.positions[self.i1]
|
|
71
|
+
# vector connecting atom1 to atom2
|
|
72
|
+
|
|
73
|
+
spring_force = self.k * (norm_of(direction) - self.d_eq)
|
|
74
|
+
# absolute spring force (float). Positive if spring is overstretched.
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
spring_force = np.clip(spring_force, -50, 50)
|
|
78
|
+
# force is clipped at 50 eV/A ()
|
|
79
|
+
|
|
80
|
+
forces[self.i1] += (norm(direction) * spring_force)
|
|
81
|
+
forces[self.i2] -= (norm(direction) * spring_force)
|
|
82
|
+
# applying harmonic force to each atom, directed toward the other one
|
|
83
|
+
|
|
84
|
+
def __repr__(self):
|
|
85
|
+
return f'Spring - ids:{self.i1}/{self.i2} - d_eq:{self.d_eq}, k:{self.k}'
|
|
86
|
+
|
|
87
|
+
class HalfSpring:
|
|
88
|
+
'''
|
|
89
|
+
ASE Custom Constraint Class
|
|
90
|
+
Adds an harmonic force between a pair of atoms,
|
|
91
|
+
only if those two atoms are at least d_max
|
|
92
|
+
Angstroms apart.
|
|
93
|
+
'''
|
|
94
|
+
def __init__(self, i1, i2, d_max, k=1000):
|
|
95
|
+
self.i1, self.i2 = i1, i2
|
|
96
|
+
self.d_max = d_max
|
|
97
|
+
self.k = k
|
|
98
|
+
|
|
99
|
+
def adjust_positions(self, atoms, newpositions):
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
def adjust_forces(self, atoms, forces):
|
|
103
|
+
|
|
104
|
+
direction = atoms.positions[self.i2] - atoms.positions[self.i1]
|
|
105
|
+
# vector connecting atom1 to atom2
|
|
106
|
+
|
|
107
|
+
if norm_of(direction) > self.d_max:
|
|
108
|
+
|
|
109
|
+
spring_force = self.k * (norm_of(direction) - self.d_max)
|
|
110
|
+
# absolute spring force (float). Positive if spring is overstretched.
|
|
111
|
+
|
|
112
|
+
spring_force = np.clip(spring_force, -50, 50)
|
|
113
|
+
# force is clipped at 50 eV/A
|
|
114
|
+
|
|
115
|
+
forces[self.i1] += (norm(direction) * spring_force)
|
|
116
|
+
forces[self.i2] -= (norm(direction) * spring_force)
|
|
117
|
+
# applying harmonic force to each atom, directed toward the other one
|
|
118
|
+
|
|
119
|
+
def __repr__(self):
|
|
120
|
+
return f'Spring - ids:{self.i1}/{self.i2} - d_max:{self.d_max}, k:{self.k}'
|
|
121
|
+
|
|
122
|
+
def get_ase_calc(embedder):
|
|
123
|
+
'''
|
|
124
|
+
Attach the correct ASE calculator
|
|
125
|
+
to the ASE Atoms object.
|
|
126
|
+
embedder: either a firecode embedder object or
|
|
127
|
+
a 4-element strings tuple containing
|
|
128
|
+
(calculator, method, procs, solvent)
|
|
129
|
+
'''
|
|
130
|
+
if isinstance(embedder, tuple):
|
|
131
|
+
calculator, method, procs, solvent = embedder
|
|
132
|
+
|
|
133
|
+
else:
|
|
134
|
+
calculator = embedder.options.calculator
|
|
135
|
+
method = embedder.options.theory_level
|
|
136
|
+
procs = embedder.procs
|
|
137
|
+
solvent = embedder.options.solvent
|
|
138
|
+
|
|
139
|
+
if calculator == 'XTB':
|
|
140
|
+
try:
|
|
141
|
+
from xtb.ase.calculator import XTB
|
|
142
|
+
except ImportError:
|
|
143
|
+
raise Exception(('Cannot import xtb python bindings. Install them with:\n'
|
|
144
|
+
'>>> conda install -c conda-forge xtb-python\n'
|
|
145
|
+
'(See https://github.com/grimme-lab/xtb-python)'))
|
|
146
|
+
|
|
147
|
+
from firecode.solvents import (solvent_synonyms, xtb_solvents,
|
|
148
|
+
xtb_supported)
|
|
149
|
+
|
|
150
|
+
solvent = solvent_synonyms[solvent] if solvent in solvent_synonyms else solvent
|
|
151
|
+
solvent = 'none' if solvent is None else solvent
|
|
152
|
+
|
|
153
|
+
if solvent not in xtb_solvents:
|
|
154
|
+
raise Exception(f'Solvent \'{solvent}\' not supported by XTB. Supported solvents are:\n{xtb_supported}')
|
|
155
|
+
|
|
156
|
+
return XTB(method=method, solvent=solvent)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
command = COMMANDS[calculator]
|
|
160
|
+
|
|
161
|
+
if calculator == 'MOPAC':
|
|
162
|
+
|
|
163
|
+
if solvent is not None:
|
|
164
|
+
method = method + ' ' + get_solvent_line(solvent, calculator, method)
|
|
165
|
+
|
|
166
|
+
return MOPAC(label='temp',
|
|
167
|
+
command=f'{command} temp.mop > temp.cmdlog 2>&1',
|
|
168
|
+
method=method+' GEO-OK')
|
|
169
|
+
|
|
170
|
+
if calculator == 'ORCA':
|
|
171
|
+
|
|
172
|
+
orcablocks = ''
|
|
173
|
+
|
|
174
|
+
if procs > 1:
|
|
175
|
+
orcablocks += f'%pal nprocs {procs} end'
|
|
176
|
+
|
|
177
|
+
if solvent is not None:
|
|
178
|
+
orcablocks += get_solvent_line(solvent, calculator, method)
|
|
179
|
+
|
|
180
|
+
return ORCA(label='temp',
|
|
181
|
+
command=f'{command} temp.inp "--oversubscribe" > temp.out 2>&1',
|
|
182
|
+
orcasimpleinput=method,
|
|
183
|
+
orcablocks=orcablocks)
|
|
184
|
+
|
|
185
|
+
if calculator == 'GAUSSIAN':
|
|
186
|
+
|
|
187
|
+
if solvent is not None:
|
|
188
|
+
method = method + ' ' + get_solvent_line(solvent, calculator, method)
|
|
189
|
+
|
|
190
|
+
mem = str(MEM_GB)+'GB' if MEM_GB >= 1 else str(int(1000*MEM_GB))+'MB'
|
|
191
|
+
|
|
192
|
+
calc = Gaussian(label='temp',
|
|
193
|
+
command=f'{command} temp.com',
|
|
194
|
+
method=method,
|
|
195
|
+
nprocshared=procs,
|
|
196
|
+
mem=mem,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
if 'g09' in command:
|
|
200
|
+
|
|
201
|
+
from ase.io import read
|
|
202
|
+
def g09_read_results(self=calc):
|
|
203
|
+
output = read(self.label + '.out', format='gaussian-out')
|
|
204
|
+
self.calc = output.calc
|
|
205
|
+
self.results = output.calc.results
|
|
206
|
+
|
|
207
|
+
calc.read_results = g09_read_results
|
|
208
|
+
|
|
209
|
+
# Adapting for g09 outputting .out files instead of g16 .log files.
|
|
210
|
+
# This is a bad fix and the issue should be corrected in the ASE
|
|
211
|
+
# source code: merge request on GitHub pending to be written
|
|
212
|
+
|
|
213
|
+
return calc
|
|
214
|
+
|
|
215
|
+
# def ase_adjust_spacings(embedder, structure, atomnos, constrained_indices, title=0, traj=None):
|
|
216
|
+
# '''
|
|
217
|
+
# embedder: firecode embedder object
|
|
218
|
+
# structure: TS candidate coordinates to be adjusted
|
|
219
|
+
# atomnos: 1-d array with element numbering for the TS
|
|
220
|
+
# constrained_indices: (n,2)-shaped array of indices to be distance constrained
|
|
221
|
+
# mols_graphs: list of NetworkX graphs, ordered as the single molecules in the TS
|
|
222
|
+
# title: number to be used for referring to this structure in the embedder log
|
|
223
|
+
# traj: if set to a string, traj+'.traj' is used as a filename for the refinement trajectory.
|
|
224
|
+
# '''
|
|
225
|
+
# atoms = Atoms(atomnos, positions=structure)
|
|
226
|
+
|
|
227
|
+
# atoms.calc = get_ase_calc(embedder)
|
|
228
|
+
|
|
229
|
+
# springs = [Spring(indices[0], indices[1], dist) for indices, dist in embedder.target_distances.items()]
|
|
230
|
+
# # adding springs to adjust the pairings for which we have target distances
|
|
231
|
+
|
|
232
|
+
# # if there are no springs, it is faster (and equivalent) to just do a classical full opitimization
|
|
233
|
+
# if not springs:
|
|
234
|
+
# from firecode.optimization_methods import optimize
|
|
235
|
+
# return optimize(
|
|
236
|
+
# structure,
|
|
237
|
+
# atomnos,
|
|
238
|
+
# embedder.options.calculator,
|
|
239
|
+
# method=embedder.options.theory_level,
|
|
240
|
+
# mols_graphs=embedder.graphs if embedder.embed != 'monomolecular' else None,
|
|
241
|
+
# procs=embedder.procs,
|
|
242
|
+
# solvent=embedder.options.solvent,
|
|
243
|
+
# max_newbonds=embedder.options.max_newbonds,
|
|
244
|
+
# check=(embedder.embed != 'refine'),
|
|
245
|
+
|
|
246
|
+
# logfunction=lambda s: embedder.log(s, p=False),
|
|
247
|
+
# title=f'Candidate_{title}'
|
|
248
|
+
# )
|
|
249
|
+
|
|
250
|
+
# nci_indices = [indices for letter, indices in embedder.pairings_table.items() if letter.islower()]
|
|
251
|
+
# halfsprings = [HalfSpring(i1, i2, 2.5) for i1, i2 in nci_indices]
|
|
252
|
+
# # HalfSprings get atoms involved in NCIs together if they are more than 2.5A apart,
|
|
253
|
+
# # but lets them achieve their natural equilibrium distance when closer
|
|
254
|
+
|
|
255
|
+
# psc = PreventScramblingConstraint(graphize(structure, atomnos),
|
|
256
|
+
# atoms,
|
|
257
|
+
# double_bond_protection=embedder.options.double_bond_protection,
|
|
258
|
+
# fix_angles=embedder.options.fix_angles_in_deformation)
|
|
259
|
+
|
|
260
|
+
# atoms.set_constraint(springs + halfsprings + [psc])
|
|
261
|
+
|
|
262
|
+
# t_start_opt = time.perf_counter()
|
|
263
|
+
# try:
|
|
264
|
+
# with LBFGS(atoms, maxstep=0.2, logfile=None, trajectory=traj) as opt:
|
|
265
|
+
|
|
266
|
+
# opt.run(fmax=0.05, steps=500)
|
|
267
|
+
# # initial coarse refinement with
|
|
268
|
+
# # Springs, Half Springs and PSC
|
|
269
|
+
|
|
270
|
+
# for spring in springs:
|
|
271
|
+
# spring.tighten()
|
|
272
|
+
# atoms.set_constraint(springs)
|
|
273
|
+
# # Tightening Springs to improve
|
|
274
|
+
# # spacings accuracy, removing PSC
|
|
275
|
+
# # spacings accuracy, removing PSC
|
|
276
|
+
|
|
277
|
+
# opt.run(fmax=0.05, steps=200)
|
|
278
|
+
# # final accurate refinement
|
|
279
|
+
|
|
280
|
+
# iterations = opt.nsteps
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
# new_structure = atoms.get_positions()
|
|
284
|
+
|
|
285
|
+
# success = scramble_check(new_structure, atomnos, constrained_indices, embedder.graphs)
|
|
286
|
+
# if iterations == 200:
|
|
287
|
+
# exit_str = 'MAX ITER'
|
|
288
|
+
# elif success:
|
|
289
|
+
# exit_str = 'REFINED'
|
|
290
|
+
# else:
|
|
291
|
+
# exit_str = 'SCRAMBLED'
|
|
292
|
+
|
|
293
|
+
# if iterations == 200:
|
|
294
|
+
# exit_str = 'MAX ITER'
|
|
295
|
+
# elif success:
|
|
296
|
+
# exit_str = 'REFINED'
|
|
297
|
+
# else:
|
|
298
|
+
# exit_str = 'SCRAMBLED'
|
|
299
|
+
|
|
300
|
+
# except PropertyNotImplementedError:
|
|
301
|
+
# exit_str = 'CRASHED'
|
|
302
|
+
|
|
303
|
+
# embedder.log(f' - {title} {exit_str} ({iterations} iterations, {time_to_string(time.perf_counter()-t_start_opt)})', p=False)
|
|
304
|
+
# embedder.log(f' - {title} {exit_str} ({iterations} iterations, {time_to_string(time.perf_counter()-t_start_opt)})', p=False)
|
|
305
|
+
|
|
306
|
+
# if exit_str == 'CRASHED':
|
|
307
|
+
# return None, None, False
|
|
308
|
+
|
|
309
|
+
# energy = atoms.get_total_energy() * 23.06054194532933 #eV to kcal/mol
|
|
310
|
+
|
|
311
|
+
# return new_structure, energy, success
|
|
312
|
+
|
|
313
|
+
def ase_saddle(embedder, coords, atomnos, constrained_indices=None, mols_graphs=None, title='temp', logfile=None, traj=None, freq=False, maxiterations=200):
|
|
314
|
+
'''
|
|
315
|
+
Runs a first order saddle optimization through the ASE package
|
|
316
|
+
'''
|
|
317
|
+
atoms = Atoms(atomnos, positions=coords)
|
|
318
|
+
|
|
319
|
+
atoms.calc = get_ase_calc(embedder)
|
|
320
|
+
|
|
321
|
+
t_start = time.perf_counter()
|
|
322
|
+
with HiddenPrints():
|
|
323
|
+
with Sella(atoms,
|
|
324
|
+
logfile=logfile,
|
|
325
|
+
order=1,
|
|
326
|
+
trajectory=traj) as opt:
|
|
327
|
+
|
|
328
|
+
opt.run(fmax=0.05, steps=maxiterations)
|
|
329
|
+
iterations = opt.nsteps
|
|
330
|
+
|
|
331
|
+
if logfile is not None:
|
|
332
|
+
t_end_berny = time.perf_counter()
|
|
333
|
+
elapsed = t_end_berny - t_start
|
|
334
|
+
exit_str = 'converged' if iterations < maxiterations else 'stopped'
|
|
335
|
+
logfile.write(f'{title} - {exit_str} in {iterations} steps ({time_to_string(elapsed)})\n')
|
|
336
|
+
|
|
337
|
+
new_structure = atoms.get_positions()
|
|
338
|
+
energy = atoms.get_total_energy() * 23.06054194532933 #eV to kcal/mol
|
|
339
|
+
|
|
340
|
+
if mols_graphs is not None:
|
|
341
|
+
success = scramble_check(new_structure, atomnos, constrained_indices, mols_graphs, max_newbonds=embedder.options.max_newbonds)
|
|
342
|
+
else:
|
|
343
|
+
success = molecule_check(coords, new_structure, atomnos, max_newbonds=embedder.options.max_newbonds)
|
|
344
|
+
|
|
345
|
+
return new_structure, energy, success
|
|
346
|
+
|
|
347
|
+
def ase_vib(embedder, coords, atomnos, logfunction=None, title='temp'):
|
|
348
|
+
'''
|
|
349
|
+
Calculate frequencies through ASE - returns frequencies and number of negatives (not in use)
|
|
350
|
+
'''
|
|
351
|
+
atoms = Atoms(atomnos, positions=coords)
|
|
352
|
+
atoms.calc = get_ase_calc(embedder)
|
|
353
|
+
vib = Vibrations(atoms, name=title)
|
|
354
|
+
|
|
355
|
+
if os.path.isdir(title):
|
|
356
|
+
os.chdir(title)
|
|
357
|
+
for f in os.listdir():
|
|
358
|
+
os.remove(f)
|
|
359
|
+
os.chdir(os.path.dirname(os.getcwd()))
|
|
360
|
+
else:
|
|
361
|
+
os.mkdir(title)
|
|
362
|
+
|
|
363
|
+
os.chdir(title)
|
|
364
|
+
|
|
365
|
+
t_start = time.perf_counter()
|
|
366
|
+
|
|
367
|
+
with HiddenPrints():
|
|
368
|
+
vib.run()
|
|
369
|
+
|
|
370
|
+
# freqs = vib.get_frequencies()
|
|
371
|
+
freqs = vib.get_energies()* 8065.544 # from eV to cm-1
|
|
372
|
+
|
|
373
|
+
if logfunction is not None:
|
|
374
|
+
elapsed = time.perf_counter() - t_start
|
|
375
|
+
logfunction(f'{title} - frequency calculation completed ({time_to_string(elapsed)})')
|
|
376
|
+
|
|
377
|
+
os.chdir(os.path.dirname(os.getcwd()))
|
|
378
|
+
|
|
379
|
+
return freqs, np.count_nonzero(freqs.imag > 1e-3)
|
|
380
|
+
|
|
381
|
+
def ase_neb(embedder, reagents, products, atomnos, ts_guess=None, n_images=6, mep_override=None, title='temp', optimizer=LBFGS, logfunction=None, write_plot=False, verbose_print=False):
|
|
382
|
+
'''
|
|
383
|
+
embedder: firecode embedder object
|
|
384
|
+
reagents: coordinates for the atom arrangement to be used as reagents
|
|
385
|
+
products: coordinates for the atom arrangement to be used as products
|
|
386
|
+
atomnos: 1-d array of atomic numbers
|
|
387
|
+
n_images: number of optimized images connecting reag/prods
|
|
388
|
+
title: name used to write the final MEP as a .xyz file
|
|
389
|
+
optimizer: ASE optimizer to be used in
|
|
390
|
+
logfile: filename to dump the optimization data to. If None, no file is written.
|
|
391
|
+
|
|
392
|
+
return: 3- element tuple with coodinates of highest point along the MEP, its
|
|
393
|
+
energy in kcal/mol and a boolean value indicating success.
|
|
394
|
+
'''
|
|
395
|
+
reagents, products = align_structures(np.array([reagents, products]))
|
|
396
|
+
first = Atoms(atomnos, positions=reagents)
|
|
397
|
+
last = Atoms(atomnos, positions=products)
|
|
398
|
+
|
|
399
|
+
if mep_override is not None:
|
|
400
|
+
|
|
401
|
+
images = [Atoms(atomnos, positions=coords) for coords in mep_override]
|
|
402
|
+
neb = DyNEB(images, fmax=0.05, climb=False, method='eb', scale_fmax=1, allow_shared_calculator=True)
|
|
403
|
+
|
|
404
|
+
elif ts_guess is None:
|
|
405
|
+
images = [first]
|
|
406
|
+
images += [first.copy() for _ in range(n_images)]
|
|
407
|
+
images += [last]
|
|
408
|
+
|
|
409
|
+
neb = DyNEB(images, fmax=0.05, climb=False, method='eb', scale_fmax=1, allow_shared_calculator=True)
|
|
410
|
+
neb.interpolate(method='idpp')
|
|
411
|
+
|
|
412
|
+
else:
|
|
413
|
+
ts_guess = Atoms(atomnos, positions=ts_guess)
|
|
414
|
+
|
|
415
|
+
images_1 = [first] + [first.copy() for _ in range(round((n_images-3)/2))] + [ts_guess]
|
|
416
|
+
interp_1 = DyNEB(images_1)
|
|
417
|
+
interp_1.interpolate(method='idpp')
|
|
418
|
+
|
|
419
|
+
images_2 = [ts_guess] + [last.copy() for _ in range(n_images-len(interp_1.images)-1)] + [last]
|
|
420
|
+
interp_2 = DyNEB(images_2)
|
|
421
|
+
interp_2.interpolate(method='idpp')
|
|
422
|
+
|
|
423
|
+
images = interp_1.images + interp_2.images[1:]
|
|
424
|
+
|
|
425
|
+
neb = DyNEB(images, fmax=0.05, climb=False, method='eb', scale_fmax=1, allow_shared_calculator=True)
|
|
426
|
+
|
|
427
|
+
if mep_override is None:
|
|
428
|
+
ase_dump(f'{title}_MEP_guess.xyz', images, atomnos)
|
|
429
|
+
|
|
430
|
+
if verbose_print and logfunction is not None and mep_override is None:
|
|
431
|
+
logfunction(f'\n\n--> Saved interpolated MEP guess to {title}_MEP_guess.xyz\n')
|
|
432
|
+
|
|
433
|
+
# Set calculators for all images
|
|
434
|
+
for _, image in enumerate(images):
|
|
435
|
+
image.calc = get_ase_calc(embedder)
|
|
436
|
+
|
|
437
|
+
t_start = time.perf_counter()
|
|
438
|
+
|
|
439
|
+
# Set the optimizer and optimize
|
|
440
|
+
try:
|
|
441
|
+
|
|
442
|
+
with warnings.catch_warnings():
|
|
443
|
+
warnings.simplefilter('ignore')
|
|
444
|
+
# ignore runtime warnings from the NEB module:
|
|
445
|
+
# if something went wrong, we will deal with it later
|
|
446
|
+
|
|
447
|
+
with optimizer(neb, maxstep=0.1, logfile=None if not verbose_print else 'neb_opt.log') as opt:
|
|
448
|
+
|
|
449
|
+
if verbose_print and logfunction is not None:
|
|
450
|
+
logfunction(f'\n--> Running NEB-CI through ASE ({embedder.options.theory_level} via {embedder.options.calculator})')
|
|
451
|
+
|
|
452
|
+
opt.run(fmax=0.05, steps=20)
|
|
453
|
+
while neb.get_residual() > 0.1:
|
|
454
|
+
opt.run(fmax=0.05, steps=10+opt.nsteps)
|
|
455
|
+
# some free relaxation before starting to climb
|
|
456
|
+
if opt.nsteps > 500 or opt.converged:
|
|
457
|
+
break
|
|
458
|
+
|
|
459
|
+
if verbose_print and logfunction is not None:
|
|
460
|
+
logfunction('--> fmax below 0.1: Activated Climbing Image and smaller maxstep')
|
|
461
|
+
|
|
462
|
+
ase_dump(f'{title}_MEP_start_of_CI.xyz', neb.images, atomnos)
|
|
463
|
+
|
|
464
|
+
optimizer.maxstep = 0.01
|
|
465
|
+
neb.climb = True
|
|
466
|
+
|
|
467
|
+
opt.run(fmax=0.05, steps=250+opt.nsteps)
|
|
468
|
+
|
|
469
|
+
iterations = opt.nsteps
|
|
470
|
+
exit_status = 'CONVERGED' if iterations < 279 else 'MAX ITER'
|
|
471
|
+
|
|
472
|
+
# success = True if exit_status == 'CONVERGED' else False
|
|
473
|
+
|
|
474
|
+
except (PropertyNotImplementedError, CalculationFailed):
|
|
475
|
+
if logfunction is not None:
|
|
476
|
+
logfunction(f' - NEB for {title} CRASHED ({time_to_string(time.perf_counter()-t_start)})\n')
|
|
477
|
+
try:
|
|
478
|
+
ase_dump(f'{title}_MEP_crashed.xyz', neb.images, atomnos)
|
|
479
|
+
except Exception():
|
|
480
|
+
pass
|
|
481
|
+
return None, None, None, False
|
|
482
|
+
|
|
483
|
+
except KeyboardInterrupt:
|
|
484
|
+
exit_status = 'ABORTED BY USER'
|
|
485
|
+
|
|
486
|
+
if logfunction is not None:
|
|
487
|
+
logfunction(f' - NEB for {title} {exit_status} ({time_to_string(time.perf_counter()-t_start)})\n')
|
|
488
|
+
|
|
489
|
+
energies = [image.get_total_energy() * 23.06054194532933 for image in images] # eV to kcal/mol
|
|
490
|
+
|
|
491
|
+
ts_id = energies.index(max(energies))
|
|
492
|
+
# print(f'TS structure is number {ts_id}, energy is {max(energies)}')
|
|
493
|
+
|
|
494
|
+
if mep_override is None:
|
|
495
|
+
os.remove(f'{title}_MEP_guess.xyz')
|
|
496
|
+
ase_dump(f'{title}_MEP.xyz', images, atomnos, energies)
|
|
497
|
+
# Save the converged MEP (minimum energy path) to an .xyz file
|
|
498
|
+
|
|
499
|
+
if write_plot:
|
|
500
|
+
|
|
501
|
+
plt.figure()
|
|
502
|
+
plt.plot(
|
|
503
|
+
range(1,len(images)+1),
|
|
504
|
+
np.array(energies)-min(energies),
|
|
505
|
+
color='tab:blue',
|
|
506
|
+
label='Image energies',
|
|
507
|
+
linewidth=3,
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
plt.plot(
|
|
511
|
+
[ts_id+1],
|
|
512
|
+
[energies[ts_id]-min(energies)],
|
|
513
|
+
color='gold',
|
|
514
|
+
label='TS guess',
|
|
515
|
+
marker='o',
|
|
516
|
+
markersize=3,
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
plt.legend()
|
|
520
|
+
plt.title(title)
|
|
521
|
+
plt.xlabel('Image number')
|
|
522
|
+
plt.ylabel('Rel. E. (kcal/mol)')
|
|
523
|
+
plt.savefig(f'{title.replace(" ", "_")}_plt.svg')
|
|
524
|
+
|
|
525
|
+
return images[ts_id].get_positions(), energies[ts_id], energies, exit_status
|
|
526
|
+
|
|
527
|
+
class OrbitalSpring:
|
|
528
|
+
'''
|
|
529
|
+
ASE Custom Constraint Class
|
|
530
|
+
Adds a series of forces based on a pair of orbitals, that is
|
|
531
|
+
virtual points "bonded" to a given atom.
|
|
532
|
+
|
|
533
|
+
:params i1, i2: indices of reactive atoms
|
|
534
|
+
:params orb1, orb2: 3D coordinates of orbitals
|
|
535
|
+
:params neighbors_of_1, neighbors_of_2: lists of indices for atoms bonded to i1/i2
|
|
536
|
+
:params d_eq: equilibrium target distance between orbital centers
|
|
537
|
+
'''
|
|
538
|
+
def __init__(self, i1, i2, orb1, orb2, neighbors_of_1, neighbors_of_2, d_eq, k=1000):
|
|
539
|
+
self.i1, self.i2 = i1, i2
|
|
540
|
+
self.orb1, self.orb2 = orb1, orb2
|
|
541
|
+
self.neighbors_of_1, self.neighbors_of_2 = neighbors_of_1, neighbors_of_2
|
|
542
|
+
self.d_eq = d_eq
|
|
543
|
+
self.k = k
|
|
544
|
+
|
|
545
|
+
def adjust_positions(self, atoms, newpositions):
|
|
546
|
+
pass
|
|
547
|
+
|
|
548
|
+
def adjust_forces(self, atoms, forces):
|
|
549
|
+
|
|
550
|
+
# First, assess if we have to move atoms 1 and 2 at all
|
|
551
|
+
|
|
552
|
+
sum_of_distances = (norm_of(atoms.positions[self.i1] - self.orb1) +
|
|
553
|
+
norm_of(atoms.positions[self.i2] - self.orb2) + self.d_eq)
|
|
554
|
+
|
|
555
|
+
reactive_atoms_distance = norm_of(atoms.positions[self.i1] - atoms.positions[self.i2])
|
|
556
|
+
|
|
557
|
+
orb_direction = self.orb2 - self.orb1
|
|
558
|
+
# vector connecting orb1 to orb2
|
|
559
|
+
|
|
560
|
+
spring_force = self.k * (norm_of(orb_direction) - self.d_eq)
|
|
561
|
+
# absolute spring force (float). Positive if spring is overstretched.
|
|
562
|
+
|
|
563
|
+
# spring_force = np.clip(spring_force, -50, 50)
|
|
564
|
+
# # force is clipped at 5 eV/A
|
|
565
|
+
|
|
566
|
+
force_direction1 = np.sign(spring_force) * norm(np.mean((norm(+orb_direction),
|
|
567
|
+
norm(self.orb1-atoms.positions[self.i1])), axis=0))
|
|
568
|
+
|
|
569
|
+
force_direction2 = np.sign(spring_force) * norm(np.mean((norm(-orb_direction),
|
|
570
|
+
norm(self.orb2-atoms.positions[self.i2])), axis=0))
|
|
571
|
+
|
|
572
|
+
# versors specifying the direction at which forces act, that is on the
|
|
573
|
+
# bisector of the angle between vector connecting atom to orbital and
|
|
574
|
+
# vector connecting the two orbitals
|
|
575
|
+
|
|
576
|
+
if np.abs(sum_of_distances - reactive_atoms_distance) > 0.2:
|
|
577
|
+
|
|
578
|
+
forces[self.i1] += (force_direction1 * spring_force)
|
|
579
|
+
forces[self.i2] += (force_direction2 * spring_force)
|
|
580
|
+
# applying harmonic force to each atom, directed toward the other one
|
|
581
|
+
|
|
582
|
+
# Now applying to neighbors the force derived by torque, scaled to match the spring_force,
|
|
583
|
+
# but only if atomic orbitals are more than two Angstroms apart. This improves convergence.
|
|
584
|
+
|
|
585
|
+
if norm_of(orb_direction) > 2:
|
|
586
|
+
torque1 = np.cross(self.orb1 - atoms.positions[self.i1], force_direction1)
|
|
587
|
+
for i in self.neighbors_of_1:
|
|
588
|
+
forces[i] += norm(np.cross(torque1, atoms.positions[i] - atoms.positions[self.i1])) * spring_force
|
|
589
|
+
|
|
590
|
+
torque2 = np.cross(self.orb2 - atoms.positions[self.i2], force_direction2)
|
|
591
|
+
for i in self.neighbors_of_2:
|
|
592
|
+
forces[i] += norm(np.cross(torque2, atoms.positions[i] - atoms.positions[self.i2])) * spring_force
|
|
593
|
+
|
|
594
|
+
def PreventScramblingConstraint(graph, atoms, double_bond_protection=False, fix_angles=False):
|
|
595
|
+
'''
|
|
596
|
+
graph: NetworkX graph of the molecule
|
|
597
|
+
atoms: ASE atoms object
|
|
598
|
+
|
|
599
|
+
return: FixInternals constraint to apply to ASE calculations
|
|
600
|
+
'''
|
|
601
|
+
angles_deg = None
|
|
602
|
+
if fix_angles:
|
|
603
|
+
allpaths = []
|
|
604
|
+
|
|
605
|
+
for node in graph:
|
|
606
|
+
allpaths.extend(findPaths(graph, node, 2))
|
|
607
|
+
|
|
608
|
+
allpaths = {tuple(sorted(path)) for path in allpaths}
|
|
609
|
+
|
|
610
|
+
angles_deg = []
|
|
611
|
+
for path in allpaths:
|
|
612
|
+
angles_deg.append([atoms.get_angle(*path), list(path)])
|
|
613
|
+
|
|
614
|
+
bonds = []
|
|
615
|
+
for bond in [[a, b] for a, b in graph.edges if a != b]:
|
|
616
|
+
bonds.append([atoms.get_distance(*bond), bond])
|
|
617
|
+
|
|
618
|
+
dihedrals_deg = None
|
|
619
|
+
if double_bond_protection:
|
|
620
|
+
double_bonds = get_double_bonds_indices(atoms.positions, atoms.get_atomic_numbers())
|
|
621
|
+
if double_bonds != []:
|
|
622
|
+
dihedrals_deg = []
|
|
623
|
+
for a, b in double_bonds:
|
|
624
|
+
n_a = neighbors(graph, a)
|
|
625
|
+
n_a.remove(b)
|
|
626
|
+
|
|
627
|
+
n_b = neighbors(graph, b)
|
|
628
|
+
n_b.remove(a)
|
|
629
|
+
|
|
630
|
+
d = [n_a[0], a, b, n_b[0]]
|
|
631
|
+
dihedrals_deg.append([atoms.get_dihedral(*d), d])
|
|
632
|
+
|
|
633
|
+
return FixInternals(dihedrals_deg=dihedrals_deg, angles_deg=angles_deg, bonds=bonds, epsilon=1)
|
|
634
|
+
|
|
635
|
+
def ase_popt(embedder, coords, atomnos, constrained_indices=None,
|
|
636
|
+
steps=500, targets=None, safe=False, safe_mask=None,
|
|
637
|
+
traj=None, logfunction=None, title='temp'):
|
|
638
|
+
'''
|
|
639
|
+
embedder: firecode embedder object
|
|
640
|
+
coords:
|
|
641
|
+
atomnos:
|
|
642
|
+
constrained_indices:
|
|
643
|
+
safe: if True, adds a potential that prevents atoms from scrambling
|
|
644
|
+
safe_mask: bool array, with False for atoms to be excluded when calculating bonds to preserve
|
|
645
|
+
traj: if set to a string, traj is used as a filename for the bending trajectory.
|
|
646
|
+
not only the atoms will be printed, but also all the orbitals and the active pivot.
|
|
647
|
+
'''
|
|
648
|
+
atoms = Atoms(atomnos, positions=coords)
|
|
649
|
+
atoms.calc = get_ase_calc(embedder)
|
|
650
|
+
constraints = []
|
|
651
|
+
|
|
652
|
+
if constrained_indices is not None:
|
|
653
|
+
for i, c in enumerate(constrained_indices):
|
|
654
|
+
i1, i2 = c
|
|
655
|
+
tgt_dist = norm_of(coords[i1]-coords[i2]) if targets is None else targets[i]
|
|
656
|
+
constraints.append(Spring(i1, i2, tgt_dist))
|
|
657
|
+
|
|
658
|
+
if safe:
|
|
659
|
+
constraints.append(PreventScramblingConstraint(graphize(coords, atomnos, safe_mask),
|
|
660
|
+
atoms,
|
|
661
|
+
double_bond_protection=embedder.options.double_bond_protection,
|
|
662
|
+
fix_angles=embedder.options.fix_angles_in_deformation))
|
|
663
|
+
|
|
664
|
+
atoms.set_constraint(constraints)
|
|
665
|
+
|
|
666
|
+
t_start_opt = time.perf_counter()
|
|
667
|
+
with LBFGS(atoms, maxstep=0.1, logfile=None, trajectory=traj) as opt:
|
|
668
|
+
opt.run(fmax=0.05, steps=steps)
|
|
669
|
+
iterations = opt.nsteps
|
|
670
|
+
|
|
671
|
+
new_structure = atoms.get_positions()
|
|
672
|
+
success = (iterations < 499)
|
|
673
|
+
|
|
674
|
+
if logfunction is not None:
|
|
675
|
+
exit_str = 'REFINED' if success else 'MAX ITER'
|
|
676
|
+
logfunction(f' - {title} {exit_str} ({iterations} iterations, {time_to_string(time.perf_counter()-t_start_opt)})')
|
|
677
|
+
|
|
678
|
+
energy = atoms.get_total_energy() * 23.06054194532933 #eV to kcal/mol
|
|
679
|
+
|
|
680
|
+
return new_structure, energy, success
|
|
681
|
+
|
|
682
|
+
def ase_bend(embedder, original_mol, conf, pivot, threshold, title='temp', traj=None, check=True):
|
|
683
|
+
'''
|
|
684
|
+
embedder: firecode embedder object
|
|
685
|
+
original_mol: Hypermolecule object to be bent
|
|
686
|
+
conf: index of conformation in original_mol to be used
|
|
687
|
+
pivot: pivot connecting two Hypermolecule orbitals to be approached/distanced
|
|
688
|
+
threshold: target distance for the specified pivot, in Angstroms
|
|
689
|
+
title: name to be used for referring to this structure in the embedder log
|
|
690
|
+
traj: if set to a string, traj+\'.traj\' is used as a filename for the bending trajectory.
|
|
691
|
+
not only the atoms will be printed, but also all the orbitals and the active pivot.
|
|
692
|
+
check: if True, after bending checks that the bent structure did not scramble.
|
|
693
|
+
If it did, returns the initial molecule.
|
|
694
|
+
'''
|
|
695
|
+
|
|
696
|
+
identifier = np.sum(original_mol.atomcoords[conf])
|
|
697
|
+
|
|
698
|
+
if hasattr(embedder, "ase_bent_mols_dict"):
|
|
699
|
+
cached = embedder.ase_bent_mols_dict.get((identifier, tuple(sorted(pivot.index)), round(threshold, 3)))
|
|
700
|
+
if cached is not None:
|
|
701
|
+
return cached
|
|
702
|
+
|
|
703
|
+
if traj is not None:
|
|
704
|
+
|
|
705
|
+
from ase.io.trajectory import Trajectory
|
|
706
|
+
|
|
707
|
+
def orbitalized(atoms, orbitals, pivot=None):
|
|
708
|
+
positions = np.concatenate((atoms.positions, orbitals))
|
|
709
|
+
|
|
710
|
+
if pivot is not None:
|
|
711
|
+
positions = np.concatenate((positions, [pivot.start], [pivot.end]))
|
|
712
|
+
|
|
713
|
+
symbols = list(atoms.numbers) + [0 for _ in orbitals]
|
|
714
|
+
|
|
715
|
+
if pivot is not None:
|
|
716
|
+
symbols += [9 for _ in range(2)]
|
|
717
|
+
# Fluorine (9) represents active orbitals
|
|
718
|
+
|
|
719
|
+
new_atoms = Atoms(symbols, positions=positions)
|
|
720
|
+
return new_atoms
|
|
721
|
+
|
|
722
|
+
try:
|
|
723
|
+
os.remove(traj)
|
|
724
|
+
except FileNotFoundError:
|
|
725
|
+
pass
|
|
726
|
+
|
|
727
|
+
i1, i2 = original_mol.reactive_indices
|
|
728
|
+
|
|
729
|
+
neighbors_of_1 = neighbors(original_mol.graph, i1)
|
|
730
|
+
neighbors_of_2 = neighbors(original_mol.graph, i2)
|
|
731
|
+
|
|
732
|
+
mol = deepcopy(original_mol)
|
|
733
|
+
final_mol = deepcopy(original_mol)
|
|
734
|
+
|
|
735
|
+
for p in mol.pivots[conf]:
|
|
736
|
+
if p.index == pivot.index:
|
|
737
|
+
active_pivot = p
|
|
738
|
+
break
|
|
739
|
+
|
|
740
|
+
dist = norm_of(active_pivot.pivot)
|
|
741
|
+
|
|
742
|
+
atoms = Atoms(mol.atomnos, positions=mol.atomcoords[conf])
|
|
743
|
+
|
|
744
|
+
atoms.calc = get_ase_calc(embedder)
|
|
745
|
+
|
|
746
|
+
if traj is not None:
|
|
747
|
+
traj_obj = Trajectory(traj + f'_conf{conf}.traj',
|
|
748
|
+
mode='a',
|
|
749
|
+
atoms=orbitalized(atoms,
|
|
750
|
+
np.vstack([atom.center for atom in mol.reactive_atoms_classes_dict[0].values()]),
|
|
751
|
+
active_pivot))
|
|
752
|
+
traj_obj.write()
|
|
753
|
+
|
|
754
|
+
unproductive_iterations = 0
|
|
755
|
+
break_reason = 'MAX ITER'
|
|
756
|
+
t_start = time.perf_counter()
|
|
757
|
+
|
|
758
|
+
for iteration in range(500):
|
|
759
|
+
|
|
760
|
+
atoms.positions = mol.atomcoords[0]
|
|
761
|
+
|
|
762
|
+
orb_memo = {index:norm_of(atom.center[0]-atom.coord) for index, atom in mol.reactive_atoms_classes_dict[0].items()}
|
|
763
|
+
|
|
764
|
+
orb1, orb2 = active_pivot.start, active_pivot.end
|
|
765
|
+
|
|
766
|
+
c1 = OrbitalSpring(i1, i2, orb1, orb2, neighbors_of_1, neighbors_of_2, d_eq=threshold)
|
|
767
|
+
|
|
768
|
+
c2 = PreventScramblingConstraint(mol.graph,
|
|
769
|
+
atoms,
|
|
770
|
+
double_bond_protection=embedder.options.double_bond_protection,
|
|
771
|
+
fix_angles=embedder.options.fix_angles_in_deformation)
|
|
772
|
+
|
|
773
|
+
atoms.set_constraint([
|
|
774
|
+
c1,
|
|
775
|
+
c2,
|
|
776
|
+
])
|
|
777
|
+
|
|
778
|
+
opt = BFGS(atoms, maxstep=0.2, logfile=None, trajectory=None)
|
|
779
|
+
|
|
780
|
+
try:
|
|
781
|
+
opt.run(fmax=0.5, steps=1)
|
|
782
|
+
except ValueError:
|
|
783
|
+
# Shake did not converge
|
|
784
|
+
break_reason = 'CRASHED'
|
|
785
|
+
break
|
|
786
|
+
|
|
787
|
+
if traj is not None:
|
|
788
|
+
traj_obj.atoms = orbitalized(atoms, np.vstack([atom.center for atom in mol.reactive_atoms_classes_dict[0].values()]))
|
|
789
|
+
traj_obj.write()
|
|
790
|
+
|
|
791
|
+
# check if we are stuck
|
|
792
|
+
if np.max(np.abs(np.linalg.norm(atoms.get_positions() - mol.atomcoords[0], axis=1))) < 0.01:
|
|
793
|
+
unproductive_iterations += 1
|
|
794
|
+
|
|
795
|
+
if unproductive_iterations == 10:
|
|
796
|
+
break_reason = 'STUCK'
|
|
797
|
+
break
|
|
798
|
+
|
|
799
|
+
else:
|
|
800
|
+
unproductive_iterations = 0
|
|
801
|
+
|
|
802
|
+
mol.atomcoords[0] = atoms.get_positions()
|
|
803
|
+
|
|
804
|
+
# Update orbitals and get temp pivots
|
|
805
|
+
for index, atom in mol.reactive_atoms_classes_dict[0].items():
|
|
806
|
+
atom.init(mol, index, update=True, orb_dim=orb_memo[index])
|
|
807
|
+
# orbitals positions are calculated based on the conformer we are working on
|
|
808
|
+
|
|
809
|
+
temp_pivots = embedder._get_pivots(mol)[0]
|
|
810
|
+
|
|
811
|
+
for p in temp_pivots:
|
|
812
|
+
if p.index == pivot.index:
|
|
813
|
+
active_pivot = p
|
|
814
|
+
break
|
|
815
|
+
# print(active_pivot)
|
|
816
|
+
|
|
817
|
+
dist = norm_of(active_pivot.pivot)
|
|
818
|
+
# print(f'{iteration}. {mol.filename} conf {conf}: pivot is {round(dist, 3)} (target {round(threshold, 3)})')
|
|
819
|
+
|
|
820
|
+
if dist - threshold < 0.1:
|
|
821
|
+
break_reason = 'CONVERGED'
|
|
822
|
+
break
|
|
823
|
+
# else:
|
|
824
|
+
# print('delta is ', round(dist - threshold, 3))
|
|
825
|
+
|
|
826
|
+
embedder.log(f' {title} - conformer {conf} - {break_reason}{" "*(9-len(break_reason))} ({iteration+1}{" "*(3-len(str(iteration+1)))} iterations, {time_to_string(time.perf_counter()-t_start)})', p=False)
|
|
827
|
+
|
|
828
|
+
if check:
|
|
829
|
+
if not molecule_check(original_mol.atomcoords[conf], mol.atomcoords[0], mol.atomnos, max_newbonds=1):
|
|
830
|
+
mol.atomcoords[0] = original_mol.atomcoords[conf]
|
|
831
|
+
# keep the bent structures only if no scrambling occurred between atoms
|
|
832
|
+
|
|
833
|
+
final_mol.atomcoords[conf] = mol.atomcoords[0]
|
|
834
|
+
|
|
835
|
+
# Now align the ensembles on the new reactive atoms positions
|
|
836
|
+
|
|
837
|
+
reference, *targets = final_mol.atomcoords
|
|
838
|
+
reference = np.array(reference)
|
|
839
|
+
targets = np.array(targets)
|
|
840
|
+
|
|
841
|
+
r = reference - np.mean(reference[final_mol.reactive_indices], axis=0)
|
|
842
|
+
ts = np.array([t - np.mean(t[final_mol.reactive_indices], axis=0) for t in targets])
|
|
843
|
+
|
|
844
|
+
output = []
|
|
845
|
+
output.append(r)
|
|
846
|
+
for target in ts:
|
|
847
|
+
matrix = get_alignment_matrix(r, target)
|
|
848
|
+
output.append([matrix @ vector for vector in target])
|
|
849
|
+
|
|
850
|
+
final_mol.atomcoords = np.array(output)
|
|
851
|
+
|
|
852
|
+
# Update orbitals and pivots
|
|
853
|
+
for conf_, _ in enumerate(final_mol.atomcoords):
|
|
854
|
+
for index, atom in final_mol.reactive_atoms_classes_dict[conf_].items():
|
|
855
|
+
atom.init(final_mol, index, update=True, orb_dim=orb_memo[index])
|
|
856
|
+
|
|
857
|
+
embedder._set_pivots(final_mol)
|
|
858
|
+
|
|
859
|
+
# add result to cache (if we have it) so we avoid recomputing it
|
|
860
|
+
if hasattr(embedder, "ase_bent_mols_dict"):
|
|
861
|
+
embedder.ase_bent_mols_dict[(identifier, tuple(sorted(pivot.index)), round(threshold, 3))] = final_mol
|
|
862
|
+
|
|
863
|
+
clean_directory()
|
|
864
|
+
|
|
865
|
+
return final_mol
|
|
866
|
+
|
|
867
|
+
def ase_dump(filename, images, atomnos, energies=None):
|
|
868
|
+
|
|
869
|
+
if energies is None:
|
|
870
|
+
energies = ["" for _ in images]
|
|
871
|
+
else:
|
|
872
|
+
energies = np.array(energies)
|
|
873
|
+
energies -= np.min(energies)
|
|
874
|
+
|
|
875
|
+
with open(filename, 'w') as f:
|
|
876
|
+
for i, (image, energy) in enumerate(zip(images, energies)):
|
|
877
|
+
e = f" Rel.E = {round(energy, 3)} kcal/mol" if energy != "" else ""
|
|
878
|
+
coords = image.get_positions()
|
|
879
|
+
write_xyz(coords, atomnos, f, title=f'STEP {i+1} - {filename[:-4]}_image_{i+1}{e}')
|