Skip to content

Commit 34d1704

Browse files
finish tests
1 parent 6d8f101 commit 34d1704

21 files changed

+1100
-876
lines changed

pymatgen/io/validation/check_common_errors.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,9 @@
55
import numpy as np
66
from typing import TYPE_CHECKING
77

8-
from pymatgen.io.validation.common import BaseValidator
8+
from pymatgen.io.validation.common import SETTINGS, BaseValidator
99
from pymatgen.io.validation.settings import IOValidationSettings
1010

11-
SETTINGS = IOValidationSettings()
12-
1311
if TYPE_CHECKING:
1412
from numpy.typing import ArrayLike
1513

@@ -232,7 +230,6 @@ def _has_frozen_degrees_of_freedom(selective_dynamics_array: ArrayLike[bool] | N
232230

233231
def _check_selective_dynamics(self, vasp_files: VaspFiles, reasons: list[str], warnings: list[str]) -> None:
234232
"""Check structure for inappropriate site properties."""
235-
236233
if (
237234
selec_dyn := vasp_files.user_input.structure.site_properties.get("selective_dynamics")
238235
) is not None and vasp_files.run_type == "relax":

pymatgen/io/validation/check_incar.py

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,17 @@
44
import numpy as np
55
from pydantic import Field
66

7-
from pymatgen.io.vasp import Incar
8-
9-
from pymatgen.io.validation.common import BaseValidator, VaspFiles
7+
from pymatgen.io.validation.common import SETTINGS, BaseValidator, VaspFiles
108
from pymatgen.io.validation.vasp_defaults import InputCategory, VaspParam
119

1210
from typing import TYPE_CHECKING
1311

1412
if TYPE_CHECKING:
13+
from typing import Any
1514
from pymatgen.io.validation.common import VaspFiles
1615

1716
# TODO: fix ISIF getting overwritten by MP input set.
1817

19-
2018
class CheckIncar(BaseValidator):
2119
"""
2220
Check calculation parameters related to INCAR input tags.
@@ -37,7 +35,8 @@ class CheckIncar(BaseValidator):
3735

3836
name: str = "Check INCAR tags"
3937
fft_grid_tolerance: float | None = Field(
40-
None, description="Tolerance for determining sufficient density of FFT grid."
38+
SETTINGS.VASP_FFT_GRID_TOLERANCE,
39+
description="Tolerance for determining sufficient density of FFT grid."
4140
)
4241
bandgap_tol: float = Field(1.0e-4, description="Tolerance for assuming a material has no gap.")
4342

@@ -70,11 +69,10 @@ def check(self, vasp_files: VaspFiles, reasons: list[str], warnings: list[str])
7069
if self.fast and len(reasons) > 0:
7170
# fast check: stop checking whenever a single check fails
7271
break
73-
7472
resp = vasp_param.check(user_incar_params[vasp_param.name], valid_incar_params[vasp_param.name])
7573
msgs[vasp_param.severity].extend(resp.get(vasp_param.severity, []))
7674

77-
def update_parameters_and_defaults(self, vasp_files: VaspFiles) -> tuple[Incar, Incar]:
75+
def update_parameters_and_defaults(self, vasp_files: VaspFiles) -> tuple[dict[str,Any], dict[str,Any]]:
7876
"""Update a set of parameters according to supplied rules and defaults.
7977
8078
While many of the parameters in VASP need only a simple check to determine
@@ -299,10 +297,19 @@ def _update_hybrid_params(self, user_incar: dict, ref_incar: dict, vasp_files: V
299297
def _update_fft_params(self, user_incar: dict, ref_incar: dict, vasp_files: VaspFiles) -> None:
300298
"""Update ENCUT and parameters related to the FFT grid."""
301299

300+
# ensure that ENCUT is appropriately updated
301+
user_incar["ENMAX"] = user_incar.get(
302+
"ENCUT",
303+
getattr(vasp_files.vasprun,"parameters",{}).get("ENMAX")
304+
)
305+
306+
ref_incar["ENMAX"] = vasp_files.valid_input_set.incar.get("ENCUT", self.vasp_defaults["ENMAX"])
307+
302308
grid_keys = {"NGX", "NGXF", "NGY", "NGYF", "NGZ", "NGZF"}
303309
# NGX/Y/Z and NGXF/YF/ZF. Not checked if not in INCAR file (as this means the VASP default was used).
304310
if any(i for i in grid_keys if i in user_incar.keys()):
305-
ref_incar["ENMAX"] = max(user_incar["ENMAX"], ref_incar["ENMAX"])
311+
enmaxs = [user_incar["ENMAX"], ref_incar["ENMAX"]]
312+
ref_incar["ENMAX"] = max([v for v in enmaxs if v < float("inf")])
306313

307314
(
308315
[
@@ -315,10 +322,10 @@ def _update_fft_params(self, user_incar: dict, ref_incar: dict, vasp_files: Vasp
315322
ref_incar["NGYF"],
316323
ref_incar["NGZF"],
317324
],
318-
) = vasp_files.valid_input_set.calculate_ng(custom_encut=ref_incar["ENMAX"])
325+
) = vasp_files.valid_input_set._calculate_ng(custom_encut=ref_incar["ENMAX"])
319326

320327
for key in grid_keys:
321-
ref_incar[key] = int(ref_incar[key] * self._fft_grid_tolerance)
328+
ref_incar[key] = int(ref_incar[key] * self.fft_grid_tolerance)
322329

323330
self.vasp_defaults[key] = VaspParam(
324331
name=key,
@@ -379,7 +386,7 @@ def _update_smearing_params(self, user_incar: dict, ref_incar: dict, vasp_files:
379386
380387
This is based on the final bandgap obtained in the calc.
381388
"""
382-
if vasp_files.bandgap:
389+
if vasp_files.bandgap is not None:
383390

384391
smearing_comment = (
385392
f"This is flagged as incorrect because this calculation had a bandgap of {round(vasp_files.bandgap,3)}"
@@ -405,8 +412,9 @@ def _update_smearing_params(self, user_incar: dict, ref_incar: dict, vasp_files:
405412
for key in ["ISMEAR", "SIGMA"]:
406413
self.vasp_defaults[key].comment = smearing_comment
407414

408-
else:
415+
if user_incar["ISMEAR"] not in [-5, -4, -2]:
409416
self.vasp_defaults["SIGMA"].operation = "<="
417+
410418
else:
411419
# These are generally applicable in all cases. Loosen check to warning.
412420
ref_incar["ISMEAR"] = [-1, 0]
@@ -418,7 +426,7 @@ def _update_smearing_params(self, user_incar: dict, ref_incar: dict, vasp_files:
418426
"may lead to significant errors in forces. To enable this check, "
419427
"supply a vasprun.xml file."
420428
)
421-
self.vasp_defaults["ISMEAR"].severity = "warning"
429+
self.vasp_defaults["ISMEAR"].severity = "warning"
422430

423431
# Also check if SIGMA is too large according to the VASP wiki,
424432
# which occurs when the entropy term in the energy is greater than 1 meV/atom.
@@ -485,7 +493,7 @@ def _update_electronic_params(self, user_incar: dict, ref_incar: dict, vasp_file
485493

486494
# ENAUG. Should only be checked for calculations where the relevant MP input set specifies ENAUG.
487495
# In that case, ENAUG should be the same or greater than in valid_input_set.
488-
if ref_incar.get("ENAUG"):
496+
if ref_incar.get("ENAUG") < float("inf"):
489497
self.vasp_defaults["ENAUG"].operation = ">="
490498

491499
# IALGO.
@@ -504,12 +512,18 @@ def _update_electronic_params(self, user_incar: dict, ref_incar: dict, vasp_file
504512
f"NELECT should be set to {nelect + user_incar['NELECT']} instead."
505513
)
506514
except Exception:
507-
self.vasp_defaults["NELECT"].operation = "auto fail"
508-
self.vasp_defaults["NELECT"].alias = "NELECT / POTCAR"
509-
self.vasp_defaults["NELECT"].comment = (
510-
"sIssue checking whether NELECT was changed to make "
511-
"the structure have a non-zero charge. This is likely due to the "
512-
"directory not having a POTCAR file."
515+
self.vasp_defaults["NELECT"] = VaspParam(
516+
name = "NELECT",
517+
value = None,
518+
tag = "electronic",
519+
operation= "auto fail",
520+
severity="warning",
521+
alias = "NELECT / POTCAR",
522+
comment=(
523+
"Issue checking whether NELECT was changed to make "
524+
"the structure have a non-zero charge. This is likely due to the "
525+
"directory not having a POTCAR file."
526+
)
513527
)
514528

515529
# NBANDS.
@@ -535,7 +549,10 @@ def _update_ionic_params(self, user_incar: dict, ref_incar: dict, vasp_files: Va
535549

536550
# IBRION.
537551
ref_incar["IBRION"] = [-1, 1, 2]
538-
if (inp_set_ibrion := vasp_files.user_input.incar.get("IBRION")) and inp_set_ibrion not in ref_incar["IBRION"]:
552+
if (
553+
(inp_set_ibrion := vasp_files.valid_input_set.incar.get("IBRION"))
554+
and inp_set_ibrion not in ref_incar["IBRION"]
555+
):
539556
ref_incar["IBRION"].append(inp_set_ibrion)
540557

541558
ionic_steps = []

pymatgen/io/validation/check_kpoints_kspacing.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@
55
from typing import TYPE_CHECKING
66
import numpy as np
77

8-
from pymatgen.io.validation.common import BaseValidator
9-
from pymatgen.io.validation.settings import IOValidationSettings
10-
11-
SETTINGS = IOValidationSettings()
8+
from pymatgen.io.validation.common import SETTINGS, BaseValidator
129

1310
if TYPE_CHECKING:
1411
from pymatgen.io.validation.common import VaspFiles
@@ -22,7 +19,7 @@ class CheckKpointsKspacing(BaseValidator):
2219
SETTINGS.VASP_KPTS_TOLERANCE,
2320
description="Tolerance for evaluating k-point density, to accommodate different the k-point generation schemes across VASP versions.",
2421
)
25-
allow_explicit_kpoint_mesh: bool = Field(
22+
allow_explicit_kpoint_mesh: bool | str | None = Field(
2623
SETTINGS.VASP_ALLOW_EXPLICIT_KPT_MESH,
2724
description="Whether to permit explicit generation of k-points (as for a bandstructure calculation).",
2825
)
@@ -73,7 +70,14 @@ def _check_user_shifted_mesh(self, vasp_files: VaspFiles, reasons: list[str], wa
7370
def _check_explicit_mesh_permitted(self, vasp_files: VaspFiles, reasons: list[str], warnings: list[str]) -> None:
7471
# Check for explicit kpoint meshes
7572

76-
if (not self.allow_explicit_kpoint_mesh) and len(vasp_files.actual_kpoints.kpts) > 1:
73+
if isinstance(self.allow_explicit_kpoint_mesh,bool):
74+
allow_explicit = self.allow_explicit_kpoint_mesh
75+
elif self.allow_explicit_kpoint_mesh == "auto":
76+
allow_explicit = vasp_files.run_type == "nonscf"
77+
else:
78+
allow_explicit = False
79+
80+
if (not allow_explicit) and len(vasp_files.actual_kpoints.kpts) > 1:
7781
reasons.append(
7882
"INPUT SETTINGS --> KPOINTS: explicitly defining "
7983
"the k-point mesh is not currently allowed. "

pymatgen/io/validation/common.py

Lines changed: 48 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@
1414
from pymatgen.io.vasp.sets import VaspInputSet
1515

1616
from pymatgen.io.validation.vasp_defaults import VaspParam, VASP_DEFAULTS_DICT
17+
from pymatgen.io.validation.settings import IOValidationSettings
1718

1819
if TYPE_CHECKING:
1920
from typing_extensions import Self
2021

22+
SETTINGS = IOValidationSettings()
2123

2224
class ValidationError(Exception):
2325
"""Define custom exception during validation."""
@@ -32,6 +34,13 @@ class _PotcarSummaryStatsKeywords(BaseModel):
3234
header: set[str] = Field(description="The keywords in the POTCAR header.")
3335
data: set[str] = Field(description="The keywords in the POTCAR body.")
3436

37+
@model_serializer
38+
def set_to_list(self) -> dict[str,list[str]]:
39+
"""Ensure JSON compliance of set fields."""
40+
return {
41+
k : list(getattr(self,k)) for k in ("header","data")
42+
}
43+
3544
class _PotcarSummaryStatsStats(BaseModel):
3645
"""Schematize `PotcarSingle._summary_stats["stats"]` field."""
3746

@@ -79,6 +88,7 @@ class LightVasprun(BaseModel):
7988
kpoints: Kpoints = Field(description="The actual k-points used in the calculation.")
8089
parameters: dict[str, Any] = Field(description="The default-padded input parameters interpreted by VASP.")
8190
bandgap: float = Field(description="The bandgap - note that this field is derived from the Vasprun object.")
91+
potcar_symbols : list[str] | None = Field(None, description="Optional: if a POTCAR is unavailable, this is used to determine the functional used in the calculation.")
8292

8393
@classmethod
8494
def from_vasprun(cls, vasprun: Vasprun) -> Self:
@@ -95,6 +105,7 @@ class VaspInputSafe(BaseModel):
95105
structure: Structure = Field(description="The structure associated with the calculation.")
96106
kpoints: Kpoints | None = Field(None, description="The optional KPOINTS or IBZKPT file used in the calculation.")
97107
potcar: list[PotcarSummaryStats] | None = Field(None, description="The optional POTCAR used in the calculation.")
108+
_pmg_vis : VaspInputSet | None = PrivateAttr(None)
98109

99110
@model_serializer
100111
def deserialize_objects(self) -> dict[str, Any]:
@@ -111,7 +122,7 @@ def deserialize_objects(self) -> dict[str, Any]:
111122

112123
@classmethod
113124
def from_vasp_input_set(cls, vis: VaspInputSet) -> Self:
114-
return cls(
125+
new_vis = cls(
115126
**{
116127
k: getattr(vis, k)
117128
for k in (
@@ -122,36 +133,23 @@ def from_vasp_input_set(cls, vis: VaspInputSet) -> Self:
122133
},
123134
potcar=PotcarSummaryStats.from_file(vis.potcar),
124135
)
136+
new_vis._pmg_vis = vis
137+
return new_vis
138+
139+
140+
def _calculate_ng(self, **kwargs) -> tuple[list[int], list[int]] | None:
141+
"""Interface to pymatgen vasp input set as needed."""
142+
if self._pmg_vis:
143+
return self._pmg_vis.calculate_ng(**kwargs)
144+
return None
125145

126146

127147
class VaspFiles(BaseModel):
128148
"""Define required and optional files for validation."""
129149

130-
user_input: VaspInputSafe = Field(description="The VASP input set used in the calculation.")
131-
_outcar: os.PathLike | Outcar | LightOutcar | None = PrivateAttr(None)
132-
_vasprun: os.PathLike | Vasprun | LightVasprun | None = PrivateAttr(None)
133-
134-
@cached_property
135-
def outcar(self) -> LightOutcar | None:
136-
"""The optional OUTCAR."""
137-
if self._outcar:
138-
if not isinstance(self._outcar, Outcar | LightOutcar):
139-
self._outcar = Outcar(self._outcar)
140-
if isinstance(self._outcar, Outcar):
141-
return LightOutcar(drift=self._outcar.drift, magnetization=self._outcar.magnetization)
142-
return self._outcar
143-
return None
144-
145-
@cached_property
146-
def vasprun(self) -> LightVasprun | None:
147-
"""The optional vasprun.xml."""
148-
if self._vasprun:
149-
if not isinstance(self._vasprun, Vasprun | LightVasprun):
150-
self._vasprun = Vasprun(self._vasprun)
151-
if isinstance(self._vasprun, Vasprun):
152-
return LightVasprun.from_vasprun(self._vasprun)
153-
return self._vasprun
154-
return None
150+
user_input : VaspInputSafe = Field(description="The VASP input set used in the calculation.")
151+
outcar: LightOutcar | None = None
152+
vasprun: LightVasprun | None = None
155153

156154
@property
157155
def actual_kpoints(self) -> Kpoints | None:
@@ -188,22 +186,34 @@ def from_paths(
188186
"kpoints": Kpoints,
189187
"poscar": Poscar,
190188
"potcar": PotcarSummaryStats,
189+
"outcar": Outcar,
190+
"vasprun": Vasprun,
191191
}
192+
potcar_enmax = None
192193
for file_name, file_cls in to_obj.items():
193194
if (path := _vars.get(file_name)) and Path(path).exists():
194195
if file_name == "poscar":
195196
config["user_input"]["structure"] = file_cls.from_file(path).structure
196-
else:
197+
elif hasattr(file_cls,"from_file"):
197198
config["user_input"][file_name] = file_cls.from_file(path)
199+
else:
200+
config[file_name] = file_cls(path)
198201

199-
vf = cls(**config)
200-
for file_name in ("outcar", "vasprun"):
201-
if (path := _vars.get(file_name)) and Path(path).exists():
202-
setattr(vf, f"_{file_name}", path)
202+
if file_name == "potcar":
203+
potcar_enmax = max(ps.ENMAX for ps in Potcar.from_file(path))
204+
205+
if config.get("outcar"):
206+
config["outcar"] = LightOutcar(
207+
drift = config["outcar"].drift, magnetization=config["outcar"].magnetization,
208+
)
209+
if config.get("vasprun"):
210+
config["vasprun"] = LightVasprun.from_vasprun(config["vasprun"])
211+
else:
212+
if not config["incar"].get("ENCUT") and potcar_enmax:
213+
config["incar"]["ENCUT"] = potcar_enmax
203214

204-
return vf
215+
return cls(**config)
205216

206-
@computed_field # type: ignore[misc]
207217
@cached_property
208218
def run_type(self) -> str:
209219
"""Get the run type of a calculation."""
@@ -234,7 +244,6 @@ def run_type(self) -> str:
234244

235245
return run_type
236246

237-
@computed_field # type: ignore[misc]
238247
@cached_property
239248
def functional(self) -> str:
240249
"""Determine the functional used in the calculation.
@@ -247,6 +256,9 @@ def functional(self) -> str:
247256
func_from_potcar = None
248257
if self.user_input.potcar:
249258
func_from_potcar = {"pe": "pbe", "ca": "lda"}.get(self.user_input.potcar[0].lexch.lower())
259+
elif self.vasprun and self.vasprun.potcar_symbols:
260+
pot_func = self.vasprun.potcar_symbols[0].split()[0].split("_")[-1]
261+
func_from_potcar = "pbe" if pot_func == "PBE" else "lda"
250262

251263
if gga := self.user_input.incar.get("GGA"):
252264
if gga.lower() == "pe":
@@ -293,7 +305,6 @@ def bandgap(self) -> float | None:
293305
return self.vasprun.bandgap
294306
return None
295307

296-
@computed_field # type: ignore[misc]
297308
@cached_property
298309
def valid_input_set(self) -> VaspInputSafe:
299310
"""
@@ -310,6 +321,8 @@ def valid_input_set(self) -> VaspInputSafe:
310321
set_name = "MPNonSCFSet"
311322
elif self.run_type == "nmr":
312323
set_name = "MPNMRSet"
324+
elif self.run_type == "md":
325+
set_name = None
313326
else:
314327
set_name = f"MP{self.run_type.capitalize()}Set"
315328
elif self.functional in ("pbesol", "scan", "r2scan", "hse06"):

0 commit comments

Comments
 (0)