Skip to content

Commit 16ef298

Browse files
tweak classes to allow for bad input sets to later fail in validation doc
1 parent 9026d69 commit 16ef298

File tree

5 files changed

+72
-36
lines changed

5 files changed

+72
-36
lines changed

pymatgen/io/validation/common.py

Lines changed: 56 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -229,8 +229,14 @@ class VaspFiles(BaseModel):
229229
"""Define required and optional files for validation."""
230230

231231
user_input: VaspInputSafe = Field(description="The VASP input set used in the calculation.")
232-
outcar: Optional[LightOutcar] = None
233-
vasprun: Optional[LightVasprun] = None
232+
outcar: LightOutcar | None = None
233+
vasprun: LightVasprun | None = None
234+
run_type: str | None = Field(None, description="The type of VASP calculation performed.")
235+
functional: str | None = Field(None, description="The density functional used in the calculation.")
236+
valid_input_set_name: str | None = Field(
237+
None, description="The import string of the reference MP-compatible input set."
238+
)
239+
validation_errors: list[str] = Field([], description="Errors arising when attempting to validate the input set.")
234240

235241
@model_validator(mode="before")
236242
@classmethod
@@ -246,6 +252,17 @@ def coerce_to_lightweight(cls, config: Any) -> Any:
246252
config["vasprun"] = LightVasprun.from_vasprun(config["vasprun"])
247253
return config
248254

255+
@model_validator(mode="after")
256+
def validate_inputs(self) -> Self:
257+
"""Check that the input set could be referenced against known input sets."""
258+
self.validation_errors = []
259+
self.run_type = self.set_run_type()
260+
self.functional = self.set_functional()
261+
self.valid_input_set_name = None # Ensure this gets reset / checked
262+
if self.run_type and self.functional:
263+
self.valid_input_set_name = self.set_valid_input_set_name()
264+
return self
265+
249266
@property
250267
def md5(self) -> str:
251268
"""Get MD5 of VaspFiles for use in validation checks."""
@@ -308,8 +325,7 @@ def from_paths(
308325

309326
return cls(**config)
310327

311-
@cached_property
312-
def run_type(self) -> str:
328+
def set_run_type(self) -> str | None:
313329
"""Get the run type of a calculation."""
314330

315331
ibrion = self.user_input.incar.get("IBRION", VASP_DEFAULTS_DICT["IBRION"].value)
@@ -322,30 +338,28 @@ def run_type(self) -> str:
322338
**{k: "relax" for k in range(1, 4)},
323339
**{k: "phonon" for k in range(5, 9)},
324340
**{k: "ts" for k in (40, 44)},
325-
}.get(ibrion)
341+
}.get(ibrion, None)
326342

327343
if self.user_input.incar.get("ICHARG", VASP_DEFAULTS_DICT["ICHARG"].value) >= 10:
328344
run_type = "nonscf"
329345
if self.user_input.incar.get("LCHIMAG", VASP_DEFAULTS_DICT["LCHIMAG"].value):
330346
run_type == "nmr"
331347

332348
if run_type is None:
333-
raise ValidationError(
349+
self.validation_errors += [
334350
"Could not determine a valid run type. We currently only validate "
335351
"Geometry optimizations (relaxations), single-points (statics), "
336352
"and non-self-consistent fixed charged density calculations. ",
337-
)
353+
]
338354

339355
return run_type
340356

341-
@cached_property
342-
def functional(self) -> str:
357+
def set_functional(self) -> str | None:
343358
"""Determine the functional used in the calculation.
344359
345360
Note that this is not a complete determination.
346361
Only the functionals used by MP are detected here.
347362
"""
348-
349363
func = None
350364
func_from_potcar = None
351365
if self.user_input.potcar:
@@ -364,11 +378,13 @@ def functional(self) -> str:
364378

365379
if (metagga := self.user_input.incar.get("METAGGA")) and metagga.lower() != "none":
366380
if gga:
367-
raise ValidationError(
381+
self.validation_errors += [
368382
"Both the GGA and METAGGA tags were set, which can lead to large errors. "
369383
"For context, see:\n"
370384
"https://github.com/materialsproject/atomate2/issues/453#issuecomment-1699605867"
371-
)
385+
]
386+
return None
387+
372388
if metagga.lower() == "scan":
373389
func = "scan"
374390
elif metagga.lower().startswith("r2sca"):
@@ -384,12 +400,13 @@ def functional(self) -> str:
384400

385401
func = func or func_from_potcar
386402
if func is None:
387-
raise ValidationError(
403+
self.validation_errors += [
388404
"Currently, we only validate calculations using the following functionals:\n"
389405
"GGA : PBE, PBEsol\n"
390406
"meta-GGA : SCAN, r2SCAN\n"
391407
"Hybrids: HSE06"
392-
)
408+
]
409+
393410
return func
394411

395412
@property
@@ -399,16 +416,14 @@ def bandgap(self) -> float | None:
399416
return self.vasprun.bandgap
400417
return None
401418

402-
@cached_property
403-
def valid_input_set(self) -> VaspInputSafe:
419+
def set_valid_input_set_name(self) -> str | None:
404420
"""
405-
Determine the MP-compliant input set for a calculation.
421+
Determine the MP-compliant input set import string for a calculation.
406422
407423
We need only determine a rough input set here.
408424
The precise details of the input set do not matter.
409425
"""
410426

