23
23
from tempfile import TemporaryDirectory
24
24
25
25
import numpy as np
26
- from monty .dev import requires
27
26
from monty .io import zopen
28
27
29
28
from pymatgen .io .common import VolumetricData
37
36
__status__ = "Beta"
38
37
__date__ = "4/5/13"
39
38
39
+
40
40
BADEREXE = which ("bader" ) or which ("bader.exe" )
41
41
42
42
43
43
class BaderAnalysis :
44
44
"""
45
- Bader analysis for Cube files and VASP outputs.
46
-
47
- .. attribute: data
48
-
49
- Atomic data parsed from bader analysis. Essentially a list of dicts
50
- of the form::
51
-
52
- [
53
- {
54
- "atomic_vol": 8.769,
55
- "min_dist": 0.8753,
56
- "charge": 7.4168,
57
- "y": 1.1598,
58
- "x": 0.0079,
59
- "z": 0.8348
60
- },
61
- ...
62
- ]
63
-
64
- .. attribute: vacuum_volume
65
-
66
- Vacuum volume of the Bader analysis.
67
-
68
- .. attribute: vacuum_charge
69
-
70
- Vacuum charge of the Bader analysis.
71
-
72
- .. attribute: nelectrons
73
-
74
- Number of electrons of the Bader analysis.
75
-
76
- .. attribute: chgcar
77
-
78
- Chgcar object associated with input CHGCAR file.
79
-
80
- .. attribute: atomic_densities
81
-
82
- list of charge densities for each atom centered on the atom
83
- excess 0's are removed from the array to reduce the size of the array
84
- the charge densities are dicts with the charge density map,
85
- the shift vector applied to move the data to the center, and the original dimension of the charge density map
86
- charge:
87
- {
88
- "data": charge density array
89
- "shift": shift used to center the atomic charge density
90
- "dim": dimension of the original charge density map
91
- }
45
+ Performs Bader analysis for Cube files and VASP outputs.
46
+
47
+ Attributes:
48
+ data (list[dict]): Atomic data parsed from bader analysis. Each dictionary in the list has the keys:
49
+ "atomic_vol", "min_dist", "charge", "x", "y", "z".
50
+ vacuum_volume (float): Vacuum volume of the Bader analysis.
51
+ vacuum_charge (float): Vacuum charge of the Bader analysis.
52
+ nelectrons (int): Number of electrons of the Bader analysis.
53
+ chgcar (Chgcar): Chgcar object associated with input CHGCAR file.
54
+ atomic_densities (list[dict]): List of charge densities for each atom centered on the atom.
55
+ Excess 0's are removed from the array to reduce its size. Each dictionary has the keys:
56
+ "data", "shift", "dim", where "data" is the charge density array,
57
+ "shift" is the shift used to center the atomic charge density, and
58
+ "dim" is the dimension of the original charge density map.
92
59
"""
93
60
94
- @requires (
95
- which ("bader" ) or which ("bader.exe" ),
96
- "BaderAnalysis requires the executable bader to be in the path."
97
- " Please download the library at http://theory.cm.utexas"
98
- ".edu/vasp/bader/ and compile the executable." ,
99
- )
100
61
def __init__ (
101
62
self ,
102
63
chgcar_filename = None ,
103
64
potcar_filename = None ,
104
65
chgref_filename = None ,
105
66
parse_atomic_densities = False ,
106
67
cube_filename = None ,
68
+ bader_exe_path : str | None = BADEREXE ,
107
69
):
108
70
"""
109
71
Initializes the Bader caller.
@@ -112,16 +74,18 @@ def __init__(
112
74
chgcar_filename (str): The filename of the CHGCAR.
113
75
potcar_filename (str): The filename of the POTCAR.
114
76
chgref_filename (str): The filename of the reference charge density.
115
- parse_atomic_densities (bool): Optional. turns on atomic partition of the charge density
77
+ parse_atomic_densities (bool, optional): turns on atomic partition of the charge density
116
78
charge densities are atom centered
117
- cube_filename (str): Optional. The filename of the cube file.
79
+ cube_filename (str, optional): The filename of the cube file.
80
+ bader_exe_path (str, optional): The path to the bader executable.
118
81
"""
119
- if not BADEREXE :
82
+ if not BADEREXE and not os . path . isfile ( bader_exe_path or "" ) :
120
83
raise RuntimeError (
121
- "BaderAnalysis requires the executable bader to be in the path. "
122
- " Please download the library at http://theory.cm.utexas "
123
- ". edu/vasp/bader/ and compile the executable ."
84
+ "BaderAnalysis requires the executable bader be in the PATH or the full path "
85
+ f"to the binary to be specified via { bader_exe_path = } . Download the binary at "
86
+ "https://theory.cm.utexas. edu/henkelman/code/bader ."
124
87
)
88
+ assert isinstance (BADEREXE , str ) # mypy type narrowing
125
89
126
90
if not (cube_filename or chgcar_filename ):
127
91
raise ValueError ("You must provide either a cube file or a CHGCAR" )
@@ -136,7 +100,7 @@ def __init__(
136
100
self .structure = self .chgcar .structure
137
101
self .potcar = Potcar .from_file (potcar_filename ) if potcar_filename is not None else None
138
102
self .natoms = self .chgcar .poscar .natoms
139
- chgrefpath = os .path .abspath (chgref_filename ) if chgref_filename else None
103
+ chgref_path = os .path .abspath (chgref_filename ) if chgref_filename else None
140
104
self .reference_used = bool (chgref_filename )
141
105
142
106
# List of nelects for each atom from potcar
@@ -152,16 +116,16 @@ def __init__(
152
116
self .is_vasp = False
153
117
self .cube = VolumetricData .from_cube (fpath )
154
118
self .structure = self .cube .structure
155
- self .nelects = None
156
- chgrefpath = os .path .abspath (chgref_filename ) if chgref_filename else None
119
+ self .nelects = None # type: ignore
120
+ chgref_path = os .path .abspath (chgref_filename ) if chgref_filename else None
157
121
self .reference_used = bool (chgref_filename )
158
122
159
123
tmpfile = "CHGCAR" if chgcar_filename else "CUBE"
160
124
with zopen (fpath , "rt" ) as f_in , open (tmpfile , "w" ) as f_out :
161
125
shutil .copyfileobj (f_in , f_out )
162
- args = [BADEREXE , tmpfile ]
126
+ args : list [ str ] = [BADEREXE , tmpfile ]
163
127
if chgref_filename :
164
- with zopen (chgrefpath , "rt" ) as f_in , open ("CHGCAR_ref" , "w" ) as f_out :
128
+ with zopen (chgref_path , "rt" ) as f_in , open ("CHGCAR_ref" , "w" ) as f_out :
165
129
shutil .copyfileobj (f_in , f_out )
166
130
args += ["-ref" , "CHGCAR_ref" ]
167
131
if parse_atomic_densities :
@@ -224,35 +188,35 @@ def __init__(
224
188
shift = (np .divide (chg .dim , 2 ) - index ).astype (int )
225
189
226
190
# Shift the data so that the atomic charge density to the center for easier manipulation
227
- shifted_data = np .roll (data , shift , axis = (0 , 1 , 2 ))
191
+ shifted_data = np .roll (data , shift , axis = (0 , 1 , 2 )) # type: ignore
228
192
229
193
# Slices a central window from the data array
230
- def slice_from_center (data , xwidth , ywidth , zwidth ):
194
+ def slice_from_center (data , x_width , y_width , z_width ):
231
195
x , y , z = data .shape
232
- startx = x // 2 - (xwidth // 2 )
233
- starty = y // 2 - (ywidth // 2 )
234
- startz = z // 2 - (zwidth // 2 )
196
+ start_x = x // 2 - (x_width // 2 )
197
+ start_y = y // 2 - (y_width // 2 )
198
+ start_z = z // 2 - (z_width // 2 )
235
199
return data [
236
- startx : startx + xwidth ,
237
- starty : starty + ywidth ,
238
- startz : startz + zwidth ,
200
+ start_x : start_x + x_width ,
201
+ start_y : start_y + y_width ,
202
+ start_z : start_z + z_width ,
239
203
]
240
204
241
205
# Finds the central encompassing volume which holds all the data within a precision
242
206
def find_encompassing_vol (data ):
243
207
total = np .sum (data )
244
- for i in range (np .max (data .shape )):
245
- sliced_data = slice_from_center (data , i , i , i )
208
+ for idx in range (np .max (data .shape )):
209
+ sliced_data = slice_from_center (data , idx , idx , idx )
246
210
if total - np .sum (sliced_data ) < 0.1 :
247
211
return sliced_data
248
212
return None
249
213
250
- d = {
214
+ dct = {
251
215
"data" : find_encompassing_vol (shifted_data ),
252
216
"shift" : shift ,
253
217
"dim" : self .chgcar .dim ,
254
218
}
255
- atomic_densities .append (d )
219
+ atomic_densities .append (dct )
256
220
self .atomic_densities = atomic_densities
257
221
258
222
def get_charge (self , atom_index ):
@@ -519,56 +483,61 @@ def bader_analysis_from_objects(chgcar, potcar=None, aeccar0=None, aeccar2=None)
519
483
:param aeccar2: (optional) Chgcar object from aeccar2 file
520
484
:return: summary dict
521
485
"""
522
- with TemporaryDirectory () as tmp_dir :
523
- if aeccar0 and aeccar2 :
524
- # construct reference file
525
- chgref = aeccar0 .linear_add (aeccar2 )
526
- chgref_path = os .path .join (tmp_dir , "CHGCAR_ref" )
527
- chgref .write_file (chgref_path )
528
- else :
529
- chgref_path = None
530
-
531
- chgcar .write_file ("CHGCAR" )
532
- chgcar_path = os .path .join (tmp_dir , "CHGCAR" )
533
-
534
- if potcar :
535
- potcar .write_file ("POTCAR" )
536
- potcar_path = os .path .join (tmp_dir , "POTCAR" )
537
- else :
538
- potcar_path = None
539
-
540
- ba = BaderAnalysis (
541
- chgcar_filename = chgcar_path ,
542
- potcar_filename = potcar_path ,
543
- chgref_filename = chgref_path ,
544
- )
545
-
546
- summary = {
547
- "min_dist" : [d ["min_dist" ] for d in ba .data ],
548
- "charge" : [d ["charge" ] for d in ba .data ],
549
- "atomic_volume" : [d ["atomic_vol" ] for d in ba .data ],
550
- "vacuum_charge" : ba .vacuum_charge ,
551
- "vacuum_volume" : ba .vacuum_volume ,
552
- "reference_used" : bool (chgref_path ),
553
- "bader_version" : ba .version ,
554
- }
486
+ orig_dir = os .getcwd ()
487
+ try :
488
+ with TemporaryDirectory () as tmp_dir :
489
+ os .chdir (tmp_dir )
490
+ if aeccar0 and aeccar2 :
491
+ # construct reference file
492
+ chgref = aeccar0 .linear_add (aeccar2 )
493
+ chgref_path = os .path .join (tmp_dir , "CHGCAR_ref" )
494
+ chgref .write_file (chgref_path )
495
+ else :
496
+ chgref_path = None
555
497
556
- if potcar :
557
- charge_transfer = [ba .get_charge_transfer (i ) for i in range (len (ba .data ))]
558
- summary ["charge_transfer" ] = charge_transfer
498
+ chgcar .write_file ("CHGCAR" )
499
+ chgcar_path = os .path .join (tmp_dir , "CHGCAR" )
559
500
560
- if chgcar . is_spin_polarized :
561
- # write a CHGCAR containing magnetization density only
562
- chgcar . data [ "total" ] = chgcar . data [ "diff" ]
563
- chgcar . is_spin_polarized = False
564
- chgcar . write_file ( "CHGCAR_mag" )
501
+ if potcar :
502
+ potcar . write_file ( "POTCAR" )
503
+ potcar_path = os . path . join ( tmp_dir , "POTCAR" )
504
+ else :
505
+ potcar_path = None
565
506
566
- chgcar_mag_path = os .path .join (tmp_dir , "CHGCAR_mag" )
567
507
ba = BaderAnalysis (
568
- chgcar_filename = chgcar_mag_path ,
508
+ chgcar_filename = chgcar_path ,
569
509
potcar_filename = potcar_path ,
570
510
chgref_filename = chgref_path ,
571
511
)
572
- summary ["magmom" ] = [d ["charge" ] for d in ba .data ]
573
512
574
- return summary
513
+ summary = {
514
+ "min_dist" : [d ["min_dist" ] for d in ba .data ],
515
+ "charge" : [d ["charge" ] for d in ba .data ],
516
+ "atomic_volume" : [d ["atomic_vol" ] for d in ba .data ],
517
+ "vacuum_charge" : ba .vacuum_charge ,
518
+ "vacuum_volume" : ba .vacuum_volume ,
519
+ "reference_used" : bool (chgref_path ),
520
+ "bader_version" : ba .version ,
521
+ }
522
+
523
+ if potcar :
524
+ charge_transfer = [ba .get_charge_transfer (i ) for i in range (len (ba .data ))]
525
+ summary ["charge_transfer" ] = charge_transfer
526
+
527
+ if chgcar .is_spin_polarized :
528
+ # write a CHGCAR containing magnetization density only
529
+ chgcar .data ["total" ] = chgcar .data ["diff" ]
530
+ chgcar .is_spin_polarized = False
531
+ chgcar .write_file ("CHGCAR_mag" )
532
+
533
+ chgcar_mag_path = os .path .join (tmp_dir , "CHGCAR_mag" )
534
+ ba = BaderAnalysis (
535
+ chgcar_filename = chgcar_mag_path ,
536
+ potcar_filename = potcar_path ,
537
+ chgref_filename = chgref_path ,
538
+ )
539
+ summary ["magmom" ] = [d ["charge" ] for d in ba .data ]
540
+ finally :
541
+ os .chdir (orig_dir )
542
+
543
+ return summary
0 commit comments