Skip to content

Commit dc49153

Browse files
Remove auto_nbands handling + fix bravais inconsistency handling (#409)
* remove auto_nbands handling, throw only a warning * fix bravais to reflect vasp advice * typo in auto_nbands fix * review comments * try 2 * ruff * try pytest xdist * fix lobster test * give up on pytest xdist --------- Co-authored-by: Andrew S. Rosen <[email protected]>
1 parent 73f1869 commit dc49153

File tree

5 files changed

+88
-99
lines changed

5 files changed

+88
-99
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,6 @@ ignore = [
116116
"D212", # Multi-line docstring summary should start at the first line
117117
"ISC001",
118118
"PD011", # pandas-use-of-dot-values
119-
"PD901", # pandas-df-variable-name
120119
"PERF203", # try-except-in-loop
121120
"PLC0415", # import-outside-top-level (used for performance/optional deps)
122121
"PLR", # pylint refactor
@@ -183,6 +182,7 @@ dev = [
183182
"pymatgen>=2025.5.16",
184183
"pytest-cov>=6.0.0",
185184
"pytest>=8.3.5",
185+
"pytest-xdist",
186186
"ruff>=0.11.2",
187187
"sphinx-markdown-builder>=0.6.8",
188188
"sphinx>=8.1.3",

src/custodian/lobster/jobs.py

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -109,16 +109,12 @@ def run(self, directory="./"):
109109
def postprocess(self, directory="./") -> None:
110110
"""Will gzip relevant files (won't gzip custodian.json and other output files from the cluster)."""
111111
if self.gzipped:
112-
for file in LOBSTEROUTPUT_FILES:
113-
if os.path.isfile(os.path.join(directory, file)):
114-
compress_file(os.path.join(directory, file), compression="gz")
115-
for file in LOBSTERINPUT_FILES:
116-
if os.path.isfile(os.path.join(directory, file)):
117-
compress_file(os.path.join(directory, file), compression="gz")
118-
if self.backup and os.path.isfile(os.path.join(directory, "lobsterin.orig")):
119-
compress_file(os.path.join(directory, "lobsterin.orig"), compression="gz")
120-
for file in FW_FILES:
121-
if os.path.isfile(os.path.join(directory, file)):
122-
compress_file(os.path.join(directory, file), compression="gz")
123-
for file in self.add_files_to_gzip:
124-
compress_file(os.path.join(directory, file), compression="gz")
112+
files_to_zip = (
113+
set(LOBSTEROUTPUT_FILES).union(LOBSTERINPUT_FILES).union(FW_FILES).union(self.add_files_to_gzip)
114+
)
115+
if self.backup:
116+
files_to_zip.add("lobsterin.orig")
117+
118+
for file in files_to_zip:
119+
if os.path.isfile(file_path := os.path.join(directory, file)):
120+
compress_file(file_path, compression="gz")

src/custodian/utils.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
"""Utility function and classes."""
22

3+
from __future__ import annotations
4+
35
import functools
46
import logging
57
import os
68
import tarfile
79
from glob import glob
8-
from typing import ClassVar
10+
from typing import TYPE_CHECKING
11+
12+
if TYPE_CHECKING:
13+
from typing import ClassVar
914

1015

1116
def backup(filenames, prefix="error", directory="./") -> None:

src/custodian/vasp/handlers.py

Lines changed: 38 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,19 @@ def check(self, directory="./"):
197197
# e-density (brmix error)
198198
if err == "brmix" and "NELECT" in incar:
199199
continue
200+
201+
# Treat auto_nbands only as a warning, do not fail a job
202+
if err == "auto_nbands":
203+
if nbands := self._get_nbands_from_outcar(directory):
204+
outcar = load_outcar(os.path.join(directory, "OUTCAR"))
205+
if (nelect := outcar.nelect) and (nbands > 2 * nelect):
206+
warnings.warn(
207+
"NBANDS seems to be too high. The electronic structure may be inaccurate. "
208+
"You may want to rerun this job with a smaller number of cores.",
209+
UserWarning,
210+
)
211+
continue
212+
200213
self.errors.add(err)
201214
error_msgs.add(msg)
202215
for msg in error_msgs:
@@ -538,12 +551,11 @@ def correct(self, directory="./"):
538551

539552
self.error_count["zbrent"] += 1
540553

541-
if "too_few_bands" in self.errors:
542-
nbands = None
543-
nbands = vi["INCAR"]["NBANDS"] if "NBANDS" in vi["INCAR"] else self._get_nbands_from_outcar(directory)
544-
if nbands:
545-
new_nbands = max(int(1.1 * nbands), nbands + 1) # This handles the case when nbands is too low (< 8).
546-
actions.append({"dict": "INCAR", "action": {"_set": {"NBANDS": new_nbands}}})
554+
if "too_few_bands" in self.errors and (
555+
nbands := vi["INCAR"].get("NBANDS") or self._get_nbands_from_outcar(directory)
556+
):
557+
new_nbands = max(int(1.1 * nbands), nbands + 1) # This handles the case when nbands is too low (< 8).
558+
actions.append({"dict": "INCAR", "action": {"_set": {"NBANDS": new_nbands}}})
547559

548560
if self.errors & {"pssyevx", "pdsyevx"} and vi["INCAR"].get("ALGO", "Normal").lower() != "normal":
549561
actions.append({"dict": "INCAR", "action": {"_set": {"ALGO": "Normal"}}})
@@ -667,23 +679,20 @@ def correct(self, directory="./"):
667679

668680
if self.errors.intersection(["bravais", "ksymm"]):
669681
# For bravais: VASP recommends refining the lattice parameters
670-
# or changing SYMPREC. See https://www.vasp.at/forum/viewtopic.php?f=3&t=19109
682+
# or changing SYMPREC (default = 1e-5). See
683+
# https://www.vasp.at/forum/viewtopic.php?f=3&t=19109
671684
# Appears to occur when SYMPREC is very low, so we change it to
672685
# the default if it's not already. If it's the default, we x10.
673686
# For ksymm, there's not much information about the issue other than the
674687
# direct and reciprocal meshes being incompatible.
675688
# This is basically the same as bravais
676689
vasp_recommended_symprec = 1e-6
677-
symprec = vi["INCAR"].get("SYMPREC", vasp_recommended_symprec)
678-
if symprec < vasp_recommended_symprec:
679-
actions.append({"dict": "INCAR", "action": {"_set": {"SYMPREC": vasp_recommended_symprec}}})
680-
elif symprec < 1e-4:
681-
# try 10xing symprec twice, then set ISYM=0 to not impose potentially artificial symmetry from
682-
# too loose symprec on charge density
683-
actions.append({"dict": "INCAR", "action": {"_set": {"SYMPREC": float(f"{symprec * 10:.1e}")}}})
684-
else:
690+
if (symprec := vi["INCAR"].get("SYMPREC", 1e-5)) > vasp_recommended_symprec:
691+
actions.append(
692+
{"dict": "INCAR", "action": {"_set": {"SYMPREC": min(symprec / 10.0, vasp_recommended_symprec)}}}
693+
)
694+
elif vi["INCAR"].get("ISYM") > 0: # Default ISYM is variable, but never 0
685695
actions.append({"dict": "INCAR", "action": {"_set": {"ISYM": 0}}})
686-
self.error_count["bravais"] += 1
687696

688697
if "nbands_not_sufficient" in self.errors:
689698
outcar = load_outcar(os.path.join(directory, "OUTCAR"))
@@ -749,50 +758,25 @@ def correct(self, directory="./"):
749758
)
750759
self.error_count["algo_tet"] += 1
751760

752-
if "auto_nbands" in self.errors and (nbands := self._get_nbands_from_outcar(directory)):
753-
outcar = load_outcar(os.path.join(directory, "OUTCAR"))
754-
755-
if (nelect := outcar.nelect) and (nbands > 2 * nelect):
756-
self.error_count["auto_nbands"] += 1
757-
warnings.warn(
758-
"NBANDS seems to be too high. The electronic structure may be inaccurate. "
759-
"You may want to rerun this job with a smaller number of cores.",
760-
UserWarning,
761-
)
762-
763-
elif nbands := vi["INCAR"].get("NBANDS"):
764-
kpar = vi["INCAR"].get("KPAR", 1)
765-
ncore = vi["INCAR"].get("NCORE", 1)
766-
# If the user set an NBANDS that isn't compatible with parallelization settings,
767-
# increase NBANDS to ensure correct task distribution and issue a UserWarning.
768-
# The number of ranks per band is (number of MPI ranks) / (KPAR * NCORE)
769-
if (ranks := outcar.run_stats.get("cores")) and (rem_bands := nbands % (ranks // (kpar * ncore))) != 0:
770-
actions.append({"dict": "INCAR", "action": {"_set": {"NBANDS": nbands + rem_bands}}})
771-
warnings.warn(
772-
f"Your NBANDS={nbands} setting was incompatible with your parallelization "
773-
f"settings, KPAR={kpar}, NCORE={ncore}, over {ranks} ranks. "
774-
f"The number of bands has been decreased accordingly to {nbands + rem_bands}.",
775-
UserWarning,
776-
)
777-
778761
VaspModder(vi=vi, directory=directory).apply_actions(actions)
779762
return {"errors": list(self.errors), "actions": actions}
780763

781764
@staticmethod
782765
def _get_nbands_from_outcar(directory: str) -> int | None:
783766
nbands = None
784-
with open(os.path.join(directory, "OUTCAR")) as file:
785-
for line in file:
786-
# Have to take the last NBANDS line since sometimes VASP
787-
# updates it automatically even if the user specifies it.
788-
# The last one is marked by NBANDS= (no space).
789-
if "NBANDS=" in line:
790-
try:
791-
d = line.split("=")
792-
nbands = int(d[-1].strip())
793-
break
794-
except (IndexError, ValueError):
795-
pass
767+
if os.path.isfile(outcar_path := os.path.join(directory, "OUTCAR")):
768+
with open(outcar_path) as file:
769+
for line in file:
770+
# Have to take the last NBANDS line since sometimes VASP
771+
# updates it automatically even if the user specifies it.
772+
# The last one is marked by NBANDS= (no space).
773+
if "NBANDS=" in line:
774+
try:
775+
d = line.split("=")
776+
nbands = int(d[-1].strip())
777+
break
778+
except (IndexError, ValueError):
779+
pass
796780
return nbands
797781

798782

tests/lobster/test_jobs.py

Lines changed: 34 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import os
22
import shutil
3+
from pathlib import Path
4+
from tempfile import TemporaryDirectory
35

46
from monty.os import cd
57
from monty.tempfile import ScratchDir
@@ -82,36 +84,38 @@ def test_setup(self) -> None:
8284

8385
def test_postprocess(self) -> None:
8486
# test gzipped and zipping of additional files
85-
with cd(os.path.join(test_files_lobster3)):
86-
with ScratchDir(".", copy_from_current_on_enter=True):
87-
shutil.copy("lobsterin", "lobsterin.orig")
88-
v = LobsterJob("hello", gzipped=True, add_files_to_gzip=VASP_OUTPUT_FILES)
89-
v.postprocess()
90-
for file in (*VASP_OUTPUT_FILES, *LOBSTER_FILES, *FW_FILES):
91-
assert os.path.isfile(f"{file}.gz")
92-
93-
with ScratchDir(".", copy_from_current_on_enter=True):
94-
shutil.copy("lobsterin", "lobsterin.orig")
95-
v = LobsterJob("hello", gzipped=False, add_files_to_gzip=VASP_OUTPUT_FILES)
96-
v.postprocess()
97-
for file in (*VASP_OUTPUT_FILES, *LOBSTER_FILES, *FW_FILES):
98-
assert os.path.isfile(file)
87+
src_path = Path(test_files_lobster3).absolute()
88+
for gzipped in (True, False):
89+
with TemporaryDirectory() as tmp_dir:
90+
cwd = Path(tmp_dir).absolute()
91+
for file_obj in Path(src_path).glob("*"):
92+
if file_obj.is_file():
93+
shutil.copy(src_path / file_obj, cwd / file_obj.name)
94+
shutil.copy(src_path / "lobsterin", cwd / "lobsterin.orig")
95+
with cd(cwd):
96+
v = LobsterJob("hello", gzipped=gzipped, add_files_to_gzip=VASP_OUTPUT_FILES)
97+
v.postprocess()
98+
assert all(
99+
os.path.isfile(f"{file}{'.gz' if gzipped else ''}")
100+
for file in (*VASP_OUTPUT_FILES, *LOBSTER_FILES, *FW_FILES)
101+
)
99102

100103
def test_postprocess_v51(self) -> None:
101104
# test gzipped and zipping of additional files for lobster v5.1
102-
with cd(os.path.join(test_files_lobster4)):
103-
with ScratchDir(".", copy_from_current_on_enter=True):
104-
shutil.copy("lobsterin", "lobsterin.orig")
105-
v = LobsterJob("hello", gzipped=True, add_files_to_gzip=VASP_OUTPUT_FILES)
106-
v.postprocess()
107-
for file in (*VASP_OUTPUT_FILES, *LOBSTEROUTPUT_FILES, *FW_FILES):
108-
if file not in ("POSCAR.lobster", "bandOverlaps.lobster"): # these files are not in the directory
109-
assert os.path.isfile(f"{file}.gz")
110-
111-
with ScratchDir(".", copy_from_current_on_enter=True):
112-
shutil.copy("lobsterin", "lobsterin.orig")
113-
v = LobsterJob("hello", gzipped=False, add_files_to_gzip=VASP_OUTPUT_FILES)
114-
v.postprocess()
115-
for file in (*VASP_OUTPUT_FILES, *LOBSTEROUTPUT_FILES, *FW_FILES):
116-
if file not in ("POSCAR.lobster", "bandOverlaps.lobster"): # these files are not in the directory
117-
assert os.path.isfile(file)
105+
src_path = Path(test_files_lobster4).absolute()
106+
for gzipped in (True, False):
107+
with TemporaryDirectory() as tmp_dir:
108+
cwd = Path(tmp_dir).absolute()
109+
for file_obj in Path(src_path).glob("*"):
110+
if file_obj.is_file():
111+
shutil.copy(src_path / file_obj, cwd / file_obj.name)
112+
shutil.copy(src_path / "lobsterin", cwd / "lobsterin.orig")
113+
with cd(cwd):
114+
v = LobsterJob("hello", gzipped=gzipped, add_files_to_gzip=VASP_OUTPUT_FILES)
115+
v.postprocess()
116+
assert all(
117+
os.path.isfile(f"{file}{'.gz' if gzipped else ''}")
118+
for file in {*VASP_OUTPUT_FILES, *LOBSTEROUTPUT_FILES, *FW_FILES}.difference(
119+
{"POSCAR.lobster", "bandOverlaps.lobster"} # these files are not in the directory
120+
)
121+
)

0 commit comments

Comments
 (0)