Skip to content

Commit 8b30879

Browse files
committed
reuse incar_parameters.json in proc_val to determine INCAR tag type
1 parent b4f520e commit 8b30879

File tree

2 files changed

+48
-88
lines changed

2 files changed

+48
-88
lines changed

src/pymatgen/io/vasp/inputs.py

Lines changed: 41 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -795,6 +795,10 @@ class Incar(UserDict, MSONable):
795795
in the `lower_str_keys/as_is_str_keys` of the `proc_val` method.
796796
"""
797797

798+
# INCAR tag/value recording
799+
with open(os.path.join(MODULE_DIR, "incar_parameters.json"), encoding="utf-8") as json_file:
800+
INCAR_PARAMS: ClassVar[dict[Literal["type", "values"], Any]] = orjson.loads(json_file.read())
801+
798802
def __init__(self, params: Mapping[str, Any] | None = None) -> None:
799803
"""
800804
Clean up params and create an Incar object.
@@ -970,88 +974,46 @@ def from_str(cls, string: str) -> Self:
970974
params[key] = cls.proc_val(key, val)
971975
return cls(params)
972976

973-
@staticmethod
974-
def proc_val(key: str, val: str) -> list | bool | float | int | str:
977+
@classmethod
978+
def proc_val(cls, key: str, val: str) -> list | bool | float | int | str:
975979
"""Helper method to convert INCAR parameters to proper types
976980
like ints, floats, lists, etc.
977981
978982
Args:
979983
key (str): INCAR parameter key.
980984
val (str): Value of INCAR parameter.
981985
"""
982-
list_keys = (
983-
"LDAUU",
984-
"LDAUL",
985-
"LDAUJ",
986-
"MAGMOM",
987-
"DIPOL",
988-
"LANGEVIN_GAMMA",
989-
"QUAD_EFG",
990-
"EINT",
991-
"LATTICE_CONSTRAINTS",
992-
)
993-
bool_keys = (
994-
"LDAU",
995-
"LWAVE",
996-
"LSCALU",
997-
"LCHARG",
998-
"LPLANE",
999-
"LUSE_VDW",
1000-
"LHFCALC",
1001-
"ADDGRID",
1002-
"LSORBIT",
1003-
"LNONCOLLINEAR",
1004-
)
1005-
float_keys = (
1006-
"EDIFF",
1007-
"SIGMA",
1008-
"TIME",
1009-
"ENCUTFOCK",
1010-
"HFSCREEN",
1011-
"POTIM",
1012-
"EDIFFG",
1013-
"AGGAC",
1014-
"PARAM1",
1015-
"PARAM2",
1016-
"ENCUT",
1017-
"NUPDOWN",
1018-
)
1019-
int_keys = (
1020-
"NSW",
1021-
"NBANDS",
1022-
"NELMIN",
1023-
"ISIF",
1024-
"IBRION",
1025-
"ISPIN",
1026-
"ISTART",
1027-
"ICHARG",
1028-
"NELM",
1029-
"ISMEAR",
1030-
"NPAR",
1031-
"LDAUPRINT",
1032-
"LMAXMIX",
1033-
"NSIM",
1034-
"NKRED",
1035-
"ISPIND",
1036-
"LDAUTYPE",
1037-
"IVDW",
1038-
)
986+
# Handle union type like "bool | str" for LREAL
987+
if incar_type := cls.INCAR_PARAMS.get(key, {}).get("type"):
988+
incar_types: list[str] = [t.strip() for t in incar_type.split("|")]
989+
else:
990+
incar_types = []
991+
992+
# Special cases
993+
# Always lower case
1039994
lower_str_keys = ("ML_MODE",)
1040995
# String keywords to read "as is" (no case transformation, only stripped)
1041996
as_is_str_keys = ("SYSTEM",)
1042997

