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 ADDED
@@ -0,0 +1,3 @@
1
+ from .nebscape import NEBscape, get_molecules_reactive_states
2
+
3
+ __all__ = ["NEBscape", "get_molecules_reactive_states"]
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