Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/pymatgen/io/vasp/incar_parameters.json
Original file line number Diff line number Diff line change
Expand Up @@ -1127,7 +1127,7 @@
"type": "int"
},
"NUPDOWN": {
"type": "int"
"type": "float"
},
"NWRITE": {
"type": "int",
Expand Down
131 changes: 43 additions & 88 deletions src/pymatgen/io/vasp/inputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -795,6 +795,10 @@ class Incar(UserDict, MSONable):
in the `lower_str_keys/as_is_str_keys` of the `proc_val` method.
"""

# INCAR tag/value recording
with open(os.path.join(MODULE_DIR, "incar_parameters.json"), encoding="utf-8") as json_file:
INCAR_PARAMS: ClassVar[dict[Literal["type", "values"], Any]] = orjson.loads(json_file.read())

def __init__(self, params: Mapping[str, Any] | None = None) -> None:
"""
Clean up params and create an Incar object.
Expand Down Expand Up @@ -970,88 +974,46 @@ def from_str(cls, string: str) -> Self:
params[key] = cls.proc_val(key, val)
return cls(params)

@staticmethod
def proc_val(key: str, val: str) -> list | bool | float | int | str:
@classmethod
def proc_val(cls, key: str, val: str) -> list | bool | float | int | str:
"""Helper method to convert INCAR parameters to proper types
like ints, floats, lists, etc.

Args:
key (str): INCAR parameter key.
val (str): Value of INCAR parameter.
"""
list_keys = (
"LDAUU",
"LDAUL",
"LDAUJ",
"MAGMOM",
"DIPOL",
"LANGEVIN_GAMMA",
"QUAD_EFG",
"EINT",
"LATTICE_CONSTRAINTS",
)
bool_keys = (
"LDAU",
"LWAVE",
"LSCALU",
"LCHARG",
"LPLANE",
"LUSE_VDW",
"LHFCALC",
"ADDGRID",
"LSORBIT",
"LNONCOLLINEAR",
)
float_keys = (
"EDIFF",
"SIGMA",
"TIME",
"ENCUTFOCK",
"HFSCREEN",
"POTIM",
"EDIFFG",
"AGGAC",
"PARAM1",
"PARAM2",
"ENCUT",
"NUPDOWN",
)
int_keys = (
"NSW",
"NBANDS",
"NELMIN",
"ISIF",
"IBRION",
"ISPIN",
"ISTART",
"ICHARG",
"NELM",
"ISMEAR",
"NPAR",
"LDAUPRINT",
"LMAXMIX",
"NSIM",
"NKRED",
"ISPIND",
"LDAUTYPE",
"IVDW",
)
# Handle union type (e.g. "bool | str" for LREAL)
if incar_type := cls.INCAR_PARAMS.get(key, {}).get("type"):
incar_types: list[str] = [t.strip() for t in incar_type.split("|")]
else:
incar_types = []

# Special cases
# Always lower case
lower_str_keys = ("ML_MODE",)
# String keywords to read "as is" (no case transformation, only stripped)
as_is_str_keys = ("SYSTEM",)

def smart_int_or_float_bool(str_: str) -> float | int | bool:
"""Determine whether a string represents an integer or a float."""
if str_.lower().startswith(".t") or str_.lower().startswith("t"):
return True
if str_.lower().startswith(".f") or str_.lower().startswith("f"):
return False
if "." in str_ or "e" in str_.lower():
return float(str_)
return int(str_)

try:
if key in list_keys:
if key in lower_str_keys:
return val.strip().lower()

if key in as_is_str_keys:
return val.strip()

if "list" in incar_types:

def smart_int_or_float_bool(str_: str) -> float | int | bool:
"""Determine whether a string represents an integer or a float."""
if str_.lower().startswith(".t") or str_.lower().startswith("t"):
return True
if str_.lower().startswith(".f") or str_.lower().startswith("f"):
return False
if "." in str_ or "e" in str_.lower():
return float(str_)
return int(str_)

output = []
tokens = re.findall(r"(-?\d+\.?\d*|[\.A-Z]+)\*?(-?\d+\.?\d*|[\.A-Z]+)?\*?(-?\d+\.?\d*|[\.A-Z]+)?", val)
for tok in tokens:
Expand All @@ -1061,27 +1023,24 @@ def smart_int_or_float_bool(str_: str) -> float | int | bool:
output.extend([smart_int_or_float_bool(tok[1])] * int(tok[0]))
else:
output.append(smart_int_or_float_bool(tok[0]))
return output

if key in bool_keys:
if output: # pass when fail to parse (val is not list)
return output

if "bool" in incar_types:
if match := re.match(r"^\.?([T|F|t|f])[A-Za-z]*\.?", val):
return match[1].lower() == "t"

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

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

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

if key in lower_str_keys:
return val.strip().lower()

if key in as_is_str_keys:
return val.strip()

except ValueError:
# If re.match doesn't hit, it would return None and thus TypeError from indexing
except (ValueError, TypeError):
Copy link
Contributor Author

@DanielYang59 DanielYang59 Oct 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Guess proc_val should be more permissive (more strict check should be done by check_params)? i.e. if a tag's val is incompatible with its type in recording, I guess we just continue to try other conversations (and at the worst keep it as string) instead of error out?

cc @shyuep

pass

# Not in known keys. We will try a hierarchy of conversions.
Expand Down Expand Up @@ -1138,13 +1097,9 @@ def check_params(self) -> None:
If a tag doesn't exist, calculation will still run, however VASP
will ignore the tag and set it as default without letting you know.
"""
# Load INCAR tag/value check reference file
with open(os.path.join(MODULE_DIR, "incar_parameters.json"), encoding="utf-8") as json_file:
incar_params = orjson.loads(json_file.read())

for tag, val in self.items():
# Check if the tag exists
if tag not in incar_params:
if tag not in self.INCAR_PARAMS:
warnings.warn(
f"Cannot find {tag} in the list of INCAR tags",
BadIncarWarning,
Expand All @@ -1153,8 +1108,8 @@ def check_params(self) -> None:
continue

# Check value type
param_type: str = incar_params[tag].get("type")
allowed_values: list[Any] = incar_params[tag].get("values")
param_type: str = self.INCAR_PARAMS[tag].get("type")
allowed_values: list[Any] = self.INCAR_PARAMS[tag].get("values")

if param_type is not None and not isinstance(val, eval(param_type)): # noqa: S307
warnings.warn(f"{tag}: {val} is not a {param_type}", BadIncarWarning, stacklevel=2)
Expand Down
45 changes: 44 additions & 1 deletion tests/io/vasp/test_inputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -1001,12 +1001,55 @@ def test_types(self):
assert incar["HFSCREEN"] == approx(0.2)
assert incar["ALGO"] == "All"

def test_proc_types(self):
def test_proc_val(self):
assert Incar.proc_val("HELLO", "-0.85 0.85") == "-0.85 0.85"
assert Incar.proc_val("ML_MODE", "train") == "train"
assert Incar.proc_val("ML_MODE", "RUN") == "run"
assert Incar.proc_val("ALGO", "fast") == "Fast"

# LREAL has union type "bool | str"
assert Incar.proc_val("LREAL", "T") is True
assert Incar.proc_val("LREAL", ".FALSE.") is False
assert Incar.proc_val("LREAL", "Auto") == "Auto"
assert Incar.proc_val("LREAL", "on") == "On"

def test_proc_val_inconsistent_type(self):
"""proc_val should not raise even when value conflicts with expected type."""

bool_values = ["T", ".FALSE."]
int_values = ["5", "-3"]
float_values = ["1.23", "-4.56e-2"]
list_values = ["3*1.0 2*0.5", "1 2 3"]
str_values = ["Auto", "Run", "Hello"]

# Expect bool
for v in int_values + float_values + list_values + str_values:
assert Incar.proc_val("LASPH", v) is not None

# Expect int
for v in bool_values + float_values + list_values + str_values:
assert Incar.proc_val("LORBIT", v) is not None

# Expect float
for v in bool_values + int_values + list_values + str_values:
assert Incar.proc_val("ENCUT", v) is not None

# Expect str
for v in bool_values + int_values + float_values + list_values:
assert Incar.proc_val("ALGO", v) is not None

# Expect list
for v in bool_values + int_values + float_values + str_values:
assert Incar.proc_val("PHON_TLIST", v) is not None

# Union type (bool | str)
for v in int_values + float_values + list_values:
assert Incar.proc_val("LREAL", v) is not None

# Non-existent
for v in int_values + float_values + list_values + str_values + bool_values:
assert Incar.proc_val("HELLOWORLD", v) is not None

def test_check_params(self):
# Triggers warnings when running into invalid parameters
incar = Incar(
Expand Down