Skip to content

Commit a92b2a9

Browse files
add checks for selective dynamics and non-zero site velocities
1 parent a0e44bc commit a92b2a9

File tree

5 files changed

+95
-3
lines changed

5 files changed

+95
-3
lines changed

pymatgen/io/validation/check_common_errors.py

+68-2
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,18 @@
66

77
from typing import TYPE_CHECKING
88

9+
from emmet.core.vasp.calc_types.enums import RunType, TaskType
10+
from pymatgen.core import Structure
11+
912
from pymatgen.io.validation.common import BaseValidator
1013

1114
if TYPE_CHECKING:
1215
from emmet.core.tasks import TaskDoc
1316
from emmet.core.vasp.calc_types.enums import RunType
1417
from emmet.core.vasp.task_valid import TaskDocument
15-
from pymatgen.core import Structure
16-
from pymatgen.io.vasp import Incar
18+
from pymatgen.io.vasp.inputs import Incar
1719
from typing import Sequence
20+
from numpy.typing import ArrayLike
1821

1922

2023
@dataclass
@@ -276,3 +279,66 @@ def _check_vasp_version(self) -> None:
276279
f"VASP VERSION --> This calculation is using VASP version {vasp_version_str}, "
277280
"but we only allow versions 5.4.4 and >=6.0.0 (as of July 2023)."
278281
)
282+
283+
284+
@dataclass
285+
class CheckStructureProperties(BaseValidator):
286+
"""Check structure for options that are not suitable for thermodynamic calculations."""
287+
288+
reasons: list[str]
289+
warnings: list[str]
290+
structures: list[dict | Structure | None] = None
291+
task_type: TaskType = None
292+
name: str = "VASP POSCAR properties validator"
293+
site_properties_to_check: tuple[str, ...] = ("selective_dynamics", "velocities")
294+
295+
def __post_init__(self) -> None:
296+
"""Extract required structure site properties."""
297+
298+
for idx, struct in enumerate(self.structures):
299+
if isinstance(struct, dict):
300+
self.structures[idx] = Structure.from_dict(struct)
301+
302+
self._site_props = {
303+
k: [struct.site_properties.get(k) for struct in self.structures if struct] # type: ignore[union-attr]
304+
for k in self.site_properties_to_check
305+
}
306+
307+
@staticmethod
308+
def _has_frozen_degrees_of_freedom(selective_dynamics_array: ArrayLike[bool] | None) -> bool:
309+
"""Check selective dynamics array for False values."""
310+
if selective_dynamics_array is None:
311+
return False
312+
return not np.all(selective_dynamics_array)
313+
314+
def _check_selective_dynamics(self) -> None:
315+
"""Check structure for inappropriate site properties."""
316+
317+
if (selec_dyn := self._site_props.get("selective_dynamics")) is not None and self.task_type in {
318+
TaskType.Structure_Optimization,
319+
TaskType.Deformation,
320+
}:
321+
if any(self._has_frozen_degrees_of_freedom(sd_array) for sd_array in selec_dyn):
322+
self.reasons.append(
323+
"Selective dynamics: certain degrees of freedom in the structure "
324+
"were not permitted to relax. To correctly place entries on the convex "
325+
"hull, all degrees of freedom should be allowed to relax."
326+
)
327+
328+
@staticmethod
329+
def _has_nonzero_velocities(velocities: ArrayLike | None, tol: float = 1.0e-8) -> bool:
330+
if velocities is None:
331+
return False
332+
return np.any(np.abs(velocities) > tol)
333+
334+
def _check_velocities(self) -> None:
335+
"""Check structure for non-zero velocities."""
336+
337+
if (velos := self._site_props.get("velocities")) is not None and self.task_type != TaskType.Molecular_Dynamics:
338+
if any(self._has_nonzero_velocities(velo) for velo in velos):
339+
self.warnings.append(
340+
"At least one of the structures had non-zero velocities. "
341+
f"While these are ignored by VASP for {self.task_type} "
342+
"calculations, please ensure that you intended to run a "
343+
"non-molecular dynamics calculation."
344+
)

pymatgen/io/validation/validation.py

+11
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from pymatgen.io.validation.check_common_errors import (
3131
CheckCommonErrors,
3232
CheckVaspVersion,
33+
CheckStructureProperties,
3334
)
3435
from pymatgen.io.validation.check_kpoints_kspacing import CheckKpointsKspacing
3536
from pymatgen.io.validation.check_potcar import CheckPotcar
@@ -173,6 +174,16 @@ def from_dict(
173174
fast=fast,
174175
).check()
175176

177+
CheckStructureProperties(
178+
**{k: cls_kwargs[k] for k in ("reasons", "warnings", "task_type")},
179+
fast=fast,
180+
structures=[
181+
task_doc["input"]["structure"],
182+
task_doc["output"]["structure"],
183+
task_doc["calcs_reversed"][0]["output"]["structure"],
184+
],
185+
).check()
186+
176187
if len(cls_kwargs["reasons"]) > 0 and fast:
177188
return cls(**cls_kwargs)
178189

Binary file not shown.

tests/test_files/vasp/test_GGA_NSCF_calc.json

-1
This file was deleted.

tests/test_validation.py

+16
Original file line numberDiff line numberDiff line change
@@ -644,3 +644,19 @@ def test_fast_mode():
644644
valid_doc = ValidationDoc.from_task_doc(task_doc, check_potcar=False, fast=True)
645645
assert len(valid_doc.reasons) == 1
646646
assert "NBANDS" in valid_doc.reasons[0]
647+
648+
649+
def test_site_properties(test_dir):
650+
651+
task_doc = TaskDoc(**loadfn(test_dir / "vasp" / "mp-1245223_site_props_check.json.gz"))
652+
vd = ValidationDoc.from_task_doc(task_doc)
653+
654+
assert not vd.valid
655+
assert any("selective dynamics" in reason.lower() for reason in vd.reasons)
656+
657+
# map non-zero velocities to input structure and re-check
658+
task_doc.input.structure.add_site_property(
659+
"velocities", task_doc.orig_inputs.poscar.structure.site_properties["velocities"]
660+
)
661+
vd = ValidationDoc.from_task_doc(task_doc)
662+
assert any("non-zero velocities" in warning.lower() for warning in vd.warnings)

0 commit comments

Comments
 (0)