Skip to content

Commit b9920da

Browse files
continue refactor
1 parent b0c96ff commit b9920da

File tree

9 files changed

+2154
-1543
lines changed

9 files changed

+2154
-1543
lines changed

pymatgen/io/validation/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
to ensure that data is compatible with some standard.
55
"""
66

7-
from pymatgen.io.validation.validation import ValidationDoc # noqa: F401
7+
from pymatgen.io.validation.validation import VaspValidator # noqa: F401
88

99
from pymatgen.io.validation.settings import IOValidationSettings as _settings
1010

pymatgen/io/validation/check_common_errors.py

Lines changed: 116 additions & 226 deletions
Large diffs are not rendered by default.

pymatgen/io/validation/check_incar.py

Lines changed: 318 additions & 444 deletions
Large diffs are not rendered by default.

pymatgen/io/validation/check_kpoints_kspacing.py

Lines changed: 55 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,41 @@
11
"""Validate VASP KPOINTS files or the KSPACING/KGAMMA INCAR settings."""
22

33
from __future__ import annotations
4+
from pydantic import Field
5+
from typing import TYPE_CHECKING
46
import numpy as np
5-
from pymatgen.io.vasp import Kpoints
6-
7-
from pymatgen.core import Structure
8-
from pymatgen.io.vasp.sets import VaspInputSet
97

108
from pymatgen.io.validation.common import BaseValidator
119

10+
if TYPE_CHECKING:
11+
from pymatgen.io.validation.common import VaspFiles
12+
13+
1214
class CheckKpointsKspacing(BaseValidator):
13-
"""
14-
Check that k-point density is sufficiently high and is compatible with lattice symmetry.
15-
16-
Parameters
17-
-----------
18-
reasons : list[str]
19-
A list of error strings to update if a check fails. These are higher
20-
severity and would deprecate a calculation.
21-
warnings : list[str]
22-
A list of warning strings to update if a check fails. These are lower
23-
severity and would flag a calculation for possible review.
24-
valid_input_set: VaspInputSet
25-
Valid input set to compare user INCAR parameters to.
26-
kpoints : Kpoints or dict
27-
Kpoints object or its .as_dict() representation used in the calculation.
28-
structure : pymatgen.core.Structure
29-
The structure used in the calculation
30-
name : str = "Check k-point density"
31-
Name of the validator class
32-
fast : bool = False
33-
Whether to perform quick check.
34-
True: stop validation if any check fails.
35-
False: perform all checks.
36-
defaults : dict
37-
Dict of default parameters
38-
kpts_tolerance : float
39-
Tolerance for evaluating k-point density, as the k-point generation
40-
scheme is inconsistent across VASP versions
41-
allow_explicit_kpoint_mesh : str | bool
42-
Whether to permit explicit generation of k-points (as for a bandstructure calculation).
43-
allow_kpoint_shifts : bool
44-
Whether to permit shifting the origin of the k-point mesh from Gamma.
45-
"""
46-
47-
reasons: list[str]
48-
warnings: list[str]
15+
"""Check that k-point density is sufficiently high and is compatible with lattice symmetry."""
16+
4917
name: str = "Check k-point density"
50-
valid_input_set: VaspInputSet = None
51-
kpoints: Kpoints = None
52-
structure: Structure = None
53-
defaults: dict | None = None
54-
kpts_tolerance: float | None = None
55-
allow_explicit_kpoint_mesh: str | bool = False
56-
allow_kpoint_shifts: bool = False
57-
58-
def _get_valid_num_kpts(self) -> int:
18+
kpts_tolerance: float | None = Field(
19+
None,
20+
description="Tolerance for evaluating k-point density, to accommodate different the k-point generation schemes across VASP versions.",
21+
)
22+
allow_explicit_kpoint_mesh: bool = Field(
23+
False, description="Whether to permit explicit generation of k-points (as for a bandstructure calculation)."
24+
)
25+
allow_kpoint_shifts: bool = Field(
26+
False, description="Whether to permit shifting the origin of the k-point mesh from Gamma."
27+
)
28+
29+
def auto_fail(self, vasp_files: VaspFiles, reasons: list[str], warnings: list[str]) -> bool:
30+
"""Quick stop if actual k-points are missing."""
31+
if vasp_files.kpoints is None:
32+
reasons.append("Missing actual k-points: please specify an IBZKPT or vasprun.xml in VaspFiles.")
33+
return vasp_files.kpoints is None
34+
35+
def _get_valid_num_kpts(
36+
self,
37+
vasp_files: VaspFiles,
38+
) -> int:
5939
"""
6040
Get the minimum permitted number of k-points for a structure according to an input set.
6141
@@ -64,68 +44,71 @@ def _get_valid_num_kpts(self) -> int:
6444
int, the minimum permitted number of k-points, consistent with self.kpts_tolerance
6545
"""
6646
# If MP input set specifies KSPACING in the INCAR
67-
if ("KSPACING" in self.valid_input_set.incar.keys()) and (self.valid_input_set.kpoints is None):
68-
valid_kspacing = self.valid_input_set.incar.get("KSPACING", self.defaults["KSPACING"]["value"])
47+
if ("KSPACING" in vasp_files.valid_input_set.incar.keys()) and (vasp_files.valid_input_set.kpoints is None):
48+
valid_kspacing = vasp_files.valid_input_set.incar.get("KSPACING", self.vasp_defaults["KSPACING"].value)
6949
# number of kpoints along each of the three lattice vectors
7050
nk = [
71-
max(1, np.ceil(self.structure.lattice.reciprocal_lattice.abc[ik] / valid_kspacing)) for ik in range(3)
51+
max(1, np.ceil(vasp_files.structure.lattice.reciprocal_lattice.abc[ik] / valid_kspacing))
52+
for ik in range(3)
7253
]
7354
valid_num_kpts = np.prod(nk)
7455
# If MP input set specifies a KPOINTS file
7556
else:
76-
valid_num_kpts = self.valid_input_set.kpoints.num_kpts or np.prod(self.valid_input_set.kpoints.kpts[0])
57+
valid_num_kpts = vasp_files.valid_input_set.kpoints.num_kpts or np.prod(
58+
vasp_files.valid_input_set.kpoints.kpts[0]
59+
)
7760

7861
return int(np.floor(int(valid_num_kpts) * self.kpts_tolerance))
7962

80-
def _check_user_shifted_mesh(self) -> None:
63+
def _check_user_shifted_mesh(self, vasp_files: VaspFiles, reasons: list[str], warnings: list[str]) -> None:
8164
# Check for user shifts
82-
if (not self.allow_kpoint_shifts) and any(shift_val != 0 for shift_val in self.kpoints.kpts_shift):
83-
self.reasons.append("INPUT SETTINGS --> KPOINTS: shifting the kpoint mesh is not currently allowed.")
65+
if (not self.allow_kpoint_shifts) and any(shift_val != 0 for shift_val in vasp_files.kpoints.kpts_shift):
66+
reasons.append("INPUT SETTINGS --> KPOINTS: shifting the kpoint mesh is not currently allowed.")
8467

85-
def _check_explicit_mesh_permitted(self) -> None:
68+
def _check_explicit_mesh_permitted(self, vasp_files: VaspFiles, reasons: list[str], warnings: list[str]) -> None:
8669
# Check for explicit kpoint meshes
8770

88-
if (not self.allow_explicit_kpoint_mesh) and len(self.kpoints.kpts) > 1:
89-
self.reasons.append(
71+
if (not self.allow_explicit_kpoint_mesh) and len(vasp_files.kpoints.kpts) > 1:
72+
reasons.append(
9073
"INPUT SETTINGS --> KPOINTS: explicitly defining "
9174
"the k-point mesh is not currently allowed. "
9275
"Automatic k-point generation is required."
9376
)
9477

95-
def _check_kpoint_density(self) -> None:
78+
def _check_kpoint_density(self, vasp_files: VaspFiles, reasons: list[str], warnings: list[str]) -> None:
9679
"""
9780
Check that k-point density is sufficiently high and is compatible with lattice symmetry.
9881
"""
9982

10083
# Check number of kpoints used
101-
valid_num_kpts = self._get_valid_num_kpts()
84+
valid_num_kpts = self._get_valid_num_kpts(vasp_files)
10285

10386
cur_num_kpts = max(
104-
self.kpoints.num_kpts,
105-
np.prod(self.kpoints.kpts),
106-
len(self.kpoints.kpts),
87+
vasp_files.kpoints.num_kpts,
88+
np.prod(vasp_files.kpoints.kpts),
89+
len(vasp_files.kpoints.kpts),
10790
)
10891
if cur_num_kpts < valid_num_kpts:
109-
self.reasons.append(
92+
reasons.append(
11093
f"INPUT SETTINGS --> KPOINTS or KSPACING: {cur_num_kpts} kpoints were "
11194
f"used, but it should have been at least {valid_num_kpts}."
11295
)
11396

114-
def _check_kpoint_mesh_symmetry(self) -> None:
97+
def _check_kpoint_mesh_symmetry(self, vasp_files: VaspFiles, reasons: list[str], warnings: list[str]) -> None:
11598
# check for valid kpoint mesh (which depends on symmetry of the structure)
11699

117-
cur_kpoint_style = self.kpoints.style.name.lower()
118-
is_hexagonal = self.structure.lattice.is_hexagonal()
119-
is_face_centered = self.structure.get_space_group_info()[0][0] == "F"
100+
cur_kpoint_style = vasp_files.kpoints.style.name.lower()
101+
is_hexagonal = vasp_files.structure.lattice.is_hexagonal()
102+
is_face_centered = vasp_files.structure.get_space_group_info()[0][0] == "F"
120103
monkhorst_mesh_is_invalid = is_hexagonal or is_face_centered
121104
if (
122105
cur_kpoint_style == "monkhorst"
123106
and monkhorst_mesh_is_invalid
124-
and any(x % 2 == 0 for x in self.kpoints.kpts[0])
107+
and any(x % 2 == 0 for x in vasp_files.kpoints.kpts[0])
125108
):
126109
# only allow Monkhorst with all odd number of subdivisions per axis.
127-
kx, ky, kz = self.kpoints.kpts[0]
128-
self.reasons.append(
110+
kx, ky, kz = vasp_files.kpoints.kpts[0]
111+
reasons.append(
129112
f"INPUT SETTINGS --> KPOINTS or KGAMMA: ({kx}x{ky}x{kz}) "
130113
"Monkhorst-Pack kpoint mesh was used."
131114
"To be compatible with the symmetry of the lattice, "
Lines changed: 45 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,61 @@
11
"""Check POTCAR against known POTCARs in pymatgen, without setting up psp_resources."""
22

33
from __future__ import annotations
4+
from functools import cached_property
45
from pydantic import Field
56
from importlib.resources import files as import_resource_files
67
from monty.serialization import loadfn
7-
import numpy as np
8-
9-
from pymatgen.core import Structure
10-
from pymatgen.io.vasp.sets import VaspInputSet
8+
from typing import TYPE_CHECKING
119

1210
from pymatgen.io.validation.common import BaseValidator
1311

14-
_potcar_summary_stats = loadfn(import_resource_files("pymatgen.io.vasp") / "potcar-summary-stats.json.bz2")
12+
if TYPE_CHECKING:
13+
from pathlib import Path
14+
from pymatgen.io.validation.common import VaspFiles
15+
1516

1617
class CheckPotcar(BaseValidator):
1718
"""
1819
Check POTCAR against library of known valid POTCARs.
19-
20-
reasons : list[str]
21-
A list of error strings to update if a check fails. These are higher
22-
severity and would deprecate a calculation.
23-
warnings : list[str]
24-
A list of warning strings to update if a check fails. These are lower
25-
severity and would flag a calculation for possible review.
26-
valid_input_set: VaspInputSet
27-
Valid input set to compare user INCAR parameters to.
28-
structure: Pymatgen Structure
29-
Structure used in the calculation.
30-
potcar: dict
31-
Spec (symbol, hash, and summary stats) for the POTCAR used in the calculation.
32-
name : str = "Check POTCARs"
33-
Name of the validator class
34-
fast : bool = False
35-
Whether to perform quick check.
36-
True: stop validation if any check fails.
37-
False: perform all checks.
38-
potcar_summary_stats : dict
39-
Dictionary of potcar summary data. Mapping is calculation type -> potcar symbol -> summary data.
40-
data_match_tol : float = 1.e-6
41-
Tolerance for matching POTCARs to summary statistics data.
42-
fast : bool = False
43-
True: stop validation when any single check fails
4420
"""
4521

46-
reasons: list[str]
47-
warnings: list[str]
48-
valid_input_set: VaspInputSet = None
49-
structure: Structure = None
50-
potcars: list[dict] = None
51-
name: str = "Check POTCARs"
52-
potcar_summary_stats: dict = Field(default_factory=lambda: _potcar_summary_stats)
53-
data_match_tol: float = 1.0e-6
54-
fast: bool = False
55-
56-
def _check_potcar_spec(self):
57-
"""
58-
Checks to make sure the POTCAR is equivalent to the correct POTCAR from the pymatgen input set."""
59-
60-
if not self.potcar_summary_stats:
61-
# If no reference summary stats specified, or we're only doing a quick check,
62-
# and there are already failure reasons, return
63-
return
64-
65-
if self.potcars is None or any(potcar.get("summary_stats") is None for potcar in self.potcars):
66-
self.reasons.append(
22+
name: str = "Check POTCAR"
23+
potcar_summary_stats_path: str | Path = Field(
24+
import_resource_files("pymatgen.io.vasp") / "potcar-summary-stats.json.bz2",
25+
description="Path to potcar summary data. Mapping is calculation type -> potcar symbol -> summary data.",
26+
)
27+
data_match_tol: float = Field(1.0e-6, description="Tolerance for matching POTCARs to summary statistics data.")
28+
29+
@cached_property
30+
def potcar_summary_stats(self) -> dict | None:
31+
"""Load POTCAR summary statistics file."""
32+
if self.potcar_summary_stats_path:
33+
return loadfn(self.potcar_summary_stats_path, cls=None)
34+
return
35+
36+
def auto_fail(self, vasp_files: VaspFiles, reasons: list[str], warnings: list[str]) -> bool:
37+
"""Skip if no POTCAR was provided, or if summary stats file was unset."""
38+
if vasp_files.potcar is None:
39+
reasons.append(
6740
"PSEUDOPOTENTIALS --> Missing POTCAR files. "
6841
"Alternatively, our potcar checker may have an issue--please create a GitHub issue if you "
6942
"know your POTCAR exists and can be read by Pymatgen."
7043
)
71-
return
44+
elif self.potcar_summary_stats is None:
45+
# If no reference summary stats specified, or we're only doing a quick check,
46+
# and there are already failure reasons, return
47+
return True
48+
return vasp_files.potcar is None
7249

73-
psp_subset = self.potcar_summary_stats.get(self.valid_input_set._config_dict["POTCAR_FUNCTIONAL"], {})
50+
def _check_potcar_spec(self, vasp_files: VaspFiles, reasons: list[str], warnings: list[str]):
51+
"""
52+
Checks to make sure the POTCAR is equivalent to the correct POTCAR from the pymatgen input set."""
53+
54+
psp_subset = self.potcar_summary_stats.get(vasp_files.valid_input_set._config_dict["POTCAR_FUNCTIONAL"], {})
7455

7556
valid_potcar_summary_stats = {} # type: ignore
76-
for element in self.structure.composition.remove_charges().as_dict():
77-
potcar_symbol = self.valid_input_set._config_dict["POTCAR"][element]
57+
for element in vasp_files.structure.composition.remove_charges().as_dict():
58+
potcar_symbol = vasp_files.valid_input_set._config_dict["POTCAR"][element]
7859
for titel_no_spc in psp_subset:
7960
for psp in psp_subset[titel_no_spc]:
8061
if psp["symbol"] == potcar_symbol:
@@ -84,15 +65,17 @@ def _check_potcar_spec(self):
8465

8566
try:
8667
incorrect_potcars = []
87-
for potcar in self.potcars:
88-
reference_summary_stats = valid_potcar_summary_stats.get(potcar["titel"].replace(" ", ""), [])
68+
for potcar in vasp_files.potcar:
69+
reference_summary_stats = valid_potcar_summary_stats.get(potcar.TITEL.replace(" ", ""), [])
8970

9071
if len(reference_summary_stats) == 0:
91-
incorrect_potcars.append(potcar["titel"].split(" ")[1])
72+
incorrect_potcars.append(potcar.TITEL.split(" ")[1])
9273
continue
9374

9475
for ref_psp in reference_summary_stats:
95-
if found_match := self.compare_potcar_stats(ref_psp, potcar["summary_stats"]):
76+
if found_match := potcar.compare_potcar_stats(
77+
ref_psp, potcar._summary_stats, tolerance=self.data_match_tol
78+
):
9679
break
9780

9881
if not found_match:
@@ -113,55 +96,15 @@ def _check_potcar_spec(self):
11396
", ".join(incorrect_potcars[:-1]) + "," + f" and {incorrect_potcars[-1]}"
11497
) # type: ignore
11598

116-
self.reasons.append(
99+
reasons.append(
117100
f"PSEUDOPOTENTIALS --> Incorrect POTCAR files were used for {incorrect_potcars}. "
118101
"Alternatively, our potcar checker may have an issue--please create a GitHub issue if you "
119102
"believe the POTCARs used are correct."
120103
)
121104

122-
except KeyError as e:
123-
print(f"POTCAR check exception: {e}")
124-
# Assume it is an old calculation without potcar_spec data and treat it as failing the POTCAR check
125-
self.reasons.append(
105+
except KeyError:
106+
reasons.append(
126107
"Issue validating POTCARS --> Likely due to an old version of Emmet "
127108
"(wherein potcar summary_stats is not saved in TaskDoc), though "
128109
"other errors have been seen. Hence, it is marked as invalid."
129110
)
130-
131-
def compare_potcar_stats(self, potcar_stats_1: dict, potcar_stats_2: dict) -> bool:
132-
"""Utility function to compare PotcarSingle._summary_stats."""
133-
134-
if not all(
135-
potcar_stats_1.get(key)
136-
for key in (
137-
"keywords",
138-
"stats",
139-
)
140-
) or (
141-
not all(
142-
potcar_stats_2.get(key)
143-
for key in (
144-
"keywords",
145-
"stats",
146-
)
147-
)
148-
):
149-
return False
150-
151-
key_match = all(
152-
set(potcar_stats_1["keywords"].get(key)) == set(potcar_stats_2["keywords"].get(key)) # type: ignore
153-
for key in ["header", "data"]
154-
)
155-
156-
data_match = False
157-
if key_match:
158-
data_diff = [
159-
abs(
160-
potcar_stats_1["stats"].get(key, {}).get(stat) - potcar_stats_2["stats"].get(key, {}).get(stat)
161-
) # type: ignore
162-
for stat in ["MEAN", "ABSMEAN", "VAR", "MIN", "MAX"]
163-
for key in ["header", "data"]
164-
]
165-
data_match = all(np.array(data_diff) < self.data_match_tol)
166-
167-
return key_match and data_match

0 commit comments

Comments
 (0)