411-
incar_updates: dict[str, Any] = {}
412427
set_name: str | None = None
413428
if self.functional == "pbe":
414429
if self.run_type == "nonscf":
@@ -420,26 +435,36 @@ def valid_input_set(self) -> VaspInputSafe:
420435
elif self.run_type in ("relax", "static"):
421436
set_name = f"MP{self.run_type.capitalize()}Set"
422437
elif self.functional in ("pbesol", "scan", "r2scan", "hse06"):
423-
if self.functional == "pbesol":
424-
incar_updates["GGA"] = "PS"
425-
elif self.functional == "scan":
426-
incar_updates["METAGGA"] = "SCAN"
427-
elif self.functional == "hse06":
428-
incar_updates.update(
429-
LHFCALC=True,
430-
HFSCREEN=0.2,
431-
GGA="PE",
432-
)
433438
set_name = f"MPScan{self.run_type.capitalize()}Set"
434439

435440
if set_name is None:
436-
raise ValidationError(
441+
self.validation_errors += [
437442
"Could not determine a valid input set from the specified "
438443
f"functional = {self.functional} and calculation type {self.run_type}."
439-
)
444+
]
445+
return None
446+
return set_name
440447

448+
@cached_property
449+
def valid_input_set(self) -> VaspInputSafe:
450+
"""Determine the MP-compliant input set for a calculation."""
451+
452+
if self.valid_input_set_name is None:
453+
raise ValidationError("Cannot determine a valid input set, see `validation_errors` for more details.")
454+
455+
incar_updates: dict[str, Any] = {}
456+
if self.functional == "pbesol":
457+
incar_updates["GGA"] = "PS"
458+
elif self.functional == "scan":
459+
incar_updates["METAGGA"] = "SCAN"
460+
elif self.functional == "hse06":
461+
incar_updates.update(
462+
LHFCALC=True,
463+
HFSCREEN=0.2,
464+
GGA="PE",
465+
)
441466
# Note that only the *previous* bandgap informs the k-point density
442-
vis = getattr(import_module("pymatgen.io.vasp.sets"), set_name)(
467+
vis = getattr(import_module("pymatgen.io.vasp.sets"), self.valid_input_set_name)(
443468
structure=self.user_input.structure,
444469
bandgap=None,
445470
user_incar_settings=incar_updates,

pymatgen/io/validation/validation.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
"""Define core validation schema."""
22

33
from __future__ import annotations
4+
45
from pathlib import Path
5-
from pydantic import BaseModel, Field, PrivateAttr
6+
from pydantic import BaseModel, Field, PrivateAttr, computed_field
67
from typing import TYPE_CHECKING
78

89
from monty.os.path import zpath
@@ -35,6 +36,7 @@ class VaspValidator(BaseModel):
3536

3637
_validated_md5: str | None = PrivateAttr(None)
3738

39+
@computed_field # type: ignore[misc]
3840
@property
3941
def valid(self) -> bool:
4042
"""Determine if the calculation is valid after ensuring inputs have not changed."""
@@ -49,13 +51,16 @@ def has_warnings(self) -> bool:
4951
def recheck(self) -> None:
5052
"""Rerun validation, prioritizing speed."""
5153
new_md5 = None
52-
if self._validated_md5 is None or (new_md5 := self.vasp_files.md5) != self._validated_md5:
54+
if (self._validated_md5 is None) or (new_md5 := self.vasp_files.md5) != self._validated_md5:
55+
self.reasons = []
56+
self.warnings = []
5357

5458
if self.vasp_files.user_input.potcar:
5559
check_list = DEFAULT_CHECKS
5660
else:
5761
check_list = [c for c in DEFAULT_CHECKS if c.__name__ != "CheckPotcar"]
5862
self.reasons, self.warnings = self.run_checks(self.vasp_files, check_list=check_list, fast=True)
63+
5964
self._validated_md5 = new_md5 or self.vasp_files.md5
6065

6166
@staticmethod
@@ -82,6 +87,11 @@ def run_checks(
8287
The first list are all reasons for validation failure,
8388
the second list contains all warnings.
8489
"""
90+
91+
if vasp_files.validation_errors:
92+
# Cannot validate the calculation, immediate failure
93+
return vasp_files.validation_errors, []
94+
8595
reasons: list[str] = []
8696
warnings: list[str] = []
8797
for check in check_list:

pymatgen/io/validation/vasp_defaults.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -922,7 +922,7 @@ def format_val(val: Any) -> Any:
922922
),
923923
VaspParam(
924924
name="LORBIT",
925-
value=None,
925+
value=0,
926926
operation=None,
927927
alias="LORBIT",
928928
tag="misc_special",

pymatgen/io/validation/vasp_defaults.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -557,7 +557,7 @@
557557
warning:
558558
severity: reason
559559
- name: LORBIT
560-
value:
560+
value: 0
561561
operation:
562562
alias: LORBIT
563563
tag: misc_special

tests/test_validation.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,8 @@ def test_common_error_checks(object_name):
319319
GGA="PE",
320320
METAGGA="R2SCAN",
321321
)
322-
VaspFiles(**vfd).valid_input_set
322+
vf_new = VaspFiles(**vfd)
323+
vf_new.valid_input_set
323324

324325
# Drift forces too high check - a warning
325326
vf = copy.deepcopy(vf_og)

0 commit comments

Comments
 (0)