1043-
def smart_int_or_float_bool(str_: str) -> float | int | bool:
1044-
"""Determine whether a string represents an integer or a float."""
1045-
if str_.lower().startswith(".t") or str_.lower().startswith("t"):
1046-
return True
1047-
if str_.lower().startswith(".f") or str_.lower().startswith("f"):
1048-
return False
1049-
if "." in str_ or "e" in str_.lower():
1050-
return float(str_)
1051-
return int(str_)
1052-
1053998
try:
1054-
if key in list_keys:
999+
if key in lower_str_keys:
1000+
return val.strip().lower()
1001+
1002+
if key in as_is_str_keys:
1003+
return val.strip()
1004+
1005+
if "list" in incar_types:
1006+
1007+
def smart_int_or_float_bool(str_: str) -> float | int | bool:
1008+
"""Determine whether a string represents an integer or a float."""
1009+
if str_.lower().startswith(".t") or str_.lower().startswith("t"):
1010+
return True
1011+
if str_.lower().startswith(".f") or str_.lower().startswith("f"):
1012+
return False
1013+
if "." in str_ or "e" in str_.lower():
1014+
return float(str_)
1015+
return int(str_)
1016+
10551017
output = []
10561018
tokens = re.findall(r"(-?\d+\.?\d*|[\.A-Z]+)\*?(-?\d+\.?\d*|[\.A-Z]+)?\*?(-?\d+\.?\d*|[\.A-Z]+)?", val)
10571019
for tok in tokens:
@@ -1061,26 +1023,22 @@ def smart_int_or_float_bool(str_: str) -> float | int | bool:
10611023
output.extend([smart_int_or_float_bool(tok[1])] * int(tok[0]))
10621024
else:
10631025
output.append(smart_int_or_float_bool(tok[0]))
1064-
return output
10651026

1066-
if key in bool_keys:
1027+
if output: # pass when fail to parse (val is not list)
1028+
return output
1029+
1030+
if "bool" in incar_types:
10671031
if match := re.match(r"^\.?([T|F|t|f])[A-Za-z]*\.?", val):
10681032
return match[1].lower() == "t"
10691033

10701034
raise ValueError(f"{key} should be a boolean type!")
10711035

1072-
if key in float_keys:
1036+
if "float" in incar_types:
10731037
return float(re.search(r"^-?\d*\.?\d*[e|E]?-?\d*", val)[0]) # type: ignore[index]
10741038

1075-
if key in int_keys:
1039+
if "int" in incar_types:
10761040
return int(re.match(r"^-?[0-9]+", val)[0]) # type: ignore[index]
10771041

1078-
if key in lower_str_keys:
1079-
return val.strip().lower()
1080-
1081-
if key in as_is_str_keys:
1082-
return val.strip()
1083-
10841042
except ValueError:
10851043
pass
10861044

@@ -1138,13 +1096,9 @@ def check_params(self) -> None:
11381096
If a tag doesn't exist, calculation will still run, however VASP
11391097
will ignore the tag and set it as default without letting you know.
11401098
"""
1141-
# Load INCAR tag/value check reference file
1142-
with open(os.path.join(MODULE_DIR, "incar_parameters.json"), encoding="utf-8") as json_file:
1143-
incar_params = orjson.loads(json_file.read())
1144-
11451099
for tag, val in self.items():
11461100
# Check if the tag exists
1147-
if tag not in incar_params:
1101+
if tag not in self.INCAR_PARAMS:
11481102
warnings.warn(
11491103
f"Cannot find {tag} in the list of INCAR tags",
11501104
BadIncarWarning,
@@ -1153,8 +1107,8 @@ def check_params(self) -> None:
11531107
continue
11541108

11551109
# Check value type
1156-
param_type: str = incar_params[tag].get("type")
1157-
allowed_values: list[Any] = incar_params[tag].get("values")
1110+
param_type: str = self.INCAR_PARAMS[tag].get("type")
1111+
allowed_values: list[Any] = self.INCAR_PARAMS[tag].get("values")
11581112

11591113
if param_type is not None and not isinstance(val, eval(param_type)): # noqa: S307
11601114
warnings.warn(f"{tag}: {val} is not a {param_type}", BadIncarWarning, stacklevel=2)

tests/io/vasp/test_inputs.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1001,12 +1001,18 @@ def test_types(self):
10011001
assert incar["HFSCREEN"] == approx(0.2)
10021002
assert incar["ALGO"] == "All"
10031003

1004-
def test_proc_types(self):
1004+
def test_proc_val(self):
10051005
assert Incar.proc_val("HELLO", "-0.85 0.85") == "-0.85 0.85"
10061006
assert Incar.proc_val("ML_MODE", "train") == "train"
10071007
assert Incar.proc_val("ML_MODE", "RUN") == "run"
10081008
assert Incar.proc_val("ALGO", "fast") == "Fast"
10091009

1010+
# LREAL has union type "bool | str"
1011+
assert Incar.proc_val("LREAL", "T") is True
1012+
assert Incar.proc_val("LREAL", ".FALSE.") is False
1013+
assert Incar.proc_val("LREAL", "Auto") == "Auto"
1014+
assert Incar.proc_val("LREAL", "on") == "On"
1015+
10101016
def test_check_params(self):
10111017
# Triggers warnings when running into invalid parameters
10121018
incar = Incar(

0 commit comments

Comments
 (0)