-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathpipelines.py
1563 lines (1416 loc) · 68.3 KB
/
pipelines.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import os
import shutil
import subprocess
import numpy as np
from statistics import mean
from matplotlib import pyplot as plt
from matplotlib import gridspec
import ase
from ase.io import read, write, Trajectory
from ase.calculators.vasp import Vasp
from ase.calculators.vasp.create_input import float_keys, exp_keys, string_keys, int_keys, bool_keys, list_int_keys, list_bool_keys, list_float_keys, special_keys, dict_keys
from ase.optimize import BFGS
from ase.neighborlist import NeighborList, natural_cutoffs
from ase.neb import NEB
from ase.eos import calculate_eos
from ase.eos import EquationOfState as EOS
from ase.constraints import FixAtoms
from ase.vibrations import Vibrations
from ase.thermochemistry import HarmonicThermo, IdealGasThermo
import pandas as pd
import re
from distutils.dir_util import copy_tree
from scipy.optimize import curve_fit
import pexpect
"""VASP related codes using ASE"""
def get_base_calc():
base_calc = Vasp(
gga="PE",
lreal="Auto",
lplane=True,
lwave=False,
lcharg=False,
ncore=8,
prec="Normal",
encut=300,
ediff=1e-6,
algo="VeryFast",
ismear=-5,
gamma=True,
)
return base_calc
# See ASE documentation for more information on why this is necessary.
def set_vasp_key(calc, key, value):
if key in float_keys:
calc.float_params[key] = value
elif key in exp_keys:
calc.exp_params[key] = value
elif key in string_keys:
calc.string_params[key] = value
elif key in int_keys:
calc.int_params[key] = value
elif key in bool_keys:
calc.bool_params[key] = value
elif key in list_int_keys:
calc.list_int_params[key] = value
elif key in list_bool_keys:
calc.list_bool_params[key] = value
elif key in list_float_keys:
calc.list_float_params[key] = value
elif key in list_float_keys:
calc.list_float_params[key] = value
elif key in special_keys:
calc.special_params[key] = value
elif key in dict_keys:
calc.dict_params[key] = value
# some keys need special treatment
# including kpts, gamma, xc
if key in calc.input_params.keys():
calc.input_params[key] = value
def cell_opt(atoms, kpts, npoints=5, eps=0.04, addnl_settings=None):
"""Optimizes the size of the simulation cell.
:param atoms: Atoms to be optimized
:type atoms: Atoms object
:param kpts: KPOINTS used for the calculation
:type kpts: list
:param npoints: Number of optimization points for the calculation, defaults to 5
:type npoints: int, optional
:param eps: Variation in volume, defaults to 0.04
:type eps: float, optional
:param addnl_settings: Dictionary containing any additional VASP settings (either editing default settings of base_calc or adding more settings), defaults to None
:type addnl_settings: dict, optional
:return: Optimized atoms
:rtype: Atoms object
"""
calc = get_base_calc()
if addnl_settings!=None:
keys = addnl_settings.keys()
for key in keys:
set_vasp_key(calc, key, addnl_settings[key])
calc.set(ibrion=-1, nsw=0, kpts=kpts)
atoms.calc = calc
eos = calculate_eos(atoms, npoints=npoints, eps=eps, trajectory="eos.traj")
v,_,_ = eos.fit()
eos.plot(filename="eos.png")
opt_factor = v / atoms.get_volume()
atoms.cell = atoms.cell * opt_factor
write("opted_cell.vasp", atoms)
return atoms
def axis_opt(atoms, kpts, axis, npoints=5, eps=0.04, addnl_settings=None):
"""Optimizes the size of the required axis of the simulation cell.
:param atoms: Atoms to be optimized
:type atoms: Atoms object
:param kpts: KPOINTS used for the calculation
:type kpts: list
:param axis: The axis to be optimized
:type axis: int
:param npoints: Number of optimization points for the calculation, defaults to 5
:type npoints: int, optional
:param eps: Variation in volume, defaults to 0.04
:type eps: float, optional
:param addnl_settings: Dictionary containing any additional VASP settings (either editing default settings of base_calc or adding more settings), defaults to None
:type addnl_settings: dict, optional
:return: Optimized atoms
:rtype: Atoms object
"""
ens = np.zeros(npoints)
vols = np.zeros(npoints)
# by defualt, shrink/expand axis by 4%
factors = np.linspace(1 - eps, 1 + eps, npoints)
for ifactor, factor in np.ndenumerate(factors):
atoms_tmp = atoms.copy()
atoms_tmp.cell[axis] = atoms.cell[axis] * factor
calc = get_base_calc()
if addnl_settings!=None:
keys = addnl_settings.keys()
for key in keys:
set_vasp_key(calc, key, addnl_settings[key])
calc.set(ibrion=-1, nsw=0, kpts=kpts, directory=f"{factor:.2f}")
atoms_tmp.calc = calc
ens[ifactor] = atoms_tmp.get_potential_energy()
vols[ifactor] = atoms_tmp.get_volume()
eos = EOS(volumes=vols, energies=ens, eos="sj")
v0,_,_ = eos.fit()
opt_factor = v0 / atoms.get_volume()
atoms.cell[axis] = atoms.cell[axis] * opt_factor
write("opted_axis.vasp", atoms)
return atoms
def geo_opt(atoms, mode="vasp", opt_levels=None, restart=None, fmax=0.02):
"""Performs geometry optimization on the system using inbuilt VASP optimizer (IBRION=2) or ASE's BFGS optimizer.
:param atoms: Atoms to be geometrically optimized
:type atoms: Atoms object
:param mode: Type of optimizer to be used, "vasp" for IBRION=2 and "ase" for BFGS, defaults to "vasp"
:type mode: str, optional
:param opt_levels: Dictionary of dictionaries, each dictionary containing settings for each level of calculation, defaults to
``{
1:{"kpts":[3,3,3]},
2:{"kpts":[5,5,5]},
3:{"kpts":[7,7,7]}
}``
:type opt_levels: dict, optional
:param restart: Restarting a calculation if `restart=True`, defaults to None
:type restart: bool, optional
:param fmax: Maximum force on optimized atoms, defaults to 0.02
:type fmax: float, optional
"""
def save_files(level):
shutil.copyfile("CONTCAR", f"opt{level}.vasp")
shutil.copyfile("OUTCAR", f"opt{level}.OUTCAR")
shutil.copyfile("vasp.out", f"opt{level}.out")
def opt_by_vasp(opt_levels, level):
calc = get_base_calc()
set_vasp_key(calc, 'nsw', 500)
set_vasp_key(calc, 'nelm', 500)
set_vasp_key(calc, 'ediffg', -1e-2)
set_vasp_key(calc, 'ibrion', 2)
level_settings = opt_levels[level]
for key in level_settings.keys():
set_vasp_key(calc, key, level_settings[key])
atoms = read("CONTCAR")
atoms.calc = calc
atoms.get_potential_energy()
atoms = read("OUTCAR", index=-1)
save_files(level)
return atoms
def opt_by_ase(atoms, opt_levels, level):
calc = get_base_calc()
level_settings = opt_levels[level]
for key in level_settings.keys():
if key in ['nsw', 'ibrion', 'ediffg']:
continue
set_vasp_key(calc, key, level_settings[key])
set_vasp_key(calc, 'ibrion', -1)
set_vasp_key(calc, 'nsw', 0)
atoms.calc = calc
opt = BFGS(atoms,
trajectory = f"opt{level}.traj",
logfile = f"opt{level}.log")
opt.run(fmax=fmax)
save_files(level)
return atoms
if not opt_levels:
# for bulks.
# other systems: pass in argument
opt_levels = {
1: {"kpts": [3, 3, 3]},
2: {"kpts": [5, 5, 5]},
3: {"kpts": [7, 7, 7]},
}
levels = opt_levels.keys()
if restart != True:
write("CONTCAR", atoms)
for level in levels:
if mode=="vasp":
atoms = opt_by_vasp(opt_levels, level)
elif mode=="ase":
atoms = opt_by_ase(atoms, opt_levels, level)
if restart == True:
last_level = 0
for level in levels:
if os.path.exists(f"opt{level}.out"):
last_level = level
levels = list(levels)
if last_level==0:
last_level = levels[0]-1
for level in range(last_level+1,levels[-1]+1):
if mode=="vasp":
if os.path.exists("CONTCAR"):
pass
else:
write("CONTCAR", atoms)
atoms = opt_by_vasp(opt_levels, level)
elif mode=="ase":
if os.path.exists(f"opt{level}.traj"):
try:
atoms = read(f"opt{level}.traj@-1")
except ase.io.formats.UnknownFileTypeError:
pass
atoms = opt_by_ase(atoms, opt_levels, level)
return atoms
def get_valence_electrons(atoms=None, addnl_settings=None):
"""Provides number of valence electrons of each element in a calculation. If POTCAR already exists in the calculation folder, atoms and addnl_settings parameters are not required. If not, based on the elements in the atoms object and calculation settings, the number of valence electrons are calculated. Example: If setups is provided as {"Li": "_sv"} in addnl_settings, the number of valence electrons for Li are 3 (instead of 1 when setups is not provided).
:param atoms: Atoms for which valence electrons of each element are to be obtained, defaults to None
:type atoms: Atoms object, optional
:param addnl_settings: Dictionary containing any additional VASP settings (either editing default settings of base_calc or adding more settings), defaults to None
:type addnl_settings: dict, optional
:return: Valence electrons of each element in the calculation
:rtype: dict
"""
valence_electrons = {}
potcar_folder = os.getenv("VASP_PP_PATH")
def get_potcar_subfolder(settings):
if settings["gga"]==None:
potcar_subfolder = "potpaw"
elif settings["gga"]=="PE":
potcar_subfolder = "potpaw_PBE"
elif settings["gga"]=="91":
potcar_subfolder = "potpaw_GGA"
return potcar_subfolder
if potcar_folder is not None and not os.path.exists("POTCAR"):
formula = str(atoms.symbols)
elements = re.findall(r'([A-Z][a-z]?)\d*', formula)
if addnl_settings is not None:
if "setups" in addnl_settings:
setups = addnl_settings["setups"]
keys = list(setups.keys())
else:
keys = None
base_calc = get_base_calc()
if "gga" in base_calc.parameters and "gga" not in addnl_settings:
potcar_subfolder = get_potcar_subfolder(base_calc.parameters)
elif "gga" in addnl_settings:
potcar_subfolder = get_potcar_subfolder(addnl_settings)
elif addnl_settings is None:
potcar_subfolder = "potpaw_PBE"
keys = None
f = open("POTCAR","w")
cwd = os.getcwd()
os.chdir(f"{potcar_folder}"+f"/{potcar_subfolder}")
for element in elements:
if keys is not None and element in keys:
os.chdir(f"{element}"+f"{setups[element]}")
else:
os.chdir(element)
temp_f = open("POTCAR", "r")
f.write(temp_f.read())
os.chdir("../")
os.chdir(cwd)
f.close()
f = open("POTCAR", "r")
lines = f.readlines()
search_str = lines[0].split()[0]
for i,line in enumerate(lines):
if search_str in line and not any(excluded in line for excluded in ["TITEL", "LPAW", "radial sets"]):
match = re.search(fr"{search_str}\s+([A-Z][a-z]?)", line)
next_line = lines[i+1]
nelect = int(float(next_line.split()[0]))
if match:
element = match.group(1)
valence_electrons[element] = nelect
return valence_electrons
def bader(atoms, kpts, valence_electrons=None, addnl_settings=None, restart=None):
"""Performs bader charge analysis on the system. Charges can be viewed in ACF.dat file or using ase gui and choosing the Initial Charges label in the view tab.
:param atoms: Atoms for which charge should be determined
:type atoms: Atoms object
:param kpts: KPOINTS used for the calculation
:type kpts: list
:param valence_electrons: Dictionary containing the symbol of atoms as key and the corresponding valence electrons from POTCAR as value, example: {"Si":4, "H":1}, defaults to None
:type valence_electrons: dict, optional
:param addnl_settings: Dictionary containing any additional VASP settings (either editing default settings of base_calc or adding more settings), defaults to None
:type addnl_settings: dict, optional
"""
def run_vasp(atoms):
calc = get_base_calc()
if addnl_settings!=None:
for key in addnl_settings.keys():
set_vasp_key(calc, key, addnl_settings[key])
calc.set(
ibrion=-1, nsw=0, lorbit=12, lcharg=True, laechg=True, kpts=kpts
)
atoms.calc = calc
atoms.get_potential_energy()
assert os.path.exists("AECCAR0"), "chgsum.pl: AECCAR0 not found"
assert os.path.exists("AECCAR2"), "chgsum.pl: AECCAR2 not found"
def run_bader():
# add charges
chgsum = os.getenv("VTST_SCRIPTS") + "/chgsum.pl"
assert os.path.exists(chgsum), "chgsum not found"
subprocess.run([chgsum, "AECCAR0", "AECCAR2"], capture_output=True)
assert os.path.exists("CHGCAR_sum"), "chgsum.pl: CHGCAR_sum not found"
# run bader
bader = os.getenv("VTST_BADER")
assert os.path.exists(bader), "bader not found"
subprocess.run(
[bader, "CHGCAR", "-ref", "CHGCAR_sum"], capture_output=True
)
assert os.path.exists("ACF.dat"), "bader: ACF.dat not found"
def read_bader(atoms, valence_electrons=valence_electrons):
if valence_electrons is None:
valence_electrons = get_valence_electrons()
else:
valence_electrons = valence_electrons
latoms = len(atoms)
df = pd.read_table(
"ACF.dat",
delim_whitespace=True,
header=0,
skiprows=[1, latoms + 2, latoms + 3, latoms + 4, latoms + 5],
)
charges = df["CHARGE"].to_numpy()
ocharges = np.array([])
for atom in atoms:
ocharges = np.append(ocharges, valence_electrons[atom.symbol])
ocharges = np.array([int(i) for i in ocharges])
dcharges = -charges + ocharges
atoms.set_initial_charges(np.round(dcharges, 2))
return atoms
if restart is not True:
run_vasp(atoms)
run_bader()
atoms_with_charge = read_bader(atoms)
return atoms_with_charge
elif restart is True:
if os.path.exists("AECCAR0") and os.path.exists("AECCAR2") and not os.path.exists("ACF.dat"):
run_bader()
atoms_with_charge = read_bader(atoms)
return atoms_with_charge
elif os.path.exists("ACF.dat"):
atoms_with_charge = read_bader(atoms)
return atoms_with_charge
else:
run_vasp(atoms)
run_bader()
atoms_with_charge = read_bader(atoms)
return atoms_with_charge
class COHP:
"""Performs COHP analysis on the system. The output is saved as cohp-1.png.
"""
def __init__(self, atoms, bonds, lobsterin_template=None):
"""Initializes the COHP class.
:param atoms: Atoms on which COHP analysis is performed
:type atoms: Atoms object
:param bonds: List of lists, where each list contains the indexes of two bonding atoms, example: [[0,1],[1,2],[2,3]]
:type bonds: list
:param lobsterin_template: Path of file which contains lobster template. If no path is given, the default template is used, defaults to None
:type lobsterin_template: str, optional
"""
self.atoms = atoms
self.bonds = bonds
if lobsterin_template:
template = lobsterin_template
else:
template = [
"COHPstartEnergy -22\n",
"COHPendEnergy 18\n",
"basisSet pbeVaspFit2015\n",
"includeOrbitals sp\n",
]
self.lobsterin_template = template
def run_vasp(self, kpts, valence_electrons=None, addnl_settings=None):
"""Runs a single point calculation on the system to generate the WAVECAR and CHGCAR files required for COHP analysis.
:param kpts: KPOINTS used for the calculation
:type kpts: list
:param valence_electrons: Dictionary containing the symbol of atoms as key and the corresponding valence electrons from POTCAR as value, example: {"Si":4, "H":1}, defaults to None
:type valence_electrons: dict, optional
:param addnl_settings: Dictionary containing any additional VASP settings (either editing default settings of base_calc or adding more settings), defaults to None
:type addnl_settings: dict, optional
"""
atoms = self.atoms
calc = get_base_calc()
if addnl_settings!=None:
for key in addnl_settings.keys():
set_vasp_key(calc, key, addnl_settings[key])
calc.set(ibrion=-1, nsw=0, isym=-1, prec="Accurate", kpts=kpts, lwave=True, lcharg=True)
if valence_electrons is None:
valence_electrons = get_valence_electrons(atoms=atoms, addnl_settings=addnl_settings)
else:
valence_electrons = valence_electrons
nelect = 0
for atom in atoms:
nelect = nelect + valence_electrons[atom.symbol]
calc.set(nbands=nelect + 20) # Giving 20 empty bands. May require more since the number of bands can be > number of basis functions, but not less!
atoms.calc = calc
atoms.get_potential_energy()
def write_lobsterin(self):
"""Adds the information about the bonds to the lobsterin file.
"""
lobsterin = "lobsterin"
with open(f"{lobsterin}", "w+") as fhandle:
for line in self.lobsterin_template:
fhandle.write(line)
for b in self.bonds:
fhandle.write(f"cohpBetween atom {b[0]+1} and atom {b[1]+1}\n")
def run_lobster(self):
"""Runs the lobster executable.
"""
lobster = os.getenv("LOBSTER")
subprocess.run([lobster], capture_output=True)
def plot(self, cohp_xlim, cohp_ylim, icohp_xlim, icohp_ylim):
"""Plots the COHP and ICOHP data.
:param cohp_xlim: Lowest and highest COHP values in eV for COHP plot, example: [-2.6, 1]
:type cohp_xlim: list
:param cohp_ylim: Lowest and highest energy values in eV for COHP plot, example: [-11, 8.5]
:type cohp_ylim: list
:param icohp_xlim: Lowest and highest ICOHP values in eV for ICOHP plot, example: [-0.01, 1.5]
:type icohp_xlim: list
:param icohp_ylim: Lowest and highest energy values in eV for ICOHP plot, example: [-11, 8.5]
:type icohp_ylim: list
:return: None
:rtype: None
"""
def read_COHP(fn):
raw = open(fn).readlines()
raw = [line for line in raw if "No" not in line][3:]
raw = [[eval(i) for i in line.split()] for line in raw]
return np.array(raw)
data_cohp = read_COHP("./COHPCAR.lobster")
symbols = self.atoms.get_chemical_symbols()
labels_cohp = [
f"{symbols[b[0]]}[{b[0]}]-{symbols[b[1]]}[{b[1]}]"
for b in self.bonds
]
icohp_ef = [
eval(line.split()[-1])
for line in open("./ICOHPLIST.lobster").readlines()[1:]
]
icohp_ef = [i for index,i in enumerate(icohp_ef) if index%2==0]
"""
COHPCAR.lobster has the following format (check LOBSTER documentation):
If ISPIN=1:
Energy (pCOHP averaged over all bonds) (IpCOHP averaged over all bonds) (pCOHP of first bond) (IpCOHP of first bond) ...
... ... ... ... ...
If ISPIN=2:
Energy (Spin up pCOHP averaged over all bonds) (Spin up IpCOHP averaged over all bonds) (Spin up pCOHP of first bond) (Spin up IpCOHP of first bond) ... (Spin down pCOHP averaged over all bonds) (Spin down IpCOHP averaged over all bonds) (Spin down pCOHP of first bond) (Spin down IpCOHP of first bond) ...
... ... ... ... ... ... ... ... ...
"""
assert (
len(labels_cohp) == (data_cohp.shape[1]-3)//2 or len(labels_cohp) == (data_cohp.shape[1]-5)//4
), "Inconsistent bonds definition and COHPCAR.lobster"
if len(labels_cohp) == (data_cohp.shape[1]-3)//2:
spin = 1
data_len = (data_cohp.shape[1]-3)//2
elif len(labels_cohp) == (data_cohp.shape[1]-5)//4:
spin = 2
data_len = ((data_cohp.shape[1]-5)//4)*2
labels_cohp = [i for label in labels_cohp for i in (label, label)]
for i in range(data_len):
fig, ax1 = plt.subplots(figsize=[2.4, 4.8])
if spin==1:
cohp_column = i*2+3
icohp_column = i*2+4
elif spin==2:
if i >= data_len//2:
i = i+1
cohp_column = i*2+3
icohp_column = i*2+4
if i >= data_len//2:
i = i-1
ax1.plot(
-data_cohp[:, cohp_column],
data_cohp[:, 0],
color="k",
label=labels_cohp[i],
)
ax1.fill_betweenx(
data_cohp[:, 0],
-data_cohp[:, cohp_column],
0,
where=-data_cohp[:, cohp_column] >= 0,
facecolor="green",
alpha=0.2,
)
ax1.fill_betweenx(
data_cohp[:, 0],
-data_cohp[:, cohp_column],
0,
where=-data_cohp[:, cohp_column] <= 0,
facecolor="red",
alpha=0.2,
)
ax1.set_ylim(cohp_ylim)
ax1.set_xlim(cohp_xlim)
ax1.set_xlabel("-COHP (eV)", color="k", fontsize="large")
ax1.set_ylabel("$E-E_F$ (eV)", fontsize="large")
ax1.tick_params(axis="x", colors="k")
# ICOHP
ax2 = ax1.twiny()
ax2.plot(-data_cohp[:, icohp_column], data_cohp[:, 0], color="grey", linestyle="dashdot")
ax2.set_ylim(icohp_ylim) # [-10, 6]
ax2.set_xlim(icohp_xlim) # [-0.01, 1.5]
ax2.set_xlabel("-ICOHP (eV)", color="grey", fontsize="large")
ax2.xaxis.tick_top()
ax2.xaxis.set_label_position("top")
ax2.tick_params(axis="x", colors="grey")
# legends
ax1.axvline(0, color="k", linestyle=":", alpha=0.5)
ax1.axhline(0, color="k", linestyle="--", alpha=0.5)
labelx = min(icohp_xlim) + 0.2
labely = min(icohp_ylim) + 0.5
ax2.annotate(
labels_cohp[i],
xy=(labelx, labely),
ha="left",
va="bottom",
bbox=dict(boxstyle="round", fc="w", alpha=0.5),
)
ax2.annotate(
f"{-icohp_ef[i]:.3f}",
xy=(labelx, -0.05),
ha="left",
va="bottom",
color="black",
)
if spin==1:
fig_name = f"{i+1}"
elif spin==2 and i<data_len//2:
fig_name = f"{i//2+1}-up"
elif spin==2 and i>=data_len//2:
fig_name = f"{i//2+1}-down"
fig.savefig(
f"cohp-{fig_name}.png",
dpi=500,
bbox_inches="tight",
transparent=True,
)
plt.close()
class frequency:
"""Performs vibrational analysis on the system using VASP or ASE. Use ASE for calculations involving large systems as it supports a parallel scheme.
"""
def __init__(self, atoms, vib_indices=None):
"""Initializes the frequency class.
:param atoms: Atoms for which vibrational analysis is performed
:type atoms: Atoms object
:param vib_indices: Indices of the atoms to be vibrated, if None, all atoms with Selective Dynamics F are ignored, defaults to None
:type vib_indices: list, optional
"""
self.atoms = atoms
if vib_indices==None:
vib_indices = self.get_vib_indices()
elif vib_indices!=None:
vib_indices = vib_indices
self.vib_indices = vib_indices
def get_vib_indices(self):
"""Returns the indices of the atoms to be vibrated. All atoms with Selective Dynamics F are ignored.
:return: Indices of the atoms to be vibrated
:rtype: list
"""
atoms = self.atoms
constr = atoms.constraints
constr = [c for c in constr if isinstance(c, FixAtoms)]
if constr!=[]:
vib_indices = [a.index for a in atoms if a.index not in constr[0].index]
elif constr==[]:
vib_indices = [a.index for a in atoms]
return vib_indices
def run(self, kpts=None, mode="ase", scheme=None, addnl_settings=None):
"""Runs frequency calculation on the system.
:param kpts: KPOINTS used for the calculation, a list of KPOINTS are to be provided if mode is "vasp" and mode is "ase" with "serial" scheme, defaults to None
:type kpts: list, optional
:param mode: Mode used to run the frequency calculation, supports `mode="ase"` and `mode="vasp"`, defaults to "ase"
:type mode: str, optional
:param scheme: Scheme used to run the frequency calculation, supports `scheme="serial"` and `scheme="parallel"` only when `mode="ase"`, defaults to None
:type scheme: str, optional
:param addnl_settings: Dictionary containing any additional VASP settings (either editing default settings of base_calc or adding more settings), defaults to None
:type addnl_settings: dict, optional
"""
atoms = self.atoms
calc = get_base_calc()
if addnl_settings!=None:
keys = addnl_settings.keys()
for key in keys:
set_vasp_key(calc, key, addnl_settings[key])
if mode == "vasp":
# Avoid this on large structures, use mode="ase" instead.
# ncore/npar unusable, leads to kpoint errors.
# isym must be switched off, leading to large memory usage.
assert kpts!=None, "kpts must be provided when mode is vasp"
calc.set(
kpts=kpts,
ibrion=5,
potim=0.015,
nsw=500,
isym=0,
)
atoms.calc = calc
atoms.get_potential_energy()
elif mode == "ase":
calc.set(kpts=kpts, lwave=True, isym=-1) # according to michael
atoms.calc = calc
vib_indices = self.vib_indices
if scheme == "serial":
assert kpts!=None, "kpts must be provided when mode is ase and scheme is serial"
vib = Vibrations(atoms, indices=vib_indices)
vib.run() # this will save json files
elif scheme == "parallel":
assert os.path.exists("./freq.py"), "freq.py is required to perform parallel calculation, see examples for the structure of freq.py"
assert os.path.exists("./freq.sh"), "freq.sh is required to perform parallel calculation, see examples for the structure of freq.sh"
for indice in vib_indices:
os.mkdir(f"{indice}")
shutil.copyfile("./freq.py", f"./{indice}/freq.py")
shutil.copyfile("./freq.sh", f"./{indice}/freq.sh")
shutil.copyfile("./POSCAR", f"./{indice}/POSCAR")
os.chdir(f"{indice}")
subprocess.run(["sed", "-i", f"s/iii/{str(indice)}/g", "freq.py"])
subprocess.run(["sbatch", "freq.sh"])
os.chdir("../")
def analysis(self, mode, thermo_style, potentialenergy, temperature, pressure=None, copy_json_files=None, **kwargs):
"""Performs analysis after the frequency calculation.
:param mode: Mode used to run the frequency calculation, supports `mode="ase"` and `mode="vasp"`, defaults to "ase"
:type mode: str
:param thermo_style: The class used from the thermochemistry module of ASE, for surfaces, use `thermo_style="Harmonic"` and for gases, use `thermo_style="IdealGas"`
:type thermo_style: str
:param potentialenergy: Potential energy of the system from geo_opt calculation
:type potentialenergy: float
:param temperature: Temperature at which the analysis is performed
:type temperature: float
:param pressure: Pressure at which the analysis is performed, defaults to None
:type pressure: float, optional
:param copy_json_files: Copies .json files from invidual atom's index directory to a common vib folder, True only if `scheme="parallel"` in the run method, defaults to None
:type copy_json_files: bool, optional
:return: _description_
:rtype: _type_
"""
atoms = self.atoms
if mode=="ase":
vib_indices = self.vib_indices
vib = Vibrations(atoms, indices=vib_indices)
if copy_json_files==True:
for indice in vib_indices:
os.chdir(f"./{indice}")
copy_tree("./vib", "../vib")
os.chdir("../")
vib.run()
vib.summary(log="vibrations.txt")
vib_energies = vib.get_energies()
vib_energies = np.array([i for i in vib_energies if i.imag==0])
elif mode=="vasp":
with open("OUTCAR", "r") as f:
vib_energies = np.array([])
for line in f:
match = re.search(r'\b\d+\.\d+\s+meV\b', line)
if match and "f/i" not in line:
value = float(match.group().split()[0])*(10**-3)
vib_energies = np.append(vib_energies,value)
if thermo_style=="Harmonic":
thermo = HarmonicThermo(vib_energies = vib_energies, potentialenergy = potentialenergy)
S = thermo.get_entropy(temperature)
F = thermo.get_helmholtz_energy(temperature)
U = thermo.get_internal_energy(temperature)
return S,F,U
elif thermo_style=="IdealGas":
# In older versions of ASE, the vib_energies array is not sorted internally in the IdealGas class.
# The first 5 (if geometry==linear, only 3N-5 modes should be considered) or
# 6 (if geometry==nonlinear, only 3N-6 modes should be considered) frequency modes are not considered towards the calculation of gibbs free energy.
# If the frequency modes were obtained from external source (and not from ASE's Vibration class which provides sorted vib_energies),
# there is a good chance that the vib_energies is not sorted, and important modes are removed from the analysis.
# Sorting is done here to prevent this!
vib_energies = np.sort(vib_energies)
assert pressure!=None, "pressure must be provided when mode is IdealGas"
assert kwargs!=None, "geometry, symmetry number and spin must be provided when mode is IdealGas"
thermo = IdealGasThermo(vib_energies = vib_energies, potentialenergy = potentialenergy, atoms = atoms, **kwargs)
H = thermo.get_enthalpy(temperature)
S = thermo.get_entropy(temperature, pressure)
G = thermo.get_gibbs_energy(temperature, pressure)
return H,S,G
def check_vib_files(self):
"""Checks if the .json files are present in the individual atom's index directory. This method only works for `mode="ase"` with `scheme="parallel"`.
"""
f = open("file_size.txt", "w")
vib_indices = self.vib_indices
for indice in vib_indices:
try:
os.chdir(f"./{indice}/vib")
except FileNotFoundError:
assert False, f"vib folder not found in {indice}. Make sure you ran a parallel frequency calculation with ase before using this method."
files = os.listdir()
for file in files:
f.write(str(indice) + " " + str(os.path.getsize(file)) + "\n")
os.chdir("../../")
class surface_charging:
"""Performs surface charging calculation using VASPsol.
"""
def __init__(self):
pass
# Parsing the OUTCAR to get NELECT.
def get_PZC_nelect(self):
os.chdir("PZC_calc")
with open('OUTCAR', 'r') as f:
for line in f:
match = re.search(r'NELECT\s+=\s+(\d+\.\d+)', line)
if match:
PZC_nelect = match.group(1)
break
os.chdir("../")
return float(PZC_nelect)
"""
Example:
If PZC_nelect = 40, n_nelect = 4 and width_nelect = 0.25, the values of nelect will be [39.50, 39.75, 40.25, 40.50].
**kwargs are used to get the arguments of the symmetrize function.
"""
def run(self, atoms, opt_levels, n_nelect=None, width_nelect=None, custom_nelect=None, symmetrize_function=None, **kwargs):
"""Runs a surface charging calculation.
:param atoms: Atoms used for surface charging calculation
:type atoms: Atoms object
:param opt_levels: Dictionary of dictionaries, each dictionary containing settings for each level of calculation
:type opt_levels: dict
:param n_nelect: Number of nelects, use this argument when PZC_nelect is not known defaults to None
:type n_nelect: int, optional
:param width_nelect: Difference between each nelect, use this argument along with n_nelect when PZC_nelect is not known, defaults to None
:type width_nelect: float, optional
:param custom_nelect: List of custom nelects, defaults to None
:type custom_nelect: list, optional
:param symmetrize_function: Function used to symmetrize atoms, defaults to None
:type symmetrize_function: function, optional
"""
if symmetrize_function!=None:
atoms = symmetrize_function(atoms, **kwargs)
write("POSCAR_sym", atoms)
# Running a neutral solvation (PZC) calculation.
PZC_calc = False
if os.path.exists(f"./POSCAR_solvated"):
PZC_calc = True
if PZC_calc==False:
try:
os.mkdir(f"PZC_calc")
except FileExistsError:
pass
os.chdir("PZC_calc")
geo_opt(atoms, mode="vasp", opt_levels=opt_levels, restart=True)
shutil.copyfile("CONTCAR", "../POSCAR_solvated")
os.chdir("../")
assert not (n_nelect==None and width_nelect==None and custom_nelect==None), "Either provide n_nelect and width_nelect or custom_nelect."
if n_nelect!=None and width_nelect!=None:
PZC_nelect = self.get_PZC_nelect()
n_nelect = int(n_nelect/2)
nelect = np.arange(PZC_nelect-n_nelect*width_nelect, PZC_nelect+n_nelect*width_nelect+width_nelect, width_nelect)
nelect = np.delete(nelect,n_nelect) # Excluding PZC_nelect.
elif custom_nelect!=None:
nelect = custom_nelect
for i in nelect:
try:
os.mkdir(f"{i}")
except FileExistsError:
pass
shutil.copyfile("POSCAR_solvated", f"./{i}/POSCAR")
shutil.copyfile("geo_opt.py", f"./{i}/geo_opt.py")
shutil.copyfile("optimize.sh", f"./{i}/optimize.sh")
os.chdir(f"{i}")
subprocess.run(["sed", "-i", f"s/nnn/{str(i)}/g", "geo_opt.py"])
subprocess.run(["sbatch", "optimize.sh"])
os.chdir("../")
def parse_output(self):
"""Parses the output of the surface charging calculation to get the energy, Fermi energy and Fermi shift.
:return: Energy, Fermi energy and Fermi shift of the calculation
:rtype: tuple
"""
class ParseError(Exception):
def __init__(self,message):
self.message = message
with open("OUTCAR", "r") as f:
outcar = f.readlines()
outcar = "".join(outcar)
# Parsing the OUTCAR to get the energy of the system.
try:
e_pattern = re.compile(r'entropy=\s+-?\d+.\d+\s+energy\(sigma->0\)\s+=\s+(-?\d+\.\d+)')
e = re.findall(e_pattern, outcar)[-1]
e = float(e)
except:
raise ParseError("Energy not found in OUTCAR")
# Parsing the OUTCAR to get Fermi energy.
try:
e_fermi_pattern = re.compile(r'E-fermi\s+:\s+(\-\d+\.\d+)')
e_fermi = re.findall(e_fermi_pattern, outcar)[-1]
e_fermi = float(e_fermi)
except:
raise ParseError("Fermi energy not found in OUTCAR")
with open("vasp.out", "r") as f:
vaspout = f.readlines()
vaspout = "".join(vaspout)
# Parsing vasp.out to get Fermi Shift.
try:
fermi_shift_pattern = re.compile(r"FERMI_SHIFT =\s+([\d.E+-]+)")
fermi_shift_array = re.findall(fermi_shift_pattern, vaspout)
fermi_shift_array = [float(i) for i in fermi_shift_array if float(i)!=0]
fermi_shift = fermi_shift_array[-1]
except:
raise ParseError("Fermi shift not found in vasp.out")
return e, e_fermi, fermi_shift
def plot_parabola(self, PZC_nelect, custom_nelect=None):
"""Generates the energy vs potential plot.
:param PZC_nelect: Number of electrons at PZC
:type PZC_nelect: float
:param custom_nelect: List of custom nelects, defaults to None
:type custom_nelect: list, optional
:return: Figure object to edit the figure
:rtype: Matplotlib figure object
"""
def isfloat(value):
try:
float(value)
return True
except ValueError:
return False
if custom_nelect==None:
subdirs = [f.path for f in os.scandir("./") if f.is_dir()]
subdirs = map(lambda x:re.sub('./', '', x), subdirs)
subdirs = [subdir for subdir in subdirs if isfloat(subdir)]
elif custom_nelect:
subdirs = custom_nelect
n_elects, es, e_fermis, fermi_shifts = np.array([]), np.array([]), np.array([]), np.array([])
for dir in subdirs:
os.chdir(f"./{dir}")
e, e_fermi, fermi_shift = self.parse_output()
n_elects = np.append(n_elects, float(dir))
es = np.append(es, e)
e_fermis = np.append(e_fermis, e_fermi)
fermi_shifts = np.append(fermi_shifts, fermi_shift)
os.chdir("../")
vacpot = 4.44
work_functions = -e_fermis - fermi_shifts
SHEpots = work_functions - vacpot
es = es + fermi_shifts*(n_elects - PZC_nelect)
gs = es + work_functions*(n_elects - PZC_nelect)
Lipots = np.array([SHEpot+3.04 for SHEpot in SHEpots])
f = open("data.txt","w")
for n_e,V,G in zip(n_elects,Lipots,gs):
f.write("NELECT = " + str(n_e) + "\n")
f.write("PotvsLi+/Li = "+ str(V) + "\n")
f.write("G = " + str(G) + "\n")
f.write("\n")
f.close()
quadratic = lambda x, a, b, c: a * x ** 2 + b * x + c
parameter, _ = curve_fit(quadratic, Lipots, gs)
with open("fit.txt", "w") as f:
for i in parameter:
f.write(f"{i}\n")
f.close()
fitted = lambda x: parameter[0] * x ** 2 + parameter[1] * x + parameter[2]
fig = plt.figure(dpi = 200, figsize=(6,5))
x = np.linspace(Lipots.min()-0.2, Lipots.max()+0.2, 100)
y = list(map(fitted, x))
plt.title('a={}\nb={}\nc={}'.format(*parameter))
plt.scatter(Lipots, gs, label='original data', color='indigo')
plt.plot(x, y, label='fitted', color='indigo')
for i, txt in enumerate(n_elects):
plt.annotate(txt, (Lipots[i]+Lipots.max()/50, gs[i]))
get_plot_settings(fig,"U vs Li+/Li (V)","G (eV)","g-pot.png","upper left")
return fig
def analysis(self, custom_nelect=None):
"""Generates the energy vs potential plot.
"""
pwd = os.getcwd()
PZC_nelect = self.get_PZC_nelect()
try:
os.rename("PZC_calc", f"{PZC_nelect}")
self.plot_parabola(PZC_nelect, custom_nelect=custom_nelect)
os.rename(f"{PZC_nelect}", "PZC_calc")
except Exception as error:
os.chdir(pwd)
os.rename(f"{PZC_nelect}", "PZC_calc")
raise error
class gibbs_free_energy:
"""Gives the gibbs free energy of the system. If surface_charging is used, the parabola fit is used to obtain the energy vs potential. If geo_opt is used, OUTCAR is used to obtain energy. The vibrational energy is obtained using the frequency class. Note: Only works if ASE is used to run the frequency calculation.
"""
def __init__(self, calc_root_dir):
"""Initializes the gibbs_free_energy class.
:param calc_root_dir: Root directory of the calculation
:type calc_root_dir: string
"""
self.calc_root_dir = calc_root_dir
def get_parabola(self):
"""Fits a parabola to the energy vs potential plot using analysis.py.
:return: Coefficients of the parabola