Skip to content

Commit aa08052

Browse files
Fix branch directory check in io.vasp.outputs.get_band_structure_from_vasp_multiple_branches (#4061)
* docstring tweak and support Path * isfile -> isdir fix * raise error if vasprun.xml missing in any branch dir * sort pymatgen core imports * add some unit test * raise error if no vasprun.xml file found at all * deprecation warning for fall back branch * remove unused ignore tag * add a temporary solution * update inherit_incar docstring * remove scratch dir * extend deprecation deadline * drop unsure test --------- Co-authored-by: Matt Horton <[email protected]>
1 parent 7e7ba75 commit aa08052

File tree

5 files changed

+73
-40
lines changed

5 files changed

+73
-40
lines changed

docs/index.md

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/pymatgen/electronic_structure/bandstructure.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1084,7 +1084,7 @@ def get_projections_on_elements_and_orbitals(
10841084

10851085

10861086
@overload
1087-
def get_reconstructed_band_structure( # type: ignore[overload-overlap]
1087+
def get_reconstructed_band_structure(
10881088
list_bs: list[BandStructure],
10891089
efermi: float | None = None,
10901090
) -> BandStructure:

src/pymatgen/io/vasp/outputs.py

Lines changed: 38 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4535,58 +4535,62 @@ class VaspParseError(ParseError):
45354535

45364536

45374537
def get_band_structure_from_vasp_multiple_branches(
4538-
dir_name: str,
4538+
dir_name: PathLike,
45394539
efermi: float | None = None,
45404540
projections: bool = False,
45414541
) -> BandStructureSymmLine | BandStructure | None:
45424542
"""Get band structure info from a VASP directory.
45434543
4544-
It takes into account that a run can be divided in several branches named
4545-
"branch_x". If the run has not been divided in branches the method will
4546-
turn to parsing vasprun.xml directly.
4544+
It takes into account that a run can be divided in several branches,
4545+
each inside a directory named "branch_x". If the run has not been
4546+
divided in branches the function will turn to parse vasprun.xml
4547+
directly from the selected directory.
45474548
45484549
Args:
4549-
dir_name: Directory containing all bandstructure runs.
4550-
efermi: Efermi for bandstructure.
4551-
projections: True if you want to get the data on site projections if
4552-
any. Note that this is sometimes very large
4550+
dir_name (PathLike): Parent directory containing all bandstructure runs.
4551+
efermi (float): Fermi level for bandstructure.
4552+
projections (bool): True if you want to get the data on site
4553+
projections if any. Note that this is sometimes very large
45534554
45544555
Returns:
4555-
A BandStructure Object.
4556-
None is there's a parsing error.
4556+
A BandStructure/BandStructureSymmLine Object.
4557+
None if no vasprun.xml found in given directory and branch directory.
45574558
"""
4558-
# TODO: Add better error handling!!!
4559-
if os.path.isfile(f"{dir_name}/branch_0"):
4560-
# Get all branch dir names
4559+
if os.path.isdir(f"{dir_name}/branch_0"):
4560+
# Get and sort all branch directories
45614561
branch_dir_names = [os.path.abspath(d) for d in glob(f"{dir_name}/branch_*") if os.path.isdir(d)]
4562-
4563-
# Sort by the directory name (e.g, branch_10)
45644562
sorted_branch_dir_names = sorted(branch_dir_names, key=lambda x: int(x.split("_")[-1]))
45654563

4566-
# Populate branches with Bandstructure instances
4567-
branches = []
4568-
for dname in sorted_branch_dir_names:
4569-
xml_file = f"{dname}/vasprun.xml"
4570-
if os.path.isfile(xml_file):
4571-
run = Vasprun(xml_file, parse_projected_eigen=projections)
4572-
branches.append(run.get_band_structure(efermi=efermi))
4573-
else:
4574-
# TODO: It might be better to throw an exception
4575-
warnings.warn(
4576-
f"Skipping {dname}. Unable to find {xml_file}",
4577-
stacklevel=2,
4578-
)
4564+
# Collect BandStructure from all branches
4565+
bs_branches: list[BandStructure | BandStructureSymmLine] = []
4566+
for directory in sorted_branch_dir_names:
4567+
vasprun_file = f"{directory}/vasprun.xml"
4568+
if not os.path.isfile(vasprun_file):
4569+
raise FileNotFoundError(f"cannot find vasprun.xml in {directory=}")
45794570

4580-
return get_reconstructed_band_structure(branches, efermi)
4571+
run = Vasprun(vasprun_file, parse_projected_eigen=projections)
4572+
bs_branches.append(run.get_band_structure(efermi=efermi))
45814573

4582-
xml_file = f"{dir_name}/vasprun.xml"
4583-
# Better handling of Errors
4584-
if os.path.isfile(xml_file):
4585-
return Vasprun(xml_file, parse_projected_eigen=projections).get_band_structure(
4574+
return get_reconstructed_band_structure(bs_branches, efermi)
4575+
4576+
# Read vasprun.xml directly if no branch head (branch_0) is found
4577+
# TODO: remove this branch and raise error directly after 2026-06-01
4578+
vasprun_file = f"{dir_name}/vasprun.xml"
4579+
if os.path.isfile(vasprun_file):
4580+
warnings.warn(
4581+
(
4582+
f"no branch dir found, reading directly from {dir_name=}\n"
4583+
"this fallback branch would be removed after 2026-06-01\n"
4584+
"please check your data dir or use Vasprun.get_band_structure directly"
4585+
),
4586+
DeprecationWarning,
4587+
stacklevel=2,
4588+
)
4589+
return Vasprun(vasprun_file, parse_projected_eigen=projections).get_band_structure(
45864590
kpoints_filename=None, efermi=efermi
45874591
)
45884592

4589-
return None
4593+
raise FileNotFoundError(f"failed to find any vasprun.xml in selected {dir_name=}")
45904594

45914595

45924596
class Xdatcar:

src/pymatgen/io/vasp/sets.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,10 +181,11 @@ class VaspInputSet(InputGenerator, abc.ABC):
181181
Curtarolo) for monoclinic. Defaults True.
182182
validate_magmom (bool): Ensure that the missing magmom values are filled in with
183183
the VASP default value of 1.0.
184-
inherit_incar (bool): Whether to inherit INCAR settings from previous
184+
inherit_incar (bool | list[str]): Whether to inherit INCAR settings from previous
185185
calculation. This might be useful to port Custodian fixes to child jobs but
186186
can also be dangerous e.g. when switching from GGA to meta-GGA or relax to
187187
static jobs. Defaults to True.
188+
Can also be a list of strings to specify which parameters are inherited.
188189
auto_kspacing (bool): If true, determines the value of KSPACING from the bandgap
189190
of a previous calculation.
190191
auto_ismear (bool): If true, the values for ISMEAR and SIGMA will be set

tests/io/vasp/test_outputs.py

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from pymatgen.core import Element
2020
from pymatgen.core.lattice import Lattice
2121
from pymatgen.core.structure import Structure
22-
from pymatgen.electronic_structure.bandstructure import BandStructureSymmLine
22+
from pymatgen.electronic_structure.bandstructure import BandStructure, BandStructureSymmLine
2323
from pymatgen.electronic_structure.core import Magmom, Orbital, OrbitalType, Spin
2424
from pymatgen.entries.compatibility import MaterialsProjectCompatibility
2525
from pymatgen.io.vasp.inputs import Incar, Kpoints, Poscar, Potcar
@@ -43,6 +43,7 @@
4343
Wavecar,
4444
Waveder,
4545
Xdatcar,
46+
get_band_structure_from_vasp_multiple_branches,
4647
)
4748
from pymatgen.io.wannier90 import Unk
4849
from pymatgen.util.testing import FAKE_POTCAR_DIR, TEST_FILES_DIR, VASP_IN_DIR, VASP_OUT_DIR, MatSciTest
@@ -1533,13 +1534,40 @@ def test_init(self):
15331534
assert len(oszicar.electronic_steps) == len(oszicar.ionic_steps)
15341535
assert len(oszicar.all_energies) == 60
15351536
assert oszicar.final_energy == approx(-526.63928)
1536-
assert set(oszicar.ionic_steps[-1]) == set({"F", "E0", "dE", "mag"})
1537+
assert set(oszicar.ionic_steps[-1]) == {"F", "E0", "dE", "mag"}
15371538

15381539
def test_static(self):
15391540
fpath = f"{TEST_DIR}/fixtures/static_silicon/OSZICAR"
15401541
oszicar = Oszicar(fpath)
15411542
assert oszicar.final_energy == approx(-10.645278)
1542-
assert set(oszicar.ionic_steps[-1]) == set({"F", "E0", "dE", "mag"})
1543+
assert set(oszicar.ionic_steps[-1]) == {"F", "E0", "dE", "mag"}
1544+
1545+
1546+
class TestGetBandStructureFromVaspMultipleBranches:
1547+
def test_read_multi_branches(self):
1548+
"""TODO: This functionality still needs a test."""
1549+
1550+
def test_missing_vasprun_in_branch_dir(self):
1551+
"""Test vasprun.xml missing from branch_*."""
1552+
os.makedirs("no_vasp/branch_0", exist_ok=False)
1553+
1554+
with pytest.raises(FileNotFoundError, match="cannot find vasprun.xml in directory"):
1555+
get_band_structure_from_vasp_multiple_branches("no_vasp")
1556+
1557+
def test_no_branch_head(self):
1558+
"""Test branch_0 is missing and read dir_name/vasprun.xml directly."""
1559+
1560+
copyfile(f"{VASP_OUT_DIR}/vasprun.force_hybrid_like_calc.xml.gz", "./vasprun.xml.gz")
1561+
decompress_file("./vasprun.xml.gz")
1562+
1563+
with pytest.warns(DeprecationWarning, match="no branch dir found, reading directly from"):
1564+
bs = get_band_structure_from_vasp_multiple_branches(".")
1565+
assert isinstance(bs, BandStructure)
1566+
1567+
def test_cannot_read_anything(self):
1568+
"""Test no branch_0/, no dir_name/vasprun.xml, no vasprun.xml at all."""
1569+
with pytest.raises(FileNotFoundError, match="failed to find any vasprun.xml in selected"):
1570+
get_band_structure_from_vasp_multiple_branches(".")
15431571

15441572

15451573
class TestLocpot(MatSciTest):

0 commit comments

Comments
 (0)