Skip to content

Commit b36cf07

Browse files
authored
Incar.proc_val reuse incar_parameters.json to determine types (#4522)
* NUPDOWN should be float? https://www.vasp.at/wiki/NUPDOWN NUPDOWN = [positive real] Default: NUPDOWN = not set Description: Sets the difference between the number of electrons in the up and down spin components. Allows calculations for a specific spin multiplet, i.e. the difference of the number of electrons in the up and down spin component will be kept fixed to the specified value. There is a word of caution required: If NUPDOWN is set in the INCAR file the initial moment for the charge density should be the same. Otherwise convergence can slow down. When starting from atomic charge densities (ICHARG=2), VASP will try to do this automatically by setting MAGMOM to NUPDOWN/NIONS. The user can of course overwrite this default by specifying a different MAGMOM (which should still result in the correct total moment). If one initializes the charge density from the one-electron wavefunctions, the initial moment is always correct, because VASP "pushes" the required number of electrons from the down to the up component. Initializing the charge density from the CHGCAR file (ICHARG=1), however, the initial moment is usually incorrect! If no value is set (or NUPDOWN=-1) a full relaxation will be performed. This is also the default. * reuse incar_parameters.json in `proc_val` to determine INCAR tag type * make proc_val more permissive: not run when type differs from recording
1 parent 3fa9705 commit b36cf07

File tree

3 files changed

+88
-90
lines changed

3 files changed

+88
-90
lines changed

src/pymatgen/io/vasp/incar_parameters.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1127,7 +1127,7 @@
11271127
"type": "int"
11281128
},
11291129
"NUPDOWN": {
1130-
"type": "int"
1130+
"type": "float"
11311131
},
11321132
"NWRITE": {
11331133
"type": "int",

src/pymatgen/io/vasp/inputs.py

Lines changed: 43 additions & 88 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 (e.g. "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,27 +1023,24 @@ 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-
1084-
except ValueError:
1042+
# If re.match doesn't hit, it would return None and thus TypeError from indexing
1043+
except (ValueError, TypeError):
10851044
pass
10861045

10871046
# Not in known keys. We will try a hierarchy of conversions.
@@ -1138,13 +1097,9 @@ def check_params(self) -> None:
11381097
If a tag doesn't exist, calculation will still run, however VASP
11391098
will ignore the tag and set it as default without letting you know.
11401099
"""
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-
11451100
for tag, val in self.items():
11461101
# Check if the tag exists
1147-
if tag not in incar_params:
1102+
if tag not in self.INCAR_PARAMS:
11481103
warnings.warn(
11491104
f"Cannot find {tag} in the list of INCAR tags",
11501105
BadIncarWarning,
@@ -1153,8 +1108,8 @@ def check_params(self) -> None:
11531108
continue
11541109

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

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

tests/io/vasp/test_inputs.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1001,12 +1001,55 @@ 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+
1016+
def test_proc_val_inconsistent_type(self):
1017+
"""proc_val should not raise even when value conflicts with expected type."""
1018+
1019+
bool_values = ["T", ".FALSE."]
1020+
int_values = ["5", "-3"]
1021+
float_values = ["1.23", "-4.56e-2"]
1022+
list_values = ["3*1.0 2*0.5", "1 2 3"]
1023+
str_values = ["Auto", "Run", "Hello"]
1024+
1025+
# Expect bool
1026+
for v in int_values + float_values + list_values + str_values:
1027+
assert Incar.proc_val("LASPH", v) is not None
1028+
1029+
# Expect int
1030+
for v in bool_values + float_values + list_values + str_values:
1031+
assert Incar.proc_val("LORBIT", v) is not None
1032+
1033+
# Expect float
1034+
for v in bool_values + int_values + list_values + str_values:
1035+
assert Incar.proc_val("ENCUT", v) is not None
1036+
1037+
# Expect str
1038+
for v in bool_values + int_values + float_values + list_values:
1039+
assert Incar.proc_val("ALGO", v) is not None
1040+
1041+
# Expect list
1042+
for v in bool_values + int_values + float_values + str_values:
1043+
assert Incar.proc_val("PHON_TLIST", v) is not None
1044+
1045+
# Union type (bool | str)
1046+
for v in int_values + float_values + list_values:
1047+
assert Incar.proc_val("LREAL", v) is not None
1048+
1049+
# Non-existent
1050+
for v in int_values + float_values + list_values + str_values + bool_values:
1051+
assert Incar.proc_val("HELLOWORLD", v) is not None
1052+
10101053
def test_check_params(self):
10111054
# Triggers warnings when running into invalid parameters
10121055
incar = Incar(

0 commit comments

Comments
 (0)