Skip to content

Commit 0b7607a

Browse files
authored
Add bader_exe_path keyword to BaderAnalysis and run bader tests in CI (#3191)
* allow passing explicit file path to class BaderAnalysis via bader_exe_path * better implementation * add test_missing_file_bader_exe_path * try installing bader exe in linux CI * fix wget url * google-style doc str * fix FileNotFoundError: [Errno 2] No such file or directory: '/tmp/tmpad1zkrcz/CHGCAR' in def test_automatic_runner(self): test_dir = os.path.join(PymatgenTest.TEST_FILES_DIR, "bader") > summary = bader_analysis_from_path(test_dir) * bader_analysis_from_objects chdir backto original dir * fix two remaining failing tests * del print statements
1 parent 5637764 commit 0b7607a

File tree

4 files changed

+123
-137
lines changed

4 files changed

+123
-137
lines changed

.github/workflows/test.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,13 @@ jobs:
7878
for pkg in cmd_line/*;
7979
do echo "$(pwd)/cmd_line/$pkg/Linux_64bit" >> "$GITHUB_PATH";
8080
done
81+
- name: Install Bader
82+
if: runner.os == 'Linux'
83+
run: |
84+
wget http://theory.cm.utexas.edu/henkelman/code/bader/download/bader_lnx_64.tar.gz
85+
tar xvzf bader_lnx_64.tar.gz
86+
sudo mv bader /usr/local/bin/
87+
continue-on-error: true
8188
- name: Install dependencies
8289
run: |
8390
python -m pip install --upgrade pip wheel

pymatgen/command_line/bader_caller.py

Lines changed: 91 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
from tempfile import TemporaryDirectory
2424

2525
import numpy as np
26-
from monty.dev import requires
2726
from monty.io import zopen
2827

2928
from pymatgen.io.common import VolumetricData
@@ -37,73 +36,36 @@
3736
__status__ = "Beta"
3837
__date__ = "4/5/13"
3938

39+
4040
BADEREXE = which("bader") or which("bader.exe")
4141

4242

4343
class BaderAnalysis:
4444
"""
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.
9259
"""
9360

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-
)
10061
def __init__(
10162
self,
10263
chgcar_filename=None,
10364
potcar_filename=None,
10465
chgref_filename=None,
10566
parse_atomic_densities=False,
10667
cube_filename=None,
68+
bader_exe_path: str | None = BADEREXE,
10769
):
10870
"""
10971
Initializes the Bader caller.
@@ -112,16 +74,18 @@ def __init__(
11274
chgcar_filename (str): The filename of the CHGCAR.
11375
potcar_filename (str): The filename of the POTCAR.
11476
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
11678
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.
11881
"""
119-
if not BADEREXE:
82+
if not BADEREXE and not os.path.isfile(bader_exe_path or ""):
12083
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."
12487
)
88+
assert isinstance(BADEREXE, str) # mypy type narrowing
12589

12690
if not (cube_filename or chgcar_filename):
12791
raise ValueError("You must provide either a cube file or a CHGCAR")
@@ -136,7 +100,7 @@ def __init__(
136100
self.structure = self.chgcar.structure
137101
self.potcar = Potcar.from_file(potcar_filename) if potcar_filename is not None else None
138102
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
140104
self.reference_used = bool(chgref_filename)
141105

142106
# List of nelects for each atom from potcar
@@ -152,16 +116,16 @@ def __init__(
152116
self.is_vasp = False
153117
self.cube = VolumetricData.from_cube(fpath)
154118
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
157121
self.reference_used = bool(chgref_filename)
158122

159123
tmpfile = "CHGCAR" if chgcar_filename else "CUBE"
160124
with zopen(fpath, "rt") as f_in, open(tmpfile, "w") as f_out:
161125
shutil.copyfileobj(f_in, f_out)
162-
args = [BADEREXE, tmpfile]
126+
args: list[str] = [BADEREXE, tmpfile]
163127
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:
165129
shutil.copyfileobj(f_in, f_out)
166130
args += ["-ref", "CHGCAR_ref"]
167131
if parse_atomic_densities:
@@ -224,35 +188,35 @@ def __init__(
224188
shift = (np.divide(chg.dim, 2) - index).astype(int)
225189

226190
# 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
228192

229193
# 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):
231195
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)
235199
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,
239203
]
240204

241205
# Finds the central encompassing volume which holds all the data within a precision
242206
def find_encompassing_vol(data):
243207
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)
246210
if total - np.sum(sliced_data) < 0.1:
247211
return sliced_data
248212
return None
249213

250-
d = {
214+
dct = {
251215
"data": find_encompassing_vol(shifted_data),
252216
"shift": shift,
253217
"dim": self.chgcar.dim,
254218
}
255-
atomic_densities.append(d)
219+
atomic_densities.append(dct)
256220
self.atomic_densities = atomic_densities
257221

258222
def get_charge(self, atom_index):
@@ -519,56 +483,61 @@ def bader_analysis_from_objects(chgcar, potcar=None, aeccar0=None, aeccar2=None)
519483
:param aeccar2: (optional) Chgcar object from aeccar2 file
520484
:return: summary dict
521485
"""
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
555497

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")
559500

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
565506

566-
chgcar_mag_path = os.path.join(tmp_dir, "CHGCAR_mag")
567507
ba = BaderAnalysis(
568-
chgcar_filename=chgcar_mag_path,
508+
chgcar_filename=chgcar_path,
569509
potcar_filename=potcar_path,
570510
chgref_filename=chgref_path,
571511
)
572-
summary["magmom"] = [d["charge"] for d in ba.data]
573512

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

Comments
 (0)