|
6 | 6 |
|
7 | 7 | from typing import TYPE_CHECKING
|
8 | 8 |
|
| 9 | +from emmet.core.vasp.calc_types.enums import RunType, TaskType |
| 10 | +from pymatgen.core import Structure |
| 11 | + |
9 | 12 | from pymatgen.io.validation.common import BaseValidator
|
10 | 13 |
|
11 | 14 | if TYPE_CHECKING:
|
12 | 15 | from emmet.core.tasks import TaskDoc
|
13 | 16 | from emmet.core.vasp.calc_types.enums import RunType
|
14 | 17 | 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 |
17 | 19 | from typing import Sequence
|
| 20 | + from numpy.typing import ArrayLike |
18 | 21 |
|
19 | 22 |
|
20 | 23 | @dataclass
|
@@ -276,3 +279,66 @@ def _check_vasp_version(self) -> None:
|
276 | 279 | f"VASP VERSION --> This calculation is using VASP version {vasp_version_str}, "
|
277 | 280 | "but we only allow versions 5.4.4 and >=6.0.0 (as of July 2023)."
|
278 | 281 | )
|
| 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 | + ) |
0 commit comments