nebscape 0.1.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.
- nebscape/__init__.py +3 -0
- nebscape/analysis.py +597 -0
- nebscape/atommapper.py +418 -0
- nebscape/index_permutation.py +577 -0
- nebscape/minhop_helpers.py +1304 -0
- nebscape/minimize_rmsd.py +1158 -0
- nebscape/modified_minimahopping.py +781 -0
- nebscape/neb_from_globalmin.py +1463 -0
- nebscape/neb_helpers.py +669 -0
- nebscape/nebscape.py +766 -0
- nebscape/objects.py +1081 -0
- nebscape/resolve_pbc.py +662 -0
- nebscape/utils.py +468 -0
- nebscape/wfl_minimahopping.py +348 -0
- nebscape/wfl_stepwise_NEB.py +405 -0
- nebscape/wfl_vibration.py +103 -0
- nebscape-0.1.0.dist-info/METADATA +99 -0
- nebscape-0.1.0.dist-info/RECORD +19 -0
- nebscape-0.1.0.dist-info/WHEEL +4 -0
nebscape/__init__.py
ADDED
nebscape/analysis.py
ADDED
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Analyzing the result of NEB by looking at chemical fidelity. Also plotting multiple NEB results.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .utils import graphfromase, get_active_bonds_from_images, get_neb_path
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import matplotlib.pyplot as plt
|
|
8
|
+
import numpy as np
|
|
9
|
+
from ase.io import read
|
|
10
|
+
from ase.geometry import find_mic
|
|
11
|
+
from scipy.ndimage import label
|
|
12
|
+
from scipy.signal import argrelextrema
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def determine_whether_reverse(
|
|
16
|
+
initial_ref, initial_trial, final_trial, slab_indices, mult=0.8
|
|
17
|
+
): # numpydoc ignore=GL08
|
|
18
|
+
# slab_indices = [at_i for at_i, tag in enumerate(initial_ref.arrays['tags']) if tag != 2]
|
|
19
|
+
initial_graph_ref = graphfromase(initial_ref, slab_indices=slab_indices, mult=mult)
|
|
20
|
+
|
|
21
|
+
initial_graph2 = graphfromase(initial_trial, slab_indices=slab_indices, mult=mult)
|
|
22
|
+
final_graph2 = graphfromase(final_trial, slab_indices=slab_indices, mult=mult)
|
|
23
|
+
|
|
24
|
+
isomorphic1 = initial_graph_ref.isomorphic_vf2(
|
|
25
|
+
initial_graph2,
|
|
26
|
+
color1=initial_graph_ref.vs["AtomicNums"],
|
|
27
|
+
color2=initial_graph2.vs["AtomicNums"],
|
|
28
|
+
)
|
|
29
|
+
if isomorphic1:
|
|
30
|
+
reverse = False
|
|
31
|
+
|
|
32
|
+
isomorphic2 = initial_graph_ref.isomorphic_vf2(
|
|
33
|
+
final_graph2,
|
|
34
|
+
color1=initial_graph_ref.vs["AtomicNums"],
|
|
35
|
+
color2=final_graph2.vs["AtomicNums"],
|
|
36
|
+
)
|
|
37
|
+
if isomorphic2:
|
|
38
|
+
reverse = True
|
|
39
|
+
|
|
40
|
+
return reverse
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def partition_neb_profile(images, slab_indices, mult=0.8):
|
|
44
|
+
"""
|
|
45
|
+
Partition NEB profile based on molecular graph.
|
|
46
|
+
|
|
47
|
+
When there is single value in one partition, it can be merged to succeeding partition with merge=True
|
|
48
|
+
It often happens with transition state
|
|
49
|
+
|
|
50
|
+
Parameters
|
|
51
|
+
----------
|
|
52
|
+
images : list(ASE atoms)
|
|
53
|
+
list of image as NEB trajectory
|
|
54
|
+
slab_indices : list
|
|
55
|
+
list of slab indices
|
|
56
|
+
mult : float, optional
|
|
57
|
+
mult value for ase neighborlist, by default 0.8
|
|
58
|
+
|
|
59
|
+
Returns
|
|
60
|
+
-------
|
|
61
|
+
list
|
|
62
|
+
Nested list of indices with image index with same chemical connectivity as a group.
|
|
63
|
+
"""
|
|
64
|
+
list_of_graphs = []
|
|
65
|
+
for i, at in enumerate(images):
|
|
66
|
+
list_of_graphs.append(graphfromase(at, slab_indices=slab_indices, mult=mult))
|
|
67
|
+
|
|
68
|
+
corr = np.zeros([len(list_of_graphs), len(list_of_graphs)])
|
|
69
|
+
for i, graphs1 in enumerate(list_of_graphs):
|
|
70
|
+
for j, graphs2 in enumerate(list_of_graphs):
|
|
71
|
+
isomorphic = graphs1.isomorphic_vf2(
|
|
72
|
+
graphs2,
|
|
73
|
+
color1=graphs1.vs["AtomicNums"],
|
|
74
|
+
color2=graphs2.vs["AtomicNums"],
|
|
75
|
+
)
|
|
76
|
+
corr[i, j] = isomorphic
|
|
77
|
+
|
|
78
|
+
labeled_array, num_features = label(corr)
|
|
79
|
+
partitions = [
|
|
80
|
+
np.unique(np.where(labeled_array == i)[0]) for i in np.unique(np.diagonal(labeled_array))
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
groups_dict = {i: np.where(corr[i, :])[0] for i in range(len(images))}
|
|
84
|
+
IS = groups_dict[0]
|
|
85
|
+
FS_index = len(images) - 1
|
|
86
|
+
FS = groups_dict[FS_index]
|
|
87
|
+
TS = np.arange(IS[-1] + 1, FS[0])
|
|
88
|
+
# print(f"{TS = }")
|
|
89
|
+
if len(TS) != 0:
|
|
90
|
+
TS_index = TS[0]
|
|
91
|
+
IS_indices = []
|
|
92
|
+
FS_indices = []
|
|
93
|
+
TS_indices = []
|
|
94
|
+
for idx, array in groups_dict.items():
|
|
95
|
+
if idx != 0 and len(array) == len(IS) and np.all(array == IS):
|
|
96
|
+
IS_indices.append(idx)
|
|
97
|
+
elif idx != FS_index and len(array) == len(FS) and np.all(array == FS):
|
|
98
|
+
FS_indices.append(idx)
|
|
99
|
+
elif len(TS) != 0 and idx != TS_index and len(array) == len(TS) and np.all(array == TS):
|
|
100
|
+
TS_indices.append(idx)
|
|
101
|
+
for i in IS_indices + FS_indices + TS_indices:
|
|
102
|
+
del groups_dict[i]
|
|
103
|
+
|
|
104
|
+
if len(TS) != 0:
|
|
105
|
+
for name, idx in zip(["IS", "TS", "FS"], [0, TS_index, FS_index]):
|
|
106
|
+
groups_dict[name] = groups_dict.pop(idx)
|
|
107
|
+
else:
|
|
108
|
+
for name, idx in zip(["IS", "FS"], [0, FS_index]):
|
|
109
|
+
groups_dict[name] = groups_dict.pop(idx)
|
|
110
|
+
|
|
111
|
+
return groups_dict, partitions
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def get_chemical_transition_state(energy, transition, maxima, minima, num_images):
|
|
115
|
+
"""
|
|
116
|
+
Get the chemical transition state from the energy profile.
|
|
117
|
+
|
|
118
|
+
Parameters
|
|
119
|
+
----------
|
|
120
|
+
energy : np.ndarray
|
|
121
|
+
The energy profile as a numpy array.
|
|
122
|
+
transition : float
|
|
123
|
+
The transition state position. (e.g 7.5 if transition happens between image #7 and #8)
|
|
124
|
+
maxima : list[int]
|
|
125
|
+
The indices of the maxima in the energy profile.
|
|
126
|
+
minima : list[int]
|
|
127
|
+
The indices of the minima in the energy profile.
|
|
128
|
+
num_images : int
|
|
129
|
+
The total number of images in the NEB calculation.
|
|
130
|
+
|
|
131
|
+
Returns
|
|
132
|
+
-------
|
|
133
|
+
int
|
|
134
|
+
The index of the chemical transition state.
|
|
135
|
+
"""
|
|
136
|
+
search_maxima = True
|
|
137
|
+
index = np.ceil(transition)
|
|
138
|
+
while search_maxima:
|
|
139
|
+
if index in maxima:
|
|
140
|
+
search_maxima = False
|
|
141
|
+
upper_maxima = index
|
|
142
|
+
elif index in minima:
|
|
143
|
+
search_maxima = False
|
|
144
|
+
upper_maxima = None
|
|
145
|
+
if index == num_images - 1:
|
|
146
|
+
upper_maxima = index
|
|
147
|
+
break
|
|
148
|
+
index += 1
|
|
149
|
+
|
|
150
|
+
search_maxima = True
|
|
151
|
+
index = np.floor(transition)
|
|
152
|
+
while search_maxima:
|
|
153
|
+
if index in maxima:
|
|
154
|
+
search_maxima = False
|
|
155
|
+
lower_maxima = index
|
|
156
|
+
elif index in minima:
|
|
157
|
+
search_maxima = False
|
|
158
|
+
lower_maxima = None
|
|
159
|
+
if index == 0:
|
|
160
|
+
lower_maxima = 0
|
|
161
|
+
break
|
|
162
|
+
index -= 1
|
|
163
|
+
|
|
164
|
+
if lower_maxima is None:
|
|
165
|
+
TS_chemical = upper_maxima
|
|
166
|
+
return int(TS_chemical)
|
|
167
|
+
elif upper_maxima is None:
|
|
168
|
+
TS_chemical = lower_maxima
|
|
169
|
+
return int(TS_chemical)
|
|
170
|
+
|
|
171
|
+
if energy[int(lower_maxima)] < energy[int(upper_maxima)]:
|
|
172
|
+
TS_chemical = upper_maxima
|
|
173
|
+
else:
|
|
174
|
+
TS_chemical = lower_maxima
|
|
175
|
+
return int(TS_chemical)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def check_chemical_fidelity(images, slab_indices, energy_key="last_op__neb_energy", verbose=True):
|
|
179
|
+
"""
|
|
180
|
+
Check chemical fidelity of NEB results.
|
|
181
|
+
|
|
182
|
+
This function checks the followings.
|
|
183
|
+
(1) Check whether a NEB profile is single step process (Also identify "chemical tranition state")
|
|
184
|
+
- This is determined by looking at both chemical connectivity and energy profile.
|
|
185
|
+
- This should be ensured if NEB profile is indeed the a priori intended one
|
|
186
|
+
- "chemical tranition state" can be identified among energy maxima that corresponds to the intended chemical change.
|
|
187
|
+
(2) Check whether the highest point coincides with chemical transition state.
|
|
188
|
+
- If the highest point is not the same as "chemical tranition state", (e.g. diffusion barrier is the highest)
|
|
189
|
+
then obtained CI-NEB results are not meaningful.
|
|
190
|
+
- Therefore, additional refinement that changes initial and final that is close to "chemical tranition state" can be attempted.
|
|
191
|
+
|
|
192
|
+
Parameters
|
|
193
|
+
----------
|
|
194
|
+
images : list[ase.Atoms]
|
|
195
|
+
List of NEB images
|
|
196
|
+
slab_indices : list
|
|
197
|
+
list of slab atom indices
|
|
198
|
+
energy_key : str, optional
|
|
199
|
+
energy key saved in ASE.atoms.info, by default "last_op__neb_energy"
|
|
200
|
+
verbose : bool, optional
|
|
201
|
+
verbosity of the output, by default True
|
|
202
|
+
|
|
203
|
+
Returns
|
|
204
|
+
-------
|
|
205
|
+
single_step_criterion : bool
|
|
206
|
+
True if NEB images shows single step process.
|
|
207
|
+
energy_criterion : bool
|
|
208
|
+
True if the hightest point is the same as "chemical tranition state".
|
|
209
|
+
barrier_less : bool
|
|
210
|
+
True if there's no apparent barrier for chemical transition.
|
|
211
|
+
indices : tuple
|
|
212
|
+
Tuple(new initial, TS_chemical, final indices) : indices of minima and TS_chemical.
|
|
213
|
+
"""
|
|
214
|
+
energy = np.array([at.info[energy_key] for at in images])
|
|
215
|
+
groups_dict, partitions = partition_neb_profile(images, slab_indices, mult=0.8)
|
|
216
|
+
maxima = argrelextrema(energy, np.greater_equal, order=1)[0]
|
|
217
|
+
assert np.argmax(energy) in maxima, "Current list of maxima is incorrect"
|
|
218
|
+
|
|
219
|
+
minima = argrelextrema(energy, np.less_equal, order=1)[0]
|
|
220
|
+
assert np.argmin(energy) in minima, "Current list of minima is incorrect"
|
|
221
|
+
if verbose:
|
|
222
|
+
print(f"{partitions = }")
|
|
223
|
+
print(f"{maxima = }")
|
|
224
|
+
print(f"{minima = }")
|
|
225
|
+
single_step_criterion = False
|
|
226
|
+
barrier_less = False
|
|
227
|
+
if len(groups_dict) == 2:
|
|
228
|
+
# print("case 1")
|
|
229
|
+
transition = (partitions[0][-1] + partitions[1][0]) / 2
|
|
230
|
+
TS_chemical = get_chemical_transition_state(energy, transition, maxima, minima, len(images))
|
|
231
|
+
|
|
232
|
+
if TS_chemical == 0 or TS_chemical == len(images) - 1:
|
|
233
|
+
barrier_less = True
|
|
234
|
+
return True, False, barrier_less, None
|
|
235
|
+
|
|
236
|
+
single_step_criterion = True
|
|
237
|
+
elif len(groups_dict) == 3:
|
|
238
|
+
if "TS" in groups_dict.keys():
|
|
239
|
+
# print("case 2-1")
|
|
240
|
+
TS = groups_dict["TS"]
|
|
241
|
+
# check if Transition part of indices belong to the same barrier segment
|
|
242
|
+
for i in range(len(minima) - 1):
|
|
243
|
+
# barrier_range is defined as interval between each minima: concave down area of profile
|
|
244
|
+
barrier_range = np.arange(minima[i], minima[i + 1] + 1)
|
|
245
|
+
# TS region should be inside of barrier_range
|
|
246
|
+
# But also there should be no minima included in TS region
|
|
247
|
+
if np.all(np.isin(TS, barrier_range)) and np.all(~np.isin(TS, minima)):
|
|
248
|
+
TS_chemical = barrier_range[np.argmax(energy[barrier_range])]
|
|
249
|
+
single_step_criterion = True
|
|
250
|
+
break
|
|
251
|
+
else:
|
|
252
|
+
# print("case 2-2")
|
|
253
|
+
transition = (groups_dict["IS"][-1] + groups_dict["FS"][0]) / 2
|
|
254
|
+
TS_chemical = get_chemical_transition_state(
|
|
255
|
+
energy, transition, maxima, minima, len(images)
|
|
256
|
+
)
|
|
257
|
+
single_step_criterion = True
|
|
258
|
+
|
|
259
|
+
if single_step_criterion and verbose:
|
|
260
|
+
print(f"{TS_chemical = }")
|
|
261
|
+
if single_step_criterion and TS_chemical == np.argmax(energy):
|
|
262
|
+
energy_criterion = True
|
|
263
|
+
indices = None
|
|
264
|
+
elif single_step_criterion and TS_chemical != np.argmax(energy):
|
|
265
|
+
smaller = minima[minima < TS_chemical].max() # nearest smaller
|
|
266
|
+
larger = minima[minima > TS_chemical].min() # nearest larger
|
|
267
|
+
energy_criterion = False
|
|
268
|
+
indices = smaller, TS_chemical, larger
|
|
269
|
+
else:
|
|
270
|
+
# There's no meaningful chemical TS
|
|
271
|
+
energy_criterion = False
|
|
272
|
+
indices = None
|
|
273
|
+
|
|
274
|
+
return single_step_criterion, energy_criterion, barrier_less, indices
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def get_same_with_dft_traj(dfttraj, trajs_dict, slab, verbose=False): # numpydoc ignore=GL08
|
|
278
|
+
slab_indices = np.arange(len(slab))
|
|
279
|
+
same_with_dft = None
|
|
280
|
+
differences_init = {}
|
|
281
|
+
differences_fin = {}
|
|
282
|
+
|
|
283
|
+
active_bonds = get_active_bonds_from_images(dfttraj, slab_indices)
|
|
284
|
+
# If transfer reaction
|
|
285
|
+
if "Form" in active_bonds.keys() and "Break" in active_bonds.keys():
|
|
286
|
+
a, b = active_bonds["Break"]
|
|
287
|
+
c, d = active_bonds["Form"]
|
|
288
|
+
if verbose:
|
|
289
|
+
print(active_bonds)
|
|
290
|
+
|
|
291
|
+
v, l_init_dft = find_mic(
|
|
292
|
+
dfttraj[-1][a].position - dfttraj[-1][b].position,
|
|
293
|
+
cell=slab.cell,
|
|
294
|
+
pbc=slab.pbc,
|
|
295
|
+
)
|
|
296
|
+
v, l_fin_dft = find_mic(
|
|
297
|
+
dfttraj[0][c].position - dfttraj[0][d].position,
|
|
298
|
+
cell=dfttraj[0].cell,
|
|
299
|
+
pbc=dfttraj[0].pbc,
|
|
300
|
+
)
|
|
301
|
+
if verbose:
|
|
302
|
+
print("dft_l", l_init_dft, l_fin_dft)
|
|
303
|
+
|
|
304
|
+
# active_bonds = get_active_bonds_from_images(trajs_dict[min(trajs_dict.keys())], slab_indices)
|
|
305
|
+
# a,b = active_bonds["Break"]
|
|
306
|
+
# c,d = active_bonds["Form"]
|
|
307
|
+
# if verbose:
|
|
308
|
+
# print(active_bonds)
|
|
309
|
+
|
|
310
|
+
for key, images in trajs_dict.items():
|
|
311
|
+
active_bonds = get_active_bonds_from_images(images, slab_indices)
|
|
312
|
+
a, b = active_bonds["Break"]
|
|
313
|
+
c, d = active_bonds["Form"]
|
|
314
|
+
if verbose:
|
|
315
|
+
print(active_bonds)
|
|
316
|
+
|
|
317
|
+
v, l_init = find_mic(
|
|
318
|
+
images[-1][a].position - images[-1][b].position,
|
|
319
|
+
cell=images[-1].cell,
|
|
320
|
+
pbc=images[-1].pbc,
|
|
321
|
+
)
|
|
322
|
+
v, l_fin = find_mic(
|
|
323
|
+
images[0][c].position - images[0][d].position,
|
|
324
|
+
cell=images[0].cell,
|
|
325
|
+
pbc=images[0].pbc,
|
|
326
|
+
)
|
|
327
|
+
if verbose:
|
|
328
|
+
print("l_init, l_fin", l_init, l_fin)
|
|
329
|
+
|
|
330
|
+
differences_init[key] = np.abs(l_init_dft - l_init)
|
|
331
|
+
differences_fin[key] = np.abs(l_fin_dft - l_fin)
|
|
332
|
+
same_with_dft_init = min(differences_init, key=differences_init.get)
|
|
333
|
+
same_with_dft_fin = min(differences_fin, key=differences_fin.get)
|
|
334
|
+
|
|
335
|
+
if same_with_dft_init == same_with_dft_fin:
|
|
336
|
+
same_with_dft = same_with_dft_init
|
|
337
|
+
else:
|
|
338
|
+
# print(f"same_with_dft conflict. ")
|
|
339
|
+
# print(f"{differences_init = }")
|
|
340
|
+
# print(f"{differences_fin = }")
|
|
341
|
+
for key in trajs_dict.keys():
|
|
342
|
+
if differences_init[key] < 1e-4 and differences_fin[key] < 1e-4:
|
|
343
|
+
# print(f"{key} were selected.")
|
|
344
|
+
return key
|
|
345
|
+
|
|
346
|
+
# if np.isclose(l_init_dft ,l_init, atol=atol) and np.isclose(l_fin_dft ,l_fin, atol=atol):
|
|
347
|
+
# same_with_dft = key
|
|
348
|
+
# break
|
|
349
|
+
|
|
350
|
+
# Dissociation reaction
|
|
351
|
+
elif "Form" not in active_bonds.keys() and "Break" in active_bonds.keys():
|
|
352
|
+
a, b = active_bonds["Break"]
|
|
353
|
+
if verbose:
|
|
354
|
+
print(active_bonds)
|
|
355
|
+
|
|
356
|
+
v, l_init_dft = find_mic(
|
|
357
|
+
dfttraj[0][a].position - dfttraj[0][b].position,
|
|
358
|
+
cell=slab.cell,
|
|
359
|
+
pbc=slab.pbc,
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
if verbose:
|
|
363
|
+
print(f"{l_init_dft=:0.3f}")
|
|
364
|
+
|
|
365
|
+
active_bonds = get_active_bonds_from_images(
|
|
366
|
+
trajs_dict[min(trajs_dict.keys())], slab_indices
|
|
367
|
+
)
|
|
368
|
+
a, b = active_bonds["Break"]
|
|
369
|
+
if verbose:
|
|
370
|
+
print(active_bonds)
|
|
371
|
+
|
|
372
|
+
for key, images in trajs_dict.items():
|
|
373
|
+
v, l_init = find_mic(
|
|
374
|
+
images[0][a].position - images[0][b].position,
|
|
375
|
+
cell=images[-1].cell,
|
|
376
|
+
pbc=images[-1].pbc,
|
|
377
|
+
)
|
|
378
|
+
if verbose:
|
|
379
|
+
print(f"{l_init=:0.3f}")
|
|
380
|
+
|
|
381
|
+
differences_init[key] = np.abs(l_init_dft - l_init)
|
|
382
|
+
|
|
383
|
+
same_with_dft = min(differences_init, key=differences_init.get)
|
|
384
|
+
|
|
385
|
+
# if np.isclose(l_init_dft ,l_init, atol=atol):
|
|
386
|
+
# same_with_dft = key
|
|
387
|
+
# break
|
|
388
|
+
|
|
389
|
+
return same_with_dft
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def plot_neb(
|
|
393
|
+
rootdir,
|
|
394
|
+
base_path="/work/Calculation/7_neb_protocol/benchmark_var_k",
|
|
395
|
+
dft_trajs_path="/work/Calculation/dft_trajs_for_release",
|
|
396
|
+
ylim=None,
|
|
397
|
+
annotate=False,
|
|
398
|
+
): # numpydoc ignore=GL08
|
|
399
|
+
markers = ["o", "s", "*"]
|
|
400
|
+
# rootdir = Path(f'/work/Calculation/7_neb_protocol/benchmark_var_k/transfer_6/transfer_ood_284_4627_6_111-8')
|
|
401
|
+
|
|
402
|
+
if len(str(rootdir).split("/")) == 1:
|
|
403
|
+
# print('hey')
|
|
404
|
+
reaction_type = rootdir.split("_")[0]
|
|
405
|
+
transfer_idx = int(rootdir.split("_")[4])
|
|
406
|
+
folder = rootdir
|
|
407
|
+
rootdir = Path(f"{base_path}/{reaction_type}_{transfer_idx}/{rootdir}")
|
|
408
|
+
else:
|
|
409
|
+
folder = str(rootdir).split("/")[-1]
|
|
410
|
+
reaction_type = folder.split("_")[0]
|
|
411
|
+
|
|
412
|
+
dfttraj = read(Path(f"{dft_trajs_path}/{reaction_type}s") / f"{folder}_neb1.0.traj", "-10:")
|
|
413
|
+
slab = read(rootdir / "slab.xyz")
|
|
414
|
+
slab_indices = [at_i for at_i, tag in enumerate(dfttraj[0].arrays["tags"]) if tag != 2]
|
|
415
|
+
reverse = None
|
|
416
|
+
|
|
417
|
+
fig, axs = plt.subplots(1, 2, figsize=(10, 5), dpi=300)
|
|
418
|
+
nebtrajs = {}
|
|
419
|
+
|
|
420
|
+
reverse = None
|
|
421
|
+
|
|
422
|
+
# if reaction_type == "dissociation":
|
|
423
|
+
# paths = Path(rootdir / f"2_NEB/00/0_geometry").glob("*")
|
|
424
|
+
# elif reaction_type == "transfer":
|
|
425
|
+
paths = Path(rootdir / "2_NEB/00").glob("[!backup]*/")
|
|
426
|
+
|
|
427
|
+
for i, path in enumerate(paths):
|
|
428
|
+
alpha = 1
|
|
429
|
+
if Path(path / "NEB_climb.xyz").is_file():
|
|
430
|
+
nebtraj = read(path / "NEB_climb.xyz", ":")
|
|
431
|
+
reverse = determine_whether_reverse(dfttraj[0], nebtraj[0], nebtraj[-1], slab_indices)
|
|
432
|
+
if reverse is None:
|
|
433
|
+
reverse = determine_whether_reverse(
|
|
434
|
+
dfttraj[0], nebtraj[0], nebtraj[-1], slab_indices
|
|
435
|
+
)
|
|
436
|
+
subdir = str(path).split("/")[-1]
|
|
437
|
+
nebtrajs[subdir] = nebtraj
|
|
438
|
+
energy_minhop = np.array([at.info["last_op__neb_energy"] for at in nebtraj])
|
|
439
|
+
if nebtraj[0].info["neb_config_type"] == "neb_last_converged":
|
|
440
|
+
converged = "T"
|
|
441
|
+
elif (
|
|
442
|
+
nebtraj[0].info["neb_config_type"] == "neb_last_unconverged"
|
|
443
|
+
and nebtraj[0].info["neb_n_steps"] < 500
|
|
444
|
+
):
|
|
445
|
+
converged = "T"
|
|
446
|
+
else:
|
|
447
|
+
converged = "F"
|
|
448
|
+
alpha = 0.1
|
|
449
|
+
|
|
450
|
+
# if reverse:
|
|
451
|
+
# print('reversed!')
|
|
452
|
+
# axs[1].plot(get_neb_path(nebtraj), energy_minhop[::-1], marker="o", c=f"C{i}", alpha=alpha, label=f"permute#{i}-{converged}")
|
|
453
|
+
# else:
|
|
454
|
+
marker = markers[int(subdir.split("_")[0])]
|
|
455
|
+
axs[1].plot(
|
|
456
|
+
get_neb_path(nebtraj),
|
|
457
|
+
energy_minhop,
|
|
458
|
+
marker=marker,
|
|
459
|
+
c=f"C{i}",
|
|
460
|
+
alpha=alpha,
|
|
461
|
+
label=f"#{subdir}-{converged}",
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
if annotate and converged == "T":
|
|
465
|
+
for j, at in enumerate(nebtraj):
|
|
466
|
+
axs[1].annotate(
|
|
467
|
+
f"{j}", (get_neb_path(nebtraj)[j], energy_minhop[j]), fontsize=8
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
# reverse = determine_whether_reverse(dfttraj[0], nebtraj[0], nebtraj[-1], slab_indices)
|
|
471
|
+
|
|
472
|
+
# for i in range(3):
|
|
473
|
+
ocp_trajs = {}
|
|
474
|
+
for i, path in enumerate(Path(rootdir / "3_OCP_benchmark").glob("*")):
|
|
475
|
+
alpha = 1
|
|
476
|
+
if Path(rootdir / f"3_OCP_benchmark/{i}").is_dir():
|
|
477
|
+
nebtraj = read(rootdir / f"3_OCP_benchmark/{i}/NEB_climb.xyz", ":")
|
|
478
|
+
ocp_trajs[i] = nebtraj
|
|
479
|
+
same_with_dft = get_same_with_dft_traj(dfttraj, ocp_trajs, slab)
|
|
480
|
+
# print(f'{same_with_dft = }')
|
|
481
|
+
|
|
482
|
+
ocp_trajs = {}
|
|
483
|
+
for i, path in enumerate(Path(rootdir / "3_OCP_benchmark").glob("*")):
|
|
484
|
+
alpha = 1
|
|
485
|
+
if Path(rootdir / f"3_OCP_benchmark/{i}").is_dir():
|
|
486
|
+
nebtraj = read(rootdir / f"3_OCP_benchmark/{i}/NEB_climb.xyz", ":")
|
|
487
|
+
ocp_trajs[i] = nebtraj
|
|
488
|
+
if reverse:
|
|
489
|
+
print("reversed!")
|
|
490
|
+
nebtraj = nebtraj[::-1]
|
|
491
|
+
energy_eq2 = np.array([at.info["last_op__neb_energy"] for at in nebtraj])
|
|
492
|
+
if nebtraj[0].info["neb_config_type"] == "neb_last_converged":
|
|
493
|
+
converged = "T"
|
|
494
|
+
elif (
|
|
495
|
+
nebtraj[0].info["neb_config_type"] == "neb_last_unconverged"
|
|
496
|
+
and nebtraj[0].info["neb_n_steps"] < 500
|
|
497
|
+
):
|
|
498
|
+
converged = "T"
|
|
499
|
+
else:
|
|
500
|
+
converged = "F"
|
|
501
|
+
alpha = 0.1
|
|
502
|
+
|
|
503
|
+
# if reverse:
|
|
504
|
+
# print('reversed')
|
|
505
|
+
# axs[0].plot(get_neb_path(nebtraj)[::-1], energy_eq2, alpha=alpha, c=f"C{i}", marker="o", label=f"permute#{i}-{converged}")
|
|
506
|
+
# else:
|
|
507
|
+
subdir = str(path).split("/")[-1]
|
|
508
|
+
if i == same_with_dft:
|
|
509
|
+
axs[0].plot(
|
|
510
|
+
get_neb_path(nebtraj),
|
|
511
|
+
energy_eq2,
|
|
512
|
+
alpha=alpha,
|
|
513
|
+
c=f"C{i}",
|
|
514
|
+
marker="o",
|
|
515
|
+
label=f"#{subdir}-{converged}**",
|
|
516
|
+
)
|
|
517
|
+
else:
|
|
518
|
+
axs[0].plot(
|
|
519
|
+
get_neb_path(nebtraj),
|
|
520
|
+
energy_eq2,
|
|
521
|
+
alpha=alpha,
|
|
522
|
+
c=f"C{i}",
|
|
523
|
+
marker="o",
|
|
524
|
+
label=f"#{subdir}-{converged}",
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
# for at in dfttraj[1:-1]:
|
|
528
|
+
# at.info["dft_energy"] = at.get_potential_energy()
|
|
529
|
+
# for at in dfttraj:
|
|
530
|
+
# at.calc = ocpcalc
|
|
531
|
+
# at.info["ocp_energy"] = at.get_potential_energy()
|
|
532
|
+
|
|
533
|
+
# energy_dft = np.array([at.info["dft_energy"] for at in dfttraj[1:-1]])
|
|
534
|
+
# energy_sp = np.array([at.info["ocp_energy"] for at in dfttraj])
|
|
535
|
+
# axs[0].plot(get_neb_path(dfttraj), energy_sp, marker="s",c=f"C3", label="eq2-sp@DFT")
|
|
536
|
+
# axs[0].plot(get_neb_path(dfttraj)[1:-1], energy_dft-energy_dft[0]+energy_sp[1], c=f"C4", marker="x", label="DFT")
|
|
537
|
+
for i in range(2):
|
|
538
|
+
# axs[i].plot(get_neb_path(dfttraj), energy_sp, marker="s",c=f"C3", label="eq2-sp@DFT")
|
|
539
|
+
# axs[i].plot(get_neb_path(dfttraj)[1:-1], energy_dft-energy_dft[0]+energy_sp[1], c=f"C4", marker="x", label="DFT")
|
|
540
|
+
axs[i].set_ylabel("Energy [eV]", fontsize=12)
|
|
541
|
+
axs[i].set_xlabel("displacement [$\AA$]", fontsize=12)
|
|
542
|
+
axs[i].legend()
|
|
543
|
+
|
|
544
|
+
axs[0].set_title("OCP geometry")
|
|
545
|
+
axs[1].set_title("Minima hopping geometry")
|
|
546
|
+
|
|
547
|
+
if ylim is None:
|
|
548
|
+
axs[0].set_ylim(
|
|
549
|
+
(
|
|
550
|
+
min(min(axs[0].get_ylim()), min(axs[1].get_ylim())),
|
|
551
|
+
max(max(axs[0].get_ylim()), max(axs[1].get_ylim())),
|
|
552
|
+
)
|
|
553
|
+
)
|
|
554
|
+
ymin, ymax = (
|
|
555
|
+
min(min(axs[0].get_ylim()), min(axs[1].get_ylim())),
|
|
556
|
+
max(max(axs[0].get_ylim()), max(axs[1].get_ylim())),
|
|
557
|
+
)
|
|
558
|
+
axs[1].set_ylim(
|
|
559
|
+
(
|
|
560
|
+
min(min(axs[0].get_ylim()), min(axs[1].get_ylim())),
|
|
561
|
+
max(max(axs[0].get_ylim()), max(axs[1].get_ylim())),
|
|
562
|
+
)
|
|
563
|
+
)
|
|
564
|
+
else:
|
|
565
|
+
ymin, ymax = ylim
|
|
566
|
+
axs[0].set_ylim((ylim[0], ylim[1]))
|
|
567
|
+
axs[1].set_ylim((ylim[0], ylim[1]))
|
|
568
|
+
### Partition neb profile
|
|
569
|
+
for j, profiles in enumerate([ocp_trajs, nebtrajs]):
|
|
570
|
+
highest_points = {}
|
|
571
|
+
for key, images in profiles.items():
|
|
572
|
+
if images[0].info["neb_config_type"] == "neb_last_converged":
|
|
573
|
+
highest_points[key] = np.max([at.info["last_op__neb_energy"] for at in images])
|
|
574
|
+
elif (
|
|
575
|
+
images[0].info["neb_config_type"] == "neb_last_unconverged"
|
|
576
|
+
and images[0].info["neb_n_steps"] < 500
|
|
577
|
+
):
|
|
578
|
+
highest_points[key] = np.max([at.info["last_op__neb_energy"] for at in images])
|
|
579
|
+
|
|
580
|
+
if len(highest_points) != 0:
|
|
581
|
+
best_neb_profile = min(highest_points, key=highest_points.get)
|
|
582
|
+
groups, partitions = partition_neb_profile(profiles[best_neb_profile], slab_indices)
|
|
583
|
+
|
|
584
|
+
# for indices in partitions:
|
|
585
|
+
# axs[j].fill_between(x[indices], (ymax - ymin) / 40 + ymin, (ymax - ymin) / 20 + ymin, color=f"C{color_i}", alpha=0.5)
|
|
586
|
+
for i in range(len(partitions) - 1):
|
|
587
|
+
transition = (partitions[i][-1] + partitions[i + 1][0]) / 2
|
|
588
|
+
plt.axvline(x=transition, linestyle=":", c="k")
|
|
589
|
+
# for indices in partitions[:-1]:
|
|
590
|
+
# axs[j].axvline(x=x[indices][-1], c=f"C{color_i}", alpha=0.9, linestyle="--")
|
|
591
|
+
|
|
592
|
+
# axs[1].set_ylim([-1,2])
|
|
593
|
+
plt.suptitle(str(rootdir).split("/")[-1])
|
|
594
|
+
plt.savefig(rootdir / "neb.png")
|
|
595
|
+
plt.tight_layout()
|
|
596
|
+
plt.show()
|
|
597
|
+
return ocp_trajs, nebtrajs
|