diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 90ab16fbf3..04545793ac 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -3,9 +3,9 @@ name: testing on: push: branches: [main] - tags: ['v*'] + tags: ["v*"] pull_request: - branches: [main] + workflow_dispatch: jobs: lint: @@ -15,7 +15,7 @@ jobs: - uses: actions/setup-python@v4 with: - python-version: "3.8" + python-version: "3.9" cache: pip cache-dependency-path: pyproject.toml @@ -31,7 +31,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.9", "3.10"] + python-version: ["3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v3 @@ -43,9 +43,16 @@ jobs: cache-dependency-path: pyproject.toml - name: Install dependencies + # ERROR: Cannot install atomate2 and atomate2[strict,tests]==0.0.1 because these package versions have conflicting dependencies. + # The conflict is caused by: + # atomate2[strict,tests] 0.0.1 depends on pymatgen>=2023.10.4 + # atomate2[strict,tests] 0.0.1 depends on pymatgen==2023.10.4; extra == "strict" + # ERROR: ResolutionImpossible: for help visit https://pip.pypa.io/en/latest/topics/dependency-resolution/#dealing-with-dependency-conflicts + # chgnet 0.2.1 depends on pymatgen>=2023.5.31 + # emmet-core 0.70.0 depends on pymatgen>=2023.10.11 run: | python -m pip install --upgrade pip - pip install .[strict,tests] + pip install .[strict,tests] - name: Test env: diff --git a/.github/workflows/update-precommit.yml b/.github/workflows/update-precommit.yml index d93aa8d165..0d6a43f422 100644 --- a/.github/workflows/update-precommit.yml +++ b/.github/workflows/update-precommit.yml @@ -8,14 +8,14 @@ on: jobs: auto-update: runs-on: ubuntu-latest - + steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: - python-version: 3.8 + python-version: 3.9 - name: Install pre-commit run: pip install pre-commit diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b3a247f078..19ff60e958 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,14 +1,13 @@ default_language_version: python: python3 -exclude: '^.github/' repos: - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.287 + rev: v0.1.1 hooks: - id: ruff args: [--fix] - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: check-yaml - id: fix-encoding-pragma @@ -16,7 +15,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/psf/black - rev: 23.7.0 + rev: 23.10.0 hooks: - id: black - repo: https://github.com/asottile/blacken-docs @@ -46,7 +45,7 @@ repos: - id: rst-directive-colons - id: rst-inline-touching-normal - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.5.1 + rev: v1.6.1 hooks: - id: mypy files: ^src/ @@ -55,9 +54,9 @@ repos: - types-pkg_resources==0.1.2 - types-paramiko - repo: https://github.com/codespell-project/codespell - rev: v2.2.5 + rev: v2.2.6 hooks: - id: codespell stages: [commit, commit-msg] - args: [--ignore-words-list, 'titel,statics,ba,nd,te'] + args: [--ignore-words-list, 'titel,statics,ba,nd,te,atomate'] types_or: [python, rst, markdown] diff --git a/ADMIN.md b/ADMIN.md index 11d8fb87b1..af6a8873ba 100644 --- a/ADMIN.md +++ b/ADMIN.md @@ -2,6 +2,7 @@ Version releases on Pypi and GitHub are handled automatically through GitHub actions. The steps to push a new release are: + 1. Update `CHANGELOG.md` with a new version and release notes. 2. Create a tagged Git commit with the above changes: `git tag v0.0.1` 3. Push the commit and tags to GitHub using: `git push origin --tags` diff --git a/README.md b/README.md index b28a034621..b2dc2e7b48 100644 --- a/README.md +++ b/README.md @@ -120,9 +120,8 @@ Atomate2 is released under a modified BSD license; the full text can be found [h ## Acknowledgements -Atomate2 was designed and developed by Alex Ganose. - -A full list of all contributors can be found [here][contributors]. +The development of atomate2 has benefited from many people across several research groups. +A full list of contributors can be found [here][contributors]. [pymatgen]: https://pymatgen.org [fireworks]: https://materialsproject.github.io/fireworks/ diff --git a/docs/about/contributors.md b/docs/about/contributors.md index 8a2c9531df..987bb88d8e 100644 --- a/docs/about/contributors.md +++ b/docs/about/contributors.md @@ -3,12 +3,12 @@ [gh]: https://cdnjs.cloudflare.com/ajax/libs/octicons/8.5.0/svg/mark-github.svg [orc]: ../_static/orcid.svg -atomate2 was designed and developed by **Alex Ganose** [![gh]][utf] [![orc]][0000-0002-4486-3321]. +The first version of atomate2 was designed and developed by **Alex Ganose** [![gh]][utf] [![orc]][0000-0002-4486-3321]. [utf]: https://github.com/utf [0000-0002-4486-3321]: https://orcid.org/0000-0002-4486-3321 -Additional contributions have been provided by: +Further substantial contributions have been provided by: **Andrew Rosen** [![gh]][Andrew-S-Rosen] [![orc]][0000-0002-0141-7006] \ Assistant Professor of Chemical & Biological Engineering \ @@ -24,7 +24,7 @@ Lawrence Livermore National Laboratory [jmmshn]: https://github.com/jmmshn [0000-0002-2743-7531]: https://orcid.org/0000-0002-2743-7531 -**Janosh Riebesell** [![gh]][janosh] [![orc]][0000-0001-5233-3462]\ +**Janosh Riebesell** [![gh]][janosh] [![orc]][0000-0001-5233-3462] \ PhD Student \ Cambridge University @@ -68,16 +68,51 @@ Microsoft Research [0000-0001-7777-8871]: https://orcid.org/0000-0001-7777-8871 **Zhuoying Zhu** [![gh]][zhuoying] [![orc]][0000-0003-1775-7651] \ -Postdoctoral researcher\ +Postdoctoral Researcher \ Lawrence Berkeley National Laboratory [zhuoying]: https://github.com/zhuoying [0000-0003-1775-7651]: https://orcid.org/0000-0003-1775-7651 **Aakash Ashok Naik** [![gh]][naik-aakash] [![orc]][0000-0002-6071-6786] \ -PhD student\ +PhD student \ Federal Institute for Materials Research and Testing (Berlin) \ Friedrich Schiller University Jena [naik-aakash]: https://github.com/naik-aakash [0000-0002-6071-6786]: https://orcid.org/0000-0002-6071-6786 + +**Aaron Kaplan** [![gh]][esoteric-ephemera] [![orc]][0000-0003-3439-4856] \ +Postdoctoral Researcher \ +Lawrence Berkeley National Laboratory + +[esoteric-ephemera]: https://github.com/esoteric-ephemera +[0000-0003-3439-4856]: https://orcid.org/0000-0003-3439-4856 + +**Matthew McDermott** [![gh]][mattmcdermott] [![orc]][0000-0002-4071-3000] \ +PhD student \ +University of California, Berkeley + +[mattmcdermott]: https://github.com/mattmcdermott +[0000-0002-4071-3000]: https://orcid.org/0000-0002-4071-3000 + +**Thomas Purcell** [![gh]][tpurcell90] [![orc]][0000-0003-4564-7206] \ +Assistant Professor of Chemistry \ +University of Arizona + +[tpurcell90]: https://github.com/tpurcell90 +[0000-0003-4564-7206]: https://orcid.org/0000-0003-4564-7206 + +**Alexander Bonkowski** [![gh]][ab5424] [![orc]][0000-0002-0525-4742] \ +PhD student in Chemistry \ +RWTH Aachen University + +[ab5424]: https://github.com/ab5424 +[0000-0002-0525-4742]: https://orcid.org/0000-0002-0525-4742 + +**Matthew Kuner** [![gh]][matthewkuner] [![orc]][0000-0002-8218-8558] \ +PhD student in Materials Science and Engineering \ +University of California, Berkeley + +[matthewkuner]: https://github.com/matthewkuner +[0000-0002-8218-8558]: https://orcid.org/0000-0002-8218-8558 diff --git a/docs/user/atomate-1-vs-2.md b/docs/user/atomate-1-vs-2.md new file mode 100644 index 0000000000..3aa317dfe9 --- /dev/null +++ b/docs/user/atomate-1-vs-2.md @@ -0,0 +1,25 @@ +# Atomate 1 vs 2 + +This document contains introductory context for people coming from atomate 1. +One of atomate2's core ideas is to allow scaling from a single material, to 100 materials, or 100,000 materials. Therefore, both local submission options and a connection to workflow managers such as FireWorks exist. We plan to support more workflow managers in the future to further ease job submission. +## Relation between managers running the actual jobs and the workflow as written in `atomate2` + +There is no leakage between job manager and the workflow definition in `atomate2`. For example, Fireworks is not a required dependency of `atomate2` or `jobflow`. Any `atomate2` workflow can be run using the local manager or an extensible set of external providers, Fireworks just one among them. E.g. all tests are run with mocked calls to the executables using the local manager. + +## Do I need to write separate codes for different managers? + +If you are adding a new manager option beyond local or FireWorks, you'll need to write a new converter in `jobflow` that takes a job or flow and converts it to the analogous object(s) for the manager you wish to use. This does not impact the `atomate2` code in any way. + +Typically, the workflow is as follows: + +1. Write a workflow in `atomate2` that represents a job or flow. +2. Import the job or flow in your script/notebook from `atomate2`. +3. Convert the job or flow to manager-compatible objects using a convenience function in `jobflow`. +4. Define the specs for the new object type based on your desired resources. +5. Dispatch the jobs. + +## What if a workflow manager stops being maintained? + +`atomate2` and `jobflow` remain unaffected in such a case. The user can choose a different manager to suit their needs. This ensures that workflow definition and dispatch are fully decoupled. + +In an ideal world, `jobflow` would offer multiple manager options, allowing users to run `atomate2` codes as per their preference. This full decoupling is one of the most powerful features of this stack. diff --git a/pyproject.toml b/pyproject.toml index 2f8e77ff93..37cd52d785 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,42 +18,43 @@ classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.9", "Topic :: Other/Nonlisted Topic", "Topic :: Scientific/Engineering", ] -requires-python = ">=3.8" +requires-python = ">=3.9" dependencies = [ "PyYAML", "click", "custodian>=2023.3.10", - "emmet-core>=0.65.0", + "emmet-core>=0.70.5", "jobflow>=0.1.11", - "monty", + "monty>=2023.9.25", "numpy", - "pydantic", - "pymatgen>=2023.1.9", + "pydantic-settings>=2.0.3", + "pydantic>=2.0.1", + "pymatgen>=2023.10.11", ] [project.optional-dependencies] amset = ["amset>=0.4.15", "pydash"] cclib = ["cclib"] -mp = ["mp-api>=0.27.5"] +mp = ["mp-api>=0.37.5"] phonons = ["phonopy>=1.10.8", "seekpath"] -lobster = ["lobsterpy>=0.3.0"] +lobster = ["lobsterpy>=0.3.2"] defects = ["dscribe>=1.2.0", "pymatgen-analysis-defects>=2022.11.30"] -forcefields = ["chgnet==0.2.0", "matgl==0.8.3", "quippy-ase==0.9.14"] +forcefields = ["chgnet>=0.2.2", "matgl>=0.8.3", "quippy-ase>=0.9.14"] docs = [ "FireWorks==2.0.3", - "autodoc_pydantic==1.9.0", + "autodoc_pydantic==2.0.1", "furo==2023.9.10", - "ipython==8.15.0", + "ipython==8.16.1", "jsonschema[format]", "myst_parser==2.0.0", - "numpydoc==1.5.0", - "sphinx==7.2.5", + "numpydoc==1.6.0", "sphinx-copybutton==0.5.2", + "sphinx==7.2.6", "sphinx_design==0.5.0", ] dev = ["pre-commit>=2.12.1"] @@ -61,24 +62,25 @@ tests = ["FireWorks==2.0.3", "pytest-cov==4.1.0", "pytest==7.4.2"] strict = [ "PyYAML==6.0.1", "cclib==1.8", - "chgnet==0.2.0", + "chgnet==0.2.2", "click==8.1.7", - "custodian==2023.7.22", + "custodian==2023.10.9", "dscribe==2.1.0", - "emmet-core==0.67.5", - "jobflow==0.1.13", - "lobsterpy==0.3.0", + "emmet-core==0.70.5", + "jobflow==0.1.14", + "lobsterpy==0.3.2", "matgl==0.8.3", - "monty==2023.9.5", - "mp-api==0.35.1", + "monty==2023.9.25", + "mp-api==0.37.5", "numpy", "phonopy==2.20.0", - "pydantic==1.10.9", + "pydantic-settings==2.0.3", + "pydantic==2.4.2", "pymatgen-analysis-defects==2023.8.22", - "pymatgen==2023.8.10", + "pymatgen==2023.10.11", "quippy-ase==0.9.14", "seekpath==2.1.0", - "typing-extensions==4.7.1", + "typing-extensions==4.8.0", ] [project.scripts] @@ -114,6 +116,7 @@ ignore_missing_imports = true no_strict_optional = true [tool.pytest.ini_options] +addopts = "-p no:warnings --import-mode=importlib" filterwarnings = [ "ignore:.*POTCAR.*:UserWarning", "ignore:.*input structure.*:UserWarning", @@ -140,36 +143,37 @@ exclude_lines = [ ] [tool.ruff] -target-version = "py38" +target-version = "py39" select = [ - "B", # flake8-bugbear - "C4", # flake8-comprehensions - "D", # pydocstyle - "E", # pycodestyle error - "EXE", # flake8-executable - "F", # pyflakes - "FLY", # flynt - "I", # isort - "ICN", # flake8-import-conventions - "ISC", # flake8-implicit-str-concat - "PD", # pandas-vet - "PERF", # perflint - "PIE", # flake8-pie - "PL", # pylint - "PT", # flake8-pytest-style - "PYI", # flakes8-pyi - "Q", # flake8-quotes - "RET", # flake8-return - "RSE", # flake8-raise - "RUF", # Ruff-specific rules - "SIM", # flake8-simplify - "SLOT", # flake8-slots - "TCH", # flake8-type-checking - "TID", # tidy imports - "TID", # flake8-tidy-imports - "UP", # pyupgrade - "W", # pycodestyle warning - "YTT", # flake8-2020 + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "D", # pydocstyle + "E", # pycodestyle error + "EXE", # flake8-executable + "F", # pyflakes + "FA", # flake8-future-annotations + "FBT003", # boolean-positional-value-in-call + "FLY", # flynt + "I", # isort + "ICN", # flake8-import-conventions + "ISC", # flake8-implicit-str-concat + "PD", # pandas-vet + "PERF", # perflint + "PIE", # flake8-pie + "PL", # pylint + "PT", # flake8-pytest-style + "PYI", # flakes8-pyi + "Q", # flake8-quotes + "RET", # flake8-return + "RSE", # flake8-raise + "RUF", # Ruff-specific rules + "SIM", # flake8-simplify + "SLOT", # flake8-slots + "TCH", # flake8-type-checking + "TID", # flake8-tidy-imports + "UP", # pyupgrade + "W", # pycodestyle warning + "YTT", # flake8-2020 ] ignore = [ "PD011", # pandas-use-of-dot-values @@ -190,5 +194,7 @@ isort.known-first-party = ["atomate2"] "**/tests/*" = ["D"] # flake8-type-checking (TCH): things inside TYPE_CHECKING aren't available # at runtime and so can't be used by pydantic models -# flake8-future-annotations (FA): future annotations only work in pydantic models in python 3.10+ -"**/schemas/*" = ["FA", "TCH"] +# flake8-future-annotations (FA): pipe operator for type unions only work in pydantic models in python 3.10+ +"**/schemas/*" = ["FA", "TCH", "UP007"] +"**/schemas.py" = ["FA", "TCH", "UP007"] +"**/settings.py" = ["FA", "TCH", "UP007"] diff --git a/src/atomate2/amset/files.py b/src/atomate2/amset/files.py index 017fb08bfc..ec04b7a713 100644 --- a/src/atomate2/amset/files.py +++ b/src/atomate2/amset/files.py @@ -15,8 +15,6 @@ if TYPE_CHECKING: from pathlib import Path -__all__ = ["copy_amset_files"] - logger = logging.getLogger(__name__) @@ -26,7 +24,7 @@ def copy_amset_files( src_dir: Path | str, src_host: str | None = None, file_client: FileClient = None, -): +) -> None: """ Copy AMSET files to current directory. @@ -80,7 +78,7 @@ def copy_amset_files( logger.info("Finished copying inputs") -def write_amset_settings(settings_updates: dict, from_prev: bool = False): +def write_amset_settings(settings_updates: dict, from_prev: bool = False) -> None: """ Write AMSET settings to file. diff --git a/src/atomate2/amset/jobs.py b/src/atomate2/amset/jobs.py index bd8391f9b1..ef098d3a56 100644 --- a/src/atomate2/amset/jobs.py +++ b/src/atomate2/amset/jobs.py @@ -14,8 +14,6 @@ from atomate2.amset.run import check_converged, run_amset from atomate2.amset.schemas import AmsetTaskDocument -__all__ = ["AmsetMaker"] - logger = logging.getLogger(__name__) @@ -48,7 +46,7 @@ def make( wavefunction_dir: str | Path = None, deformation_dir: str | Path = None, bandstructure_dir: str | Path = None, - ): + ) -> Response: """ Run an AMSET calculation. diff --git a/src/atomate2/amset/run.py b/src/atomate2/amset/run.py index 5d954e2839..19bc8e5e40 100644 --- a/src/atomate2/amset/run.py +++ b/src/atomate2/amset/run.py @@ -8,13 +8,11 @@ import numpy as np from pydash import get -__all__ = ["run_amset", "check_converged"] - logger = logging.getLogger(__name__) _CONVERGENCE_PROPERTIES = ("mobility.overall", "seebeck") -def run_amset(): +def run_amset() -> None: """Run amset in the current directory.""" # Run AMSET using the command line as calling from python can cause issues # with multiprocessing diff --git a/src/atomate2/amset/schemas.py b/src/atomate2/amset/schemas.py index 83f14a3179..875ae67568 100644 --- a/src/atomate2/amset/schemas.py +++ b/src/atomate2/amset/schemas.py @@ -3,7 +3,7 @@ import logging import re from pathlib import Path -from typing import Any, Dict, List, Tuple, Union +from typing import Any, Union import numpy as np from emmet.core.math import Matrix3D, Vector3D @@ -22,7 +22,6 @@ except ImportError: amset = None -__all__ = ["TransportData", "MeshData", "UsageStats", "AmsetTaskDocument"] logger = logging.getLogger(__name__) @@ -30,23 +29,23 @@ class TransportData(BaseModel): """Definition of AMSET transport data model.""" - doping: List[float] = Field(None, description="Carrier concentrations in cm^-3") - temperatures: List[float] = Field(None, description="Temperatures in K") - fermi_levels: List[List[float]] = Field( + doping: list[float] = Field(None, description="Carrier concentrations in cm^-3") + temperatures: list[float] = Field(None, description="Temperatures in K") + fermi_levels: list[list[float]] = Field( None, description="Fermi level positions in eV, given as (ndoping, ntemps)" ) - conductivity: List[List[Matrix3D]] = Field( + conductivity: list[list[Matrix3D]] = Field( None, description="Conductivity tensor in S/m, given as (ndoping, ntemps, 3, 3)" ) - seebeck: List[List[Matrix3D]] = Field( + seebeck: list[list[Matrix3D]] = Field( None, description="Seebeck tensor in µV/K, given as (ndoping, ntemps, 3, 3)" ) - electronic_thermal_conductivity: List[List[Matrix3D]] = Field( + electronic_thermal_conductivity: list[list[Matrix3D]] = Field( None, description="Electronic thermal conductivity tensor in W/mK, given as " "(ndoping, ntemps, 3, 3)", ) - mobility: Dict[str, List[List[Matrix3D]]] = Field( + mobility: dict[str, list[list[Matrix3D]]] = Field( None, description="Carrier mobility tensor in cm^2/Vs, given as " "{scattering_type: (ndoping, ntemps, 3, 3)}", @@ -74,32 +73,32 @@ class UsageStats(BaseModel): class MeshData(BaseModel): """Definition of full AMSET mesh data.""" - energies: Dict[str, List[List[float]]] = Field( + energies: dict[str, list[list[float]]] = Field( None, description="Band structure energies in eV on the irreducible mesh." ) - kpoints: List[Vector3D] = Field( + kpoints: list[Vector3D] = Field( None, description="K-points in fractional coordinates" ) - ir_kpoints: List[Vector3D] = Field( + ir_kpoints: list[Vector3D] = Field( None, description="Irreducible k-points in fractional coordinates" ) - ir_to_full_kpoint_mapping: List[int] = Field( + ir_to_full_kpoint_mapping: list[int] = Field( None, description="Mapping from irreducible to full k-points" ) efermi: float = Field(None, description="Intrinsic Fermi level from band structure") - vb_idx: Dict[str, int] = Field( + vb_idx: dict[str, int] = Field( None, description="Index of highest valence band for each spin" ) num_electrons: float = Field(None, description="Number of electrons in the system") - velocities: Dict[str, List[List[Vector3D]]] = Field( + velocities: dict[str, list[list[Vector3D]]] = Field( None, description="Band velocities for each irreducible k-point." ) - scattering_rates: Dict[str, List[List[List[List[List[float]]]]]] = Field( + scattering_rates: dict[str, list[list[list[list[list[float]]]]]] = Field( None, description="Scattering rates in s^-1, given as " "{spin: (nscattering_types, ndoping, ntemps, nbands, nkpoints)}", ) - fd_cutoffs: Tuple[List[List[float]], List[List[float]]] = Field( + fd_cutoffs: tuple[list[list[float]], list[list[float]]] = Field( None, description="Energy cutoffs within which the scattering rates are calculated" "given as (min_cutoff, max_cutoff) where each cutoff is given" @@ -129,15 +128,13 @@ class AmsetTaskDocument(StructureMetadata): nkpoints: int = Field(None, description="Total number of interpolated k-points") log: str = Field(None, description="Full AMSET running log") is_metal: bool = Field(None, description="Whether the system is a metal") - scattering_labels: List[str] = Field( + scattering_labels: list[str] = Field( None, description="The scattering types used in the calculation" ) soc: bool = Field(None, description="Whether spin-orbit coupling was included") structure: Structure = Field(None, description="The structure used in this task") - _schema: str = Field( - __version__, - description="Version of atomate2 used to create the document", - alias="schema", + schema: str = Field( + __version__, description="Version of atomate2 used to create the document" ) @classmethod @@ -145,9 +142,9 @@ class AmsetTaskDocument(StructureMetadata): def from_directory( cls, dir_name: Union[Path, str], - additional_fields: Dict[str, Any] = None, + additional_fields: dict[str, Any] = None, include_mesh: bool = False, - ): + ) -> "AmsetTaskDocument": """ Create a task document from a directory containing VASP files. @@ -156,7 +153,7 @@ def from_directory( dir_name : path or str The path to the folder containing the calculation outputs. additional_fields : dict - Dictionary of additional fields to add to output document. + dictionary of additional fields to add to output document. include_mesh : bool Whether to include the full AMSET mesh in the document. @@ -168,7 +165,7 @@ def from_directory( from amset.io import load_mesh from amset.util import cast_dict_list - additional_fields = {} if additional_fields is None else additional_fields + additional_fields = additional_fields or {} dir_name = Path(dir_name) settings = loadfn("settings.yaml") @@ -208,7 +205,7 @@ def from_directory( transport=transport, usage_stats=timing, kpoint_mesh=inter_mesh, - nkpoints=np.product(inter_mesh), + nkpoints=np.prod(inter_mesh), log=log, **mesh_kwargs, ) diff --git a/src/atomate2/cli/__init__.py b/src/atomate2/cli/__init__.py index 25aa3ce3c1..e5cc56fed8 100644 --- a/src/atomate2/cli/__init__.py +++ b/src/atomate2/cli/__init__.py @@ -6,7 +6,7 @@ @click.group(context_settings={"help_option_names": ["-h", "--help"]}) -def cli(): +def cli() -> None: """Command-line interface for atomate2.""" diff --git a/src/atomate2/cli/dev.py b/src/atomate2/cli/dev.py index 690863fb9b..ffa5b0130f 100644 --- a/src/atomate2/cli/dev.py +++ b/src/atomate2/cli/dev.py @@ -4,13 +4,13 @@ @click.group(context_settings={"help_option_names": ["-h", "--help"]}) -def dev(): +def dev() -> None: """Tools for atomate2 developers.""" @dev.command(context_settings={"help_option_names": ["-h", "--help"]}) @click.argument("test_dir") -def vasp_test_data(test_dir): +def vasp_test_data(test_dir) -> None: """Generate test data for VASP unit tests. This script expects there is an outputs.json file and job folders in the current @@ -143,13 +143,12 @@ def test_my_flow(mock_vasp, clean_dir, si_structure): # automatically use fake VASP and write POTCAR.spec during the test mock_vasp(ref_paths, fake_run_vasp_kwargs) - # !!! Generate job job = MyMaker().make(si_structure) # run the flow or job and ensure that it finished running successfully responses = run_locally(job, create_folders=True, ensure_success=True) - # !!! validation on the outputs + # validate the outputs output1 = responses[job.uuid][1].output assert isinstance(output1, TaskDoc) assert output1.output.energy == pytest.approx(-10.85037078) @@ -158,7 +157,7 @@ def test_my_flow(mock_vasp, clean_dir, si_structure): print(test_function_str) -def _potcar_to_potcar_spec(potcar_filename, output_filename): +def _potcar_to_potcar_spec(potcar_filename, output_filename) -> None: """Convert a POTCAR file to a POTCAR.spec file.""" from pymatgen.io.vasp import Potcar diff --git a/src/atomate2/common/analysis/elastic.py b/src/atomate2/common/analysis/elastic.py index ba82ccb5e5..f308ed7a98 100644 --- a/src/atomate2/common/analysis/elastic.py +++ b/src/atomate2/common/analysis/elastic.py @@ -2,8 +2,6 @@ from __future__ import annotations -__all__ = ["get_default_strain_states"] - def get_default_strain_states(order: int) -> list[tuple[int, int, int, int, int, int]]: """ diff --git a/src/atomate2/common/files.py b/src/atomate2/common/files.py index 3b56d634a5..d9ed4a3b35 100644 --- a/src/atomate2/common/files.py +++ b/src/atomate2/common/files.py @@ -7,15 +7,6 @@ from atomate2.utils.file_client import FileClient, auto_fileclient -__all__ = [ - "copy_files", - "delete_files", - "rename_files", - "gzip_files", - "gunzip_files", - "get_zfile", -] - @auto_fileclient def copy_files( @@ -28,7 +19,7 @@ def copy_files( prefix: str = "", allow_missing: bool = False, file_client: FileClient | None = None, -): +) -> None: r""" Copy files between source and destination folders. @@ -86,7 +77,7 @@ def delete_files( exclude_files: list[str | Path] | None = None, allow_missing: bool = False, file_client: FileClient | None = None, -): +) -> None: r""" Delete files in a directory. @@ -133,7 +124,7 @@ def rename_files( host: str | None = None, allow_missing: bool = False, file_client: FileClient | None = None, -): +) -> None: """ Delete files in a directory. @@ -177,7 +168,7 @@ def gzip_files( allow_missing: bool = False, force: bool = False, file_client: FileClient = None, -): +) -> None: r""" Gzip files in a directory. @@ -230,7 +221,7 @@ def gunzip_files( allow_missing: bool = False, force: bool = False, file_client: FileClient | None = None, -): +) -> None: r""" Gunzip files in a directory. @@ -367,7 +358,7 @@ def get_zfile( def gzip_output_folder( directory: str | Path, setting: bool | str, files_list: list[str] -): +) -> None: """ Zip the content of the output folder based on the specific code setting. diff --git a/src/atomate2/common/flows/defect.py b/src/atomate2/common/flows/defect.py index eaaf6ccc31..9dca7ca9fd 100644 --- a/src/atomate2/common/flows/defect.py +++ b/src/atomate2/common/flows/defect.py @@ -51,7 +51,7 @@ class ConfigurationCoordinateMaker(Maker): relax_maker: Maker static_maker: Maker - name: str = "config. coordinate" + name: str = "config coordinate" distortions: tuple[float, ...] = DEFAULT_DISTORTIONS def make( @@ -59,7 +59,7 @@ def make( structure: Structure, charge_state1: int, charge_state2: int, - ): + ) -> Flow: """ Make a job for the calculation of the configuration coordinate diagram. @@ -199,6 +199,13 @@ class FormationEnergyMaker(Maker, ABC): sites with selective dynamics set to True. So this setting only works with `relax_radius`. + validate_charge: bool + Whether to validate the charge of the defect. If True (default), the charge + of the output structure will have to match the charge of the input defect. + This helps catch situations where the charge of the output defect is either + improperly set or improperly parsed before the data is stored in the + database. + collect_defect_entry_data: bool Whether to collect the defect entry data at the end of the flow. If True, the output of all the charge states for each symmetry distinct @@ -234,7 +241,6 @@ class FormationEnergyMaker(Maker, ABC): 'defect_uuid': 'a1c31095-0494-4eed-9862-95311f80a993' } ] - """ defect_relax_maker: Maker @@ -242,9 +248,10 @@ class FormationEnergyMaker(Maker, ABC): name: str = "formation energy" relax_radius: float | str | None = None perturb: float | None = None + validate_charge: bool = True collect_defect_entry_data: bool = False - def __post_init__(self): + def __post_init__(self) -> None: """Apply post init updates.""" self.validate_maker() if self.bulk_relax_maker is None: @@ -256,7 +263,7 @@ def make( bulk_supercell_dir: str | Path | None = None, supercell_matrix: npt.NDArray | None = None, defect_index: int | str = "", - ): + ) -> Flow: """Make a flow to calculate the formation energy diagram. Start a series of charged supercell relaxations from a single defect @@ -317,6 +324,7 @@ def make( }, relax_radius=self.relax_radius, perturb=self.perturb, + validate_charge=self.validate_charge, ) jobs.extend([get_sc_job, spawn_output]) @@ -354,7 +362,7 @@ def structure_from_prv(self, previous_dir: str) -> Structure: """ @abstractmethod - def validate_maker(self): + def validate_maker(self) -> None: """Check some key settings in the relax maker. Since this workflow is pretty complex but allows you to use any diff --git a/src/atomate2/common/flows/elastic.py b/src/atomate2/common/flows/elastic.py new file mode 100644 index 0000000000..eaaf4d7d81 --- /dev/null +++ b/src/atomate2/common/flows/elastic.py @@ -0,0 +1,166 @@ +"""Flows for calculating elastic constants.""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from typing import TYPE_CHECKING + +from jobflow import Flow, Maker, OnMissing +from pymatgen.symmetry.analyzer import SpacegroupAnalyzer + +from atomate2 import SETTINGS +from atomate2.common.jobs.elastic import ( + fit_elastic_tensor, + generate_elastic_deformations, + run_elastic_deformations, +) + +if TYPE_CHECKING: + from pathlib import Path + + from emmet.core.math import Matrix3D + from pymatgen.core.structure import Structure + + from atomate2.forcefields.jobs import ForceFieldRelaxMaker + from atomate2.vasp.jobs.base import BaseVaspMaker + + +@dataclass +class BaseElasticMaker(Maker, ABC): + """ + Maker to calculate elastic constants. + + Calculate the elastic constant of a material. Initially, a tight structural + relaxation is performed to obtain the structure in a state of approximately zero + stress. Subsequently, perturbations are applied to the lattice vectors and the + resulting stress tensor is calculated from DFT, while allowing for relaxation of the + ionic degrees of freedom. Finally, constitutive relations from linear elasticity, + relating stress and strain, are employed to fit the full 6x6 elastic tensor. From + this, aggregate properties such as Voigt and Reuss bounds on the bulk and shear + moduli are derived. + + .. Note:: + It is heavily recommended to symmetrize the structure before passing it to + this flow. Otherwise, the symmetry reduction routines will not be as + effective at reducing the total number of deformations needed. + + Parameters + ---------- + name : str + Name of the flows produced by this maker. + order : int + Order of the tensor expansion to be determined. Can be either 2 or 3. + sym_reduce : bool + Whether to reduce the number of deformations using symmetry. + symprec : float + Symmetry precision to use in the reduction of symmetry. + bulk_relax_maker : .BaseVaspMaker or .ForceFieldRelaxMaker or None + A maker to perform a tight relaxation on the bulk. Set to ``None`` to skip the + bulk relaxation. + elastic_relax_maker : .BaseVaspMaker or .ForceFieldRelaxMaker + Maker used to generate elastic relaxations. + generate_elastic_deformations_kwargs : dict + Keyword arguments passed to :obj:`generate_elastic_deformations`. + fit_elastic_tensor_kwargs : dict + Keyword arguments passed to :obj:`fit_elastic_tensor`. + task_document_kwargs : dict + Additional keyword args passed to :obj:`.ElasticDocument.from_stresses()`. + """ + + name: str = "elastic" + order: int = 2 + sym_reduce: bool = True + symprec: float = SETTINGS.SYMPREC + bulk_relax_maker: BaseVaspMaker | ForceFieldRelaxMaker | None = None + elastic_relax_maker: BaseVaspMaker | ForceFieldRelaxMaker = ( + None # constant volume optimization + ) + generate_elastic_deformations_kwargs: dict = field(default_factory=dict) + fit_elastic_tensor_kwargs: dict = field(default_factory=dict) + task_document_kwargs: dict = field(default_factory=dict) + + def make( + self, + structure: Structure, + prev_dir: str | Path | None = None, + equilibrium_stress: Matrix3D = None, + conventional: bool = False, + ) -> Flow: + """ + Make flow to calculate the elastic constant. + + Parameters + ---------- + structure : .Structure + A pymatgen structure. + prev_vasp_dir : str or Path or None + A previous vasp calculation directory to use for copying outputs. + equilibrium_stress : tuple of tuple of float + The equilibrium stress of the (relaxed) structure, if known. + conventional : bool + Whether to transform the structure into the conventional cell. + """ + jobs = [] + + if self.bulk_relax_maker is not None: + # optionally relax the structure + bulk_kwargs = {} + if self.prev_calc_dir_argname is not None: + bulk_kwargs[self.prev_calc_dir_argname] = prev_dir + bulk = self.bulk_relax_maker.make(structure, **bulk_kwargs) + jobs.append(bulk) + structure = bulk.output.structure + prev_dir = bulk.output.dir_name + if equilibrium_stress is None: + equilibrium_stress = bulk.output.output.stress + + if conventional: + sga = SpacegroupAnalyzer(structure, symprec=self.symprec) + structure = sga.get_conventional_standard_structure() + + deformations = generate_elastic_deformations( + structure, + order=self.order, + sym_reduce=self.sym_reduce, + symprec=self.symprec, + **self.generate_elastic_deformations_kwargs, + ) + + vasp_deformation_calcs = run_elastic_deformations( + structure, + deformations.output, + elastic_relax_maker=self.elastic_relax_maker, + prev_dir=prev_dir, + ) + fit_tensor = fit_elastic_tensor( + structure, + vasp_deformation_calcs.output, + equilibrium_stress=equilibrium_stress, + order=self.order, + symprec=self.symprec if self.sym_reduce else None, + **self.fit_elastic_tensor_kwargs, + **self.task_document_kwargs, + ) + + # allow some of the deformations to fail + fit_tensor.config.on_missing_references = OnMissing.NONE + + jobs += [deformations, vasp_deformation_calcs, fit_tensor] + + return Flow( + jobs=jobs, + output=fit_tensor.output, + name=self.name, + ) + + @property + @abstractmethod + def prev_calc_dir_argname(self): + """Name of argument informing static maker of previous calculation directory. + + As this differs between different DFT codes (e.g., VASP, CP2K), it + has been left as a property to be implemented by the inheriting class. + Note: this is only applicable if a relax_maker is specified; i.e., two + calculations are performed for each ordering (relax -> static) + """ diff --git a/src/atomate2/common/jobs/defect.py b/src/atomate2/common/jobs/defect.py index 853f83a5c3..769f553b9e 100644 --- a/src/atomate2/common/jobs/defect.py +++ b/src/atomate2/common/jobs/defect.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING, Callable, Iterable +from typing import TYPE_CHECKING, Callable import numpy as np from jobflow import Flow, Response, job @@ -19,6 +19,7 @@ from atomate2.common.schemas.defects import CCDDocument if TYPE_CHECKING: + from collections.abc import Iterable from pathlib import Path from emmet.core.tasks import TaskDoc @@ -29,15 +30,6 @@ logger = logging.getLogger(__name__) -__all__ = [ - "get_charged_structures", - "spawn_energy_curve_calcs", - "get_ccd_documents", - "get_supercell_from_prv_calc", - "bulk_supercell_calculation", - "spawn_defect_q_jobs", -] - class CCDInput(BaseModel): """Document model to help construct CCDDocument.""" @@ -49,7 +41,7 @@ class CCDInput(BaseModel): @job -def get_charged_structures(structure: Structure, charges: Iterable): +def get_charged_structures(structure: Structure, charges: Iterable) -> list[Structure]: """Add charges to a structure. This needs to be a job so the results of other jobs can be passed in. @@ -81,7 +73,7 @@ def spawn_energy_curve_calcs( prev_vasp_dir: str | Path | None = None, add_name: str = "", add_info: dict | None = None, -): +) -> Response: """Compute the total energy curve from a reference to distorted structure. Parameters @@ -153,7 +145,7 @@ def get_ccd_documents( inputs1: Iterable[CCDInput], inputs2: Iterable[CCDInput], undistorted_index: int, -): +) -> Response: """ Get the configuration coordinate diagram from the task documents. @@ -218,7 +210,7 @@ def get_supercell_from_prv_calc( Output containing the supercell transformation and the dir_name """ sc_structure = structure_from_prv(prv_calc_dir) - (sc_mat_prv, _) = get_matched_structure_mapping( + sc_mat_prv, _ = get_matched_structure_mapping( uc_struct=uc_structure, sc_struct=sc_structure ) @@ -409,18 +401,18 @@ def check_charge_state(charge_state: int, task_structure: Structure) -> Response if int(charge_state) != int(task_structure.charge): raise ValueError( f"The charge of the output structure is {task_structure.charge}, " - f"but expect charge state from the Defect object is {charge_state}." + f"but expected charge state from the Defect object is {charge_state}." ) return True @job -def get_defect_entry(charge_state_summary: dict, bulk_summary: dict): +def get_defect_entry(charge_state_summary: dict, bulk_summary: dict) -> list[dict]: """Get a defect entry from a defect calculation and a bulk calculation.""" - bulk_c_entry = bulk_summary["sc_entry"] + bulk_sc_entry = bulk_summary["sc_entry"] bulk_struct_entry = ComputedStructureEntry( structure=bulk_summary["sc_struct"], - energy=bulk_c_entry.energy, + energy=bulk_sc_entry.energy, ) bulk_dir_name = bulk_summary["dir_name"] bulk_locpot = bulk_summary["locpot_plnr"] diff --git a/src/atomate2/common/jobs/elastic.py b/src/atomate2/common/jobs/elastic.py new file mode 100644 index 0000000000..171968b344 --- /dev/null +++ b/src/atomate2/common/jobs/elastic.py @@ -0,0 +1,231 @@ +"""Jobs used in the calculation of elastic tensors.""" + +from __future__ import annotations + +import contextlib +import logging +from typing import TYPE_CHECKING + +import numpy as np +from jobflow import Flow, Response, job +from pymatgen.alchemy.materials import TransformedStructure +from pymatgen.analysis.elasticity import Deformation, Strain, Stress +from pymatgen.core.tensors import symmetry_reduce +from pymatgen.transformations.standard_transformations import ( + DeformStructureTransformation, +) + +from atomate2 import SETTINGS +from atomate2.common.analysis.elastic import get_default_strain_states +from atomate2.common.schemas.elastic import ElasticDocument + +if TYPE_CHECKING: + from pathlib import Path + + from emmet.core.math import Matrix3D + from pymatgen.core.structure import Structure + + from atomate2.forcefields.jobs import ForceFieldRelaxMaker + from atomate2.vasp.jobs.base import BaseVaspMaker + + +logger = logging.getLogger(__name__) + + +@job +def generate_elastic_deformations( + structure: Structure, + order: int = 2, + strain_states: list[tuple[int, int, int, int, int, int]] | None = None, + strain_magnitudes: list[float] | list[list[float]] | None = None, + symprec: float = SETTINGS.SYMPREC, + sym_reduce: bool = True, +) -> list[Deformation]: + """ + Generate elastic deformations. + + Parameters + ---------- + structure : Structure + A pymatgen structure object. + order : int + Order of the tensor expansion to be determined. Can be either 2 or 3. + strain_states : None or list of tuple of int + List of Voigt-notation strains, e.g. ``[(1, 0, 0, 0, 0, 0), (0, 1, 0, 0, 0, 0), + etc]``. + strain_magnitudes : None or list of float or list of list of float + A list of strain magnitudes to multiply by for each strain state, e.g. ``[-0.01, + -0.005, 0.005, 0.01]``. Alternatively, a list of lists can be specified, where + each inner list corresponds to a specific strain state. + + symprec : float + Symmetry precision. + sym_reduce : bool + Whether to reduce the number of deformations using symmetry. + + Returns + ------- + List[Deformation] + A list of deformations. + """ + if strain_states is None: + strain_states = get_default_strain_states(order) + + if strain_magnitudes is None: + strain_magnitudes = np.linspace(-0.01, 0.01, 5 + (order - 2) * 2) + + if np.array(strain_magnitudes).ndim == 1: + strain_magnitudes = [strain_magnitudes] * len(strain_states) # type: ignore[assignment] + + strains = [] + for state, magnitudes in zip(strain_states, strain_magnitudes): + strains.extend([Strain.from_voigt(m * np.array(state)) for m in magnitudes]) + + # remove zero strains + strains = [strain for strain in strains if (abs(strain) > 1e-10).any()] + + if np.linalg.matrix_rank([strain.voigt for strain in strains]) < 6: + # TODO: check for sufficiency of input for nth order + raise ValueError("strain list is insufficient to fit an elastic tensor") + + if sym_reduce: + strain_mapping = symmetry_reduce(strains, structure, symprec=symprec) + logger.info( + f"Using symmetry to reduce number of strains from {len(strains)} to " + f"{len(list(strain_mapping.keys()))}" + ) + strains = list(strain_mapping.keys()) + + return [s.get_deformation_matrix() for s in strains] + + +@job +def run_elastic_deformations( + structure: Structure, + deformations: list[Deformation], + prev_dir: str | Path | None = None, + prev_dir_argname: str = None, + elastic_relax_maker: BaseVaspMaker | ForceFieldRelaxMaker = None, +) -> Response: + """ + Run elastic deformations. + + Note, this job will replace itself with N relaxation calculations, where N is + the number of deformations. + + Parameters + ---------- + structure : Structure + A pymatgen structure. + deformations : list of Deformation + The deformations to apply. + prev_dir : str or Path or None + A previous directory to use for copying outputs. + prev_dir_argname: str + argument name for the prev_dir variable + elastic_relax_maker : .BaseVaspMaker or .ForceFieldRelaxMaker + A VaspMaker or a ForceFieldMaker to use to generate the elastic relaxation jobs. + """ + relaxations = [] + outputs = [] + for i, deformation in enumerate(deformations): + # deform the structure + dst = DeformStructureTransformation(deformation=deformation) + ts = TransformedStructure(structure, transformations=[dst]) + deformed_structure = ts.final_structure + + with contextlib.suppress(Exception): + # write details of the transformation to the transformations.json file + # this file will automatically get added to the task document and allow + # the elastic builder to reconstruct the elastic document; note the ":" is + # automatically converted to a "." in the filename. + elastic_relax_maker.write_additional_data["transformations:json"] = ts + + elastic_job_kwargs = {} + if prev_dir is not None and prev_dir_argname is not None: + elastic_job_kwargs[prev_dir_argname] = prev_dir + # create the job + relax_job = elastic_relax_maker.make(deformed_structure, **elastic_job_kwargs) + relax_job.append_name(f" {i + 1}/{len(deformations)}") + relaxations.append(relax_job) + + # extract the outputs we want + output = { + "stress": relax_job.output.output.stress, + "deformation": deformation, + "uuid": relax_job.output.uuid, + "job_dir": relax_job.output.dir_name, + } + + outputs.append(output) + + relax_flow = Flow(relaxations, outputs) + return Response(replace=relax_flow) + + +@job(output_schema=ElasticDocument) +def fit_elastic_tensor( + structure: Structure, + deformation_data: list[dict], + equilibrium_stress: Matrix3D | None = None, + order: int = 2, + fitting_method: str = SETTINGS.ELASTIC_FITTING_METHOD, + symprec: float = SETTINGS.SYMPREC, + allow_elastically_unstable_structs: bool = True, +) -> ElasticDocument: + """ + Analyze stress/strain data to fit the elastic tensor and related properties. + + Parameters + ---------- + structure : ~pymatgen.core.structure.Structure + A pymatgen structure. + deformation_data : list of dict + The deformation data, as a list of dictionaries, each containing the keys + "stress", "deformation". + equilibrium_stress : None or tuple of tuple of float + The equilibrium stress of the (relaxed) structure, if known. + order : int + Order of the tensor expansion to be fitted. Can be either 2 or 3. + fitting_method : str + The method used to fit the elastic tensor. See pymatgen for more details on the + methods themselves. The options are: + + - "finite_difference" (note this is required if fitting a 3rd order tensor) + - "independent" + - "pseudoinverse" + symprec : float + Symmetry precision for deriving symmetry equivalent deformations. If + ``symprec=None``, then no symmetry operations will be applied. + allow_elastically_unstable_structs : bool + Whether to allow the ElasticDocument to still complete in the event that + the structure is elastically unstable. + """ + stresses = [] + deformations = [] + uuids = [] + job_dirs = [] + for data in deformation_data: + # stress could be none if the deformation calculation failed + if data["stress"] is None: + continue + + stresses.append(Stress(data["stress"])) + deformations.append(Deformation(data["deformation"])) + uuids.append(data["uuid"]) + job_dirs.append(data["job_dir"]) + + logger.info("Analyzing stress/strain data") + + return ElasticDocument.from_stresses( + structure, + stresses, + deformations, + uuids, + job_dirs, + fitting_method=fitting_method, + order=order, + equilibrium_stress=equilibrium_stress, + symprec=symprec, + allow_elastically_unstable_structs=allow_elastically_unstable_structs, + ) diff --git a/src/atomate2/common/jobs/phonons.py b/src/atomate2/common/jobs/phonons.py index ded46157c2..e788917c1b 100644 --- a/src/atomate2/common/jobs/phonons.py +++ b/src/atomate2/common/jobs/phonons.py @@ -33,20 +33,11 @@ logger = logging.getLogger(__name__) -__all__ = [ - "get_total_energy_per_cell", - "get_supercell_size", - "generate_phonon_displacements", - "run_phonon_displacements", - "generate_frequencies_eigenvectors", - "PhononDisplacementMaker", -] - @job def get_total_energy_per_cell( total_dft_energy_per_formula_unit: float, structure: Structure -): +) -> float: """ Job that computes total dft energy of the cell. @@ -68,7 +59,7 @@ def get_total_energy_per_cell( @job def get_supercell_size( structure: Structure, min_length: float, prefer_90_degrees: bool, **kwargs -): +) -> list[list[float]]: """ Determine supercell size with given min_length. @@ -136,7 +127,7 @@ def generate_phonon_displacements( use_symmetrized_structure: str | None, kpath_scheme: str, code: str, -): +) -> list[Structure]: """ Generate displaced structures with phonopy. @@ -205,7 +196,7 @@ def generate_frequencies_eigenvectors( epsilon_static: Matrix3D = None, born: Matrix3D = None, **kwargs, -): +) -> PhononBSDOSDoc: """ Analyze the phonon runs and summarize the results. @@ -237,7 +228,6 @@ def generate_frequencies_eigenvectors( Born charges kwargs: dict Additional parameters that are passed to PhononBSDOSDoc.from_forces_born - """ return PhononBSDOSDoc.from_forces_born( structure=structure, @@ -263,7 +253,7 @@ def run_phonon_displacements( supercell_matrix, phonon_maker: BaseVaspMaker | ForceFieldStaticMaker = None, prev_vasp_dir: str | Path = None, -): +) -> Flow: """ Run phonon displacements. diff --git a/src/atomate2/common/jobs/utils.py b/src/atomate2/common/jobs/utils.py index a504a7c797..dffcec0d4e 100644 --- a/src/atomate2/common/jobs/utils.py +++ b/src/atomate2/common/jobs/utils.py @@ -12,12 +12,6 @@ if TYPE_CHECKING: from pymatgen.core import Structure -__all__ = [ - "structure_to_primitive", - "structure_to_conventional", - "retrieve_structure_from_materials_project", -] - @job def structure_to_primitive( @@ -36,7 +30,6 @@ def structure_to_primitive( Returns ------- .Structure - """ sga = SpacegroupAnalyzer(structure, symprec=symprec) return sga.get_primitive_standard_structure() @@ -59,7 +52,6 @@ def structure_to_conventional( Returns ------- .Structure - """ sga = SpacegroupAnalyzer(structure, symprec=symprec) return sga.get_conventional_standard_structure() diff --git a/src/atomate2/common/schemas/cclib.py b/src/atomate2/common/schemas/cclib.py index d8297a8747..0a473736c5 100644 --- a/src/atomate2/common/schemas/cclib.py +++ b/src/atomate2/common/schemas/cclib.py @@ -3,7 +3,7 @@ import logging import os from pathlib import Path -from typing import Any, Dict, List, Optional, Tuple, Type, TypeVar, Union +from typing import Any, Optional, TypeVar, Union from emmet.core.structure import MoleculeMetadata from monty.dev import requires @@ -21,13 +21,12 @@ except ImportError: cclib = None -__all__ = ["TaskDocument"] logger = logging.getLogger(__name__) _T = TypeVar("_T", bound="TaskDocument") -class TaskDocument(MoleculeMetadata): +class TaskDocument(MoleculeMetadata, extra="allow"): # type: ignore[call-arg] """ Definition of a cclib-generated task document. @@ -41,37 +40,35 @@ class TaskDocument(MoleculeMetadata): logfile: str = Field( None, description="Path to the log file used in the post-processing analysis" ) - attributes: Dict = Field( + attributes: dict = Field( None, description="Computed properties and calculation outputs" ) - metadata: Dict = Field( + metadata: dict = Field( None, description="Calculation metadata, including input parameters and runtime " "statistics", ) task_label: str = Field(None, description="A description of the task") - tags: List[str] = Field(None, description="Optional tags for this task document") + tags: list[str] = Field(None, description="Optional tags for this task document") last_updated: str = Field( default_factory=datetime_str, description="Timestamp for this task document was last updated", ) - _schema: str = Field( - __version__, - description="Version of atomate2 used to create the document", - alias="schema", + schema: str = Field( + __version__, description="Version of atomate2 used to create the document" ) @classmethod @requires(cclib, "The cclib TaskDocument requires cclib to be installed.") def from_logfile( - cls: Type[_T], + cls: type[_T], dir_name: Union[str, Path], - logfile_extensions: Union[str, List[str]], + logfile_extensions: Union[str, list[str]], store_trajectory: bool = False, - additional_fields: Optional[Dict[str, Any]] = None, - analysis: Optional[Union[str, List[str]]] = None, + additional_fields: Optional[dict[str, Any]] = None, + analysis: Optional[Union[str, list[str]]] = None, proatom_dir: Optional[Union[Path, str]] = None, - ) -> _T: + ) -> "TaskDocument": """ Create a TaskDocument from a log file. @@ -123,7 +120,7 @@ def from_logfile( logger.info(f"Getting task doc from {logfile}") - additional_fields = {} if additional_fields is None else additional_fields + additional_fields = additional_fields or {} # Let's parse the log file with cclib cclib_obj = ccread(logfile, logging.ERROR) @@ -248,7 +245,7 @@ def from_logfile( metadata=metadata, ) doc.molecule = final_molecule - return doc.copy(update=additional_fields) + return doc.model_copy(update=additional_fields) @requires(cclib, "cclib_calculate requires cclib to be installed.") @@ -257,7 +254,7 @@ def cclib_calculate( method: str, cube_file: Union[Path, str], proatom_dir: Union[Path, str], -) -> Optional[Dict[str, Any]]: +) -> Optional[dict[str, Any]]: """ Run a cclib population analysis. @@ -354,8 +351,8 @@ def cclib_calculate( def _get_homos_lumos( - moenergies: List[List[float]], homo_indices: List[int] -) -> Tuple[List[float], Optional[List[float]], Optional[List[float]]]: + moenergies: list[list[float]], homo_indices: list[int] +) -> tuple[list[float], Optional[list[float]], Optional[list[float]]]: """ Calculate the HOMO, LUMO, and HOMO-LUMO gap energies in eV. diff --git a/src/atomate2/common/schemas/defects.py b/src/atomate2/common/schemas/defects.py index eea91e13c8..2853172a4a 100644 --- a/src/atomate2/common/schemas/defects.py +++ b/src/atomate2/common/schemas/defects.py @@ -1,7 +1,7 @@ """General schemas for defect workflow outputs.""" import logging -from typing import Any, Callable, Dict, List, Optional, Tuple, Union +from typing import Any, Callable, Optional, Union import numpy as np from emmet.core.tasks import TaskDoc @@ -26,11 +26,11 @@ class FormationEnergyDiagramDocument(BaseModel): None, description="The ComputedEntry representing the bulk structure." ) - defect_entries: List[DefectEntry] = Field( + defect_entries: list[DefectEntry] = Field( None, description="The defect entries for the formation energy diagram." ) - pd_entries: List[ComputedEntry] = Field( + pd_entries: list[ComputedEntry] = Field( None, description="The entries used to construct the phase diagram." ) @@ -54,11 +54,11 @@ class FormationEnergyDiagramDocument(BaseModel): None, description="The directory name of the pristine supercell calculation." ) - defect_sc_dirs: Dict[int, str] = Field( + defect_sc_dirs: dict[int, str] = Field( None, description="The directory names of the charged defect calculations." ) - dielectric: Union[float, List[List[float]]] = Field( + dielectric: Union[float, list[list[float]]] = Field( None, description="The dielectric constant or tensor, can be used to compute " "finite-size corrections.", @@ -87,7 +87,7 @@ def from_formation_energy_diagram( ) def as_formation_energy_diagram( - self, pd_entries: Optional[List[ComputedEntry]] = None + self, pd_entries: Optional[list[ComputedEntry]] = None ) -> FormationEnergyDiagram: """Create a `FormationEnergyDiagram` object from the document. @@ -122,41 +122,41 @@ class CCDDocument(BaseModel): None, description="The structure of defect (supercell) in charge state (q2)." ) - distortions1: List[float] = Field( + distortions1: list[float] = Field( None, description="The distortions of the defect (supercell) in charge state (q1).", ) - distortions2: List[float] = Field( + distortions2: list[float] = Field( None, description="The distortions of the defect (supercell) in charge state (q2).", ) - energies1: List[float] = Field( + energies1: list[float] = Field( None, description="The energies of the defect (supercell) in charge state (q1)." ) - energies2: List[float] = Field( + energies2: list[float] = Field( None, description="The energies of the defect (supercell) in charge state (q2)." ) - static_dirs1: List[str] = Field( + static_dirs1: list[str] = Field( None, description="Directories of distorted calculations for the defect (supercell) " "in charge state (q1).", ) - static_dirs2: List[str] = Field( + static_dirs2: list[str] = Field( None, description="Directories of distorted calculations for the defect (supercell) " "in charge state (q2).", ) - static_uuids1: List[str] = Field( + static_uuids1: Optional[list[str]] = Field( None, description="UUIDs of distorted calculations for the defect (supercell) in " "charge state (q1).", ) - static_uuids2: List[str] = Field( + static_uuids2: Optional[list[str]] = Field( None, description="UUIDs of distorted calculations for the defect (supercell) in " "charge state (q2).", @@ -177,17 +177,17 @@ class CCDDocument(BaseModel): @classmethod def from_task_outputs( cls, - structures1: List[Structure], - structures2: List[Structure], - energies1: List[float], - energies2: List[float], - static_dirs1: List[str], - static_dirs2: List[str], - static_uuids1: List[str], - static_uuids2: List[str], + structures1: list[Structure], + structures2: list[Structure], + energies1: list[float], + energies2: list[float], + static_dirs1: list[str], + static_dirs2: list[str], + static_uuids1: list[str], + static_uuids2: list[str], relaxed_uuid1: str, relaxed_uuid2: str, - ): + ) -> "CCDDocument": """Create a CCDDocument from a lists of structures and energies. The directories and the UUIDs of the static calculations are also provided as @@ -221,7 +221,9 @@ def from_task_outputs( UUID of relaxed calculation in charge state (q2). """ - def get_ent(struct, energy, dir_name, uuid): + def get_ent( + struct: Structure, energy: float, dir_name, uuid + ) -> ComputedStructureEntry: return ComputedStructureEntry( structure=struct, energy=energy, @@ -242,8 +244,8 @@ def get_ent(struct, energy, dir_name, uuid): @classmethod def from_entries( cls, - entries1: List[ComputedStructureEntry], - entries2: List[ComputedStructureEntry], + entries1: list[ComputedStructureEntry], + entries2: list[ComputedStructureEntry], relaxed_uuid1: Optional[str] = None, relaxed_uuid2: Optional[str] = None, ) -> "CCDDocument": @@ -263,14 +265,14 @@ def from_entries( """ - def find_entry(entries, uuid) -> Tuple[int, ComputedStructureEntry]: + def find_entry(entries, uuid) -> tuple[int, ComputedStructureEntry]: """Find the entry with the given UUID.""" for itr, entry in enumerate(entries): if entry.data["uuid"] == uuid: return itr, entry raise ValueError(f"Could not find entry with UUID: {uuid}") - def dQ_entries(e1, e2): + def dQ_entries(e1, e2) -> float: """Get the displacement between two entries.""" return get_dQ(e1.structure, e2.structure) @@ -312,10 +314,10 @@ def dQ_entries(e1, e2): relaxed_index2=idx2, ) - def get_taskdocs(self): + def get_taskdocs(self) -> list[list[TaskDoc]]: """Get the distorted task documents.""" - def remove_host_name(dir_name): + def remove_host_name(dir_name) -> str: return dir_name.split(":")[-1] return [ @@ -331,8 +333,8 @@ def remove_host_name(dir_name): def sort_pos_dist( - list_in: List[Any], s1: Any, s2: Any, dist: Callable -) -> Tuple[List[Any], List[float]]: + list_in: list[Any], s1: Any, s2: Any, dist: Callable +) -> tuple[list[Any], list[float]]: """ Sort a list defined when we can only compute a positive-definite distance. diff --git a/src/atomate2/common/schemas/elastic.py b/src/atomate2/common/schemas/elastic.py index 5d0177f151..a596e1ae6d 100644 --- a/src/atomate2/common/schemas/elastic.py +++ b/src/atomate2/common/schemas/elastic.py @@ -1,6 +1,6 @@ """Schemas for elastic tensor fitting and related properties.""" from copy import deepcopy -from typing import List, Optional +from typing import Optional import numpy as np from emmet.core.math import Matrix3D, MatrixVoigt @@ -19,13 +19,6 @@ from atomate2 import SETTINGS -__all__ = [ - "DerivedProperties", - "FittingData", - "ElasticTensorDocument", - "ElasticDocument", -] - class DerivedProperties(BaseModel): """Properties derived from an elastic tensor.""" @@ -68,7 +61,7 @@ class DerivedProperties(BaseModel): snyder_total: float = Field( None, description="Synder's total sound velocity (SI units)." ) - clark_thermalcond: float = Field( + clark_thermalcond: Optional[float] = Field( None, description="Clarke's thermal conductivity (SI units)." ) cahill_thermalcond: float = Field( @@ -84,20 +77,20 @@ class DerivedProperties(BaseModel): class FittingData(BaseModel): """Data used to fit elastic tensors.""" - cauchy_stresses: List[Matrix3D] = Field( + cauchy_stresses: list[Matrix3D] = Field( None, description="The Cauchy stresses used to fit the elastic tensor." ) - strains: List[Matrix3D] = Field( + strains: list[Matrix3D] = Field( None, description="The strains used to fit the elastic tensor." ) - pk_stresses: List[Matrix3D] = Field( + pk_stresses: list[Matrix3D] = Field( None, description="The Piola-Kirchoff stresses used to fit the elastic tensor." ) - deformations: List[Matrix3D] = Field( + deformations: list[Matrix3D] = Field( None, description="The deformations corresponding to each strain state." ) - uuids: List[str] = Field(None, description="The uuids of the deformation jobs.") - job_dirs: List[str] = Field( + uuids: list[str] = Field(None, description="The uuids of the deformation jobs.") + job_dirs: list[Optional[str]] = Field( None, description="The directories where the deformation jobs were run." ) @@ -118,7 +111,7 @@ class ElasticDocument(StructureMetadata): elastic_tensor: ElasticTensorDocument = Field( None, description="Fitted elastic tensor." ) - eq_stress: Matrix3D = Field( + eq_stress: Optional[Matrix3D] = Field( None, description="The equilibrium stress of the structure." ) derived_properties: DerivedProperties = Field( @@ -138,16 +131,16 @@ class ElasticDocument(StructureMetadata): def from_stresses( cls, structure: Structure, - stresses: List[Stress], - deformations: List[Deformation], - uuids: List[str], - job_dirs: List[str], + stresses: list[Stress], + deformations: list[Deformation], + uuids: list[str], + job_dirs: list[str], fitting_method: str = SETTINGS.ELASTIC_FITTING_METHOD, order: Optional[int] = None, equilibrium_stress: Optional[Matrix3D] = None, symprec: float = SETTINGS.SYMPREC, allow_elastically_unstable_structs: bool = True, - ): + ) -> "ElasticDocument": """ Create an elastic document from strains and stresses. @@ -214,7 +207,7 @@ def from_stresses( strains, pk_stresses, eq_stress=eq_stress ) else: - raise ValueError(f"Unsupported elastic fitting method {fitting_method}") + raise ValueError(f"Unsupported elastic {fitting_method=}") ieee = result.convert_to_ieee(structure) property_tensor = ieee if order == 2 else ElasticTensor(ieee[0]) @@ -251,13 +244,13 @@ def from_stresses( def _expand_strains( structure: Structure, - strains: List[Strain], - stresses: List[Stress], - uuids: List[str], - job_dirs: List[str], + strains: list[Strain], + stresses: list[Stress], + uuids: list[str], + job_dirs: list[str], symprec: float, tol: float = 1e-3, -): +) -> tuple[list, list, list[str], list[str]]: """ Use symmetry to expand strains. diff --git a/src/atomate2/common/schemas/phonons.py b/src/atomate2/common/schemas/phonons.py index ae92fa4a1a..73aed8e11d 100644 --- a/src/atomate2/common/schemas/phonons.py +++ b/src/atomate2/common/schemas/phonons.py @@ -2,7 +2,7 @@ import copy import logging -from typing import Dict, List, Optional, Union +from typing import Optional, Union import numpy as np from emmet.core.math import Matrix3D @@ -29,14 +29,6 @@ logger = logging.getLogger(__name__) -__all__ = [ - "PhononBSDOSDoc", - "PhononComputationalSettings", - "PhononUUIDs", - "PhononJobDirs", - "ThermalDisplacementData", -] - class PhononComputationalSettings(BaseModel): """Collection to store computational settings for the phonon computation.""" @@ -56,15 +48,15 @@ class ThermalDisplacementData(BaseModel): "cutoff frequency in THz to avoid numerical issues in the " "computation of the thermal displacement parameters" ) - thermal_displacement_matrix_cif: List[List[Matrix3D]] = Field( + thermal_displacement_matrix_cif: list[list[Matrix3D]] = Field( None, description="field including thermal displacement matrices in CIF format" ) - thermal_displacement_matrix: List[List[Matrix3D]] = Field( + thermal_displacement_matrix: list[list[Matrix3D]] = Field( None, description="field including thermal displacement matrices in Cartesian " "coordinate system", ) - temperatures_thermal_displacements: List[int] = Field( + temperatures_thermal_displacements: list[int] = Field( None, description="temperatures at which the thermal displacement matrices" "have been computed", @@ -74,25 +66,27 @@ class ThermalDisplacementData(BaseModel): class PhononUUIDs(BaseModel): """Collection to save all uuids connected to the phonon run.""" - optimization_run_uuid: str = Field(None, description="optimization run uuid") - displacements_uuids: List[str] = Field( + optimization_run_uuid: Optional[str] = Field( + None, description="optimization run uuid" + ) + displacements_uuids: Optional[list[str]] = Field( None, description="The uuids of the displacement jobs." ) - static_run_uuid: str = Field(None, description="static run uuid") - born_run_uuid: str = Field(None, description="born run uuid") + static_run_uuid: Optional[str] = Field(None, description="static run uuid") + born_run_uuid: Optional[str] = Field(None, description="born run uuid") class ForceConstants(MSONable): """A force constants class.""" - def __init__(self, force_constants: List[List[Matrix3D]]): + def __init__(self, force_constants: list[list[Matrix3D]]) -> None: self.force_constants = force_constants class PhononJobDirs(BaseModel): """Collection to save all job directories relevant for the phonon run.""" - displacements_job_dirs: List[Optional[str]] = Field( + displacements_job_dirs: list[Optional[str]] = Field( None, description="The directories where the displacement jobs were run." ) static_run_job_dir: Optional[str] = Field( @@ -124,30 +118,30 @@ class PhononBSDOSDoc(StructureMetadata): description="Phonon density of states object.", ) - free_energies: List[float] = Field( + free_energies: list[float] = Field( None, description="vibrational part of the free energies in J/mol per " "formula unit for temperatures in temperature_list", ) - heat_capacities: List[float] = Field( + heat_capacities: list[float] = Field( None, description="heat capacities in J/K/mol per " "formula unit for temperatures in temperature_list", ) - internal_energies: List[float] = Field( + internal_energies: list[float] = Field( None, description="internal energies in J/mol per " "formula unit for temperatures in temperature_list", ) - entropies: List[float] = Field( + entropies: list[float] = Field( None, description="entropies in J/(K*mol) per formula unit" "for temperatures in temperature_list ", ) - temperatures: List[int] = Field( + temperatures: list[int] = Field( None, description="temperatures at which the vibrational" " part of the free energies" @@ -161,17 +155,17 @@ class PhononBSDOSDoc(StructureMetadata): ) # needed, e.g. to compute Grueneisen parameter etc - force_constants: ForceConstants = Field( + force_constants: Optional[ForceConstants] = Field( None, description="Force constants between every pair of atoms in the structure" ) - born: List[Matrix3D] = Field( + born: Optional[list[Matrix3D]] = Field( None, description="born charges as computed from phonopy. Only for symmetrically " "different atoms", ) - epsilon_static: Matrix3D = Field( + epsilon_static: Optional[Matrix3D] = Field( None, description="The high-frequency dielectric constant" ) @@ -194,7 +188,7 @@ class PhononBSDOSDoc(StructureMetadata): "Field including all relevant job directories" ) - uuids: PhononUUIDs = Field("Field including all relevant uuids") + uuids: Optional[PhononUUIDs] = Field("Field including all relevant uuids") @classmethod def from_forces_born( @@ -207,12 +201,12 @@ def from_forces_born( use_symmetrized_structure: Union[str, None], kpath_scheme: str, code: str, - displacement_data: Dict[str, list], + displacement_data: dict[str, list], total_dft_energy: float, epsilon_static: Matrix3D = None, born: Matrix3D = None, **kwargs, - ): + ) -> "PhononBSDOSDoc": """ Generate collection of phonon data. @@ -252,7 +246,7 @@ def from_forces_born( cell = get_phonopy_structure(structure) if use_symmetrized_structure == "primitive" and kpath_scheme != "seekpath": - primitive_matrix: Union[List[List[float]], str] = [ + primitive_matrix: Union[list[list[float]], str] = [ [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], @@ -493,7 +487,7 @@ def from_forces_born( @staticmethod def get_kpath( structure: Structure, kpath_scheme: str, symprec: float, **kpath_kwargs - ): + ) -> tuple: """ Get high-symmetry points in k-space in phonopy format. diff --git a/src/atomate2/common/utils.py b/src/atomate2/common/utils.py index fd0b939eb1..5fd1c29167 100644 --- a/src/atomate2/common/utils.py +++ b/src/atomate2/common/utils.py @@ -116,6 +116,10 @@ def parse_additional_json(dir_name: Path) -> dict[str, Any]: additional_json = {} for filename in dir_name.glob("*.json*"): key = filename.name.split(".")[0] - if key not in ("custodian", "transformations"): + # ignore FW.json(.gz) so jobflow doesn't try to parse prev_vasp_dir + # OutputReferences was causing atomate2 MP workflows to fail with ValueError: + # Could not resolve reference 7f5a7f14-464c-4a5b-85f9-8d11b595be3b not in store + # or cache contact @janosh in case of questions + if key not in ("custodian", "transformations", "FW"): additional_json[key] = loadfn(filename, cls=None) return additional_json diff --git a/src/atomate2/cp2k/drones.py b/src/atomate2/cp2k/drones.py index 04cb83ae6a..c17f350a43 100644 --- a/src/atomate2/cp2k/drones.py +++ b/src/atomate2/cp2k/drones.py @@ -12,8 +12,6 @@ logger = logging.getLogger(__name__) -__all__ = ["Cp2kDrone"] - class Cp2kDrone(AbstractDrone): """ @@ -25,7 +23,7 @@ class Cp2kDrone(AbstractDrone): Additional keyword args passed to :obj:`.TaskDocument.from_directory`. """ - def __init__(self, **task_document_kwargs): + def __init__(self, **task_document_kwargs) -> None: self.task_document_kwargs = task_document_kwargs def assimilate(self, path: str | Path | None = None) -> TaskDocument: diff --git a/src/atomate2/cp2k/files.py b/src/atomate2/cp2k/files.py index 16a3cedfba..6091219fa2 100644 --- a/src/atomate2/cp2k/files.py +++ b/src/atomate2/cp2k/files.py @@ -5,7 +5,7 @@ import logging import re from pathlib import Path -from typing import TYPE_CHECKING, Sequence +from typing import TYPE_CHECKING from pymatgen.io.cp2k.outputs import Cp2kOutput @@ -15,12 +15,12 @@ from atomate2.utils.path import strip_hostname if TYPE_CHECKING: + from collections.abc import Sequence + from pymatgen.core import Structure from atomate2.cp2k.sets.base import Cp2kInputGenerator -__all__ = ["copy_cp2k_outputs", "write_cp2k_input_set"] - logger = logging.getLogger(__name__) @@ -32,7 +32,7 @@ def copy_cp2k_outputs( additional_cp2k_files: list[str] | None = None, restart_to_input: bool = True, file_client: FileClient | None = None, -): +) -> None: """ Copy CP2K output files to the current directory. @@ -161,7 +161,7 @@ def write_cp2k_input_set( apply_input_updates: bool = True, optional_files: dict | None = None, **kwargs, -): +) -> None: """ Write CP2K input set. @@ -201,7 +201,7 @@ def cleanup_cp2k_outputs( host: str | None = None, file_patterns: Sequence[str] = ("*bak*",), file_client: FileClient | None = None, -): +) -> None: """ Remove unnecessary files. diff --git a/src/atomate2/cp2k/flows/core.py b/src/atomate2/cp2k/flows/core.py index 71c7717e4c..209545ed47 100644 --- a/src/atomate2/cp2k/flows/core.py +++ b/src/atomate2/cp2k/flows/core.py @@ -26,16 +26,6 @@ from atomate2.cp2k.jobs.base import BaseCp2kMaker -__all__ = [ - "DoubleRelaxMaker", - "BandStructureMaker", - "RelaxBandStructureMaker", - "HybridFlowMaker", - "HybridStaticFlowMaker", - "HybridRelaxFlowMaker", - "HybridCellOptFlowMaker", -] - @dataclass class DoubleRelaxMaker(Maker): @@ -56,7 +46,9 @@ class DoubleRelaxMaker(Maker): relax_maker1: Maker = field(default_factory=RelaxMaker) relax_maker2: Maker = field(default_factory=RelaxMaker) - def make(self, structure: Structure, prev_cp2k_dir: str | Path | None = None): + def make( + self, structure: Structure, prev_cp2k_dir: str | Path | None = None + ) -> Flow: """ Create a flow with two chained relaxations. @@ -83,7 +75,7 @@ def make(self, structure: Structure, prev_cp2k_dir: str | Path | None = None): return Flow([relax1, relax2], relax2.output, name=self.name) @classmethod - def from_relax_maker(cls, relax_maker: BaseCp2kMaker): + def from_relax_maker(cls, relax_maker: BaseCp2kMaker) -> DoubleRelaxMaker: """ Instantiate the DoubleRelaxMaker with two relax makers of the same type. @@ -122,7 +114,9 @@ class BandStructureMaker(Maker): static_maker: Maker = field(default_factory=StaticMaker) bs_maker: Maker = field(default_factory=NonSCFMaker) - def make(self, structure: Structure, prev_cp2k_dir: str | Path | None = None): + def make( + self, structure: Structure, prev_cp2k_dir: str | Path | None = None + ) -> Flow: """ Create a band structure flow. @@ -142,7 +136,8 @@ def make(self, structure: Structure, prev_cp2k_dir: str | Path | None = None): jobs = [static_job] outputs = {} - if self.bandstructure_type in ("both", "uniform"): + bandstructure_type = self.bandstructure_type + if bandstructure_type in ("both", "uniform"): uniform_job = self.bs_maker.make( static_job.output.structure, prev_cp2k_dir=static_job.output.dir_name, @@ -156,7 +151,7 @@ def make(self, structure: Structure, prev_cp2k_dir: str | Path | None = None): } outputs.update(output) - if self.bandstructure_type in ("both", "line"): + if bandstructure_type in ("both", "line"): line_job = self.bs_maker.make( static_job.output.structure, prev_cp2k_dir=static_job.output.dir_name, @@ -170,10 +165,8 @@ def make(self, structure: Structure, prev_cp2k_dir: str | Path | None = None): } outputs.update(output) - if self.bandstructure_type not in ("both", "line", "uniform"): - raise ValueError( - f"Unrecognised bandstructure type {self.bandstructure_type}" - ) + if bandstructure_type not in ("both", "line", "uniform"): + raise ValueError(f"Unrecognised {bandstructure_type=}") return Flow(jobs, outputs, name=self.name) @@ -199,7 +192,9 @@ class RelaxBandStructureMaker(Maker): relax_maker: Maker = field(default_factory=DoubleRelaxMaker) band_structure_maker: Maker = field(default_factory=BandStructureMaker) - def make(self, structure: Structure, prev_cp2k_dir: str | Path | None = None): + def make( + self, structure: Structure, prev_cp2k_dir: str | Path | None = None + ) -> Flow: """ Run a relaxation and then calculate the uniform and line mode band structures. @@ -248,7 +243,7 @@ class HybridFlowMaker(Maker): pbe_maker: Maker = field(default=lambda: StaticMaker(store_output_data=False)) hybrid_maker: Maker = field(default_factory=HybridStaticMaker) - def __post_init__(self): + def __post_init__(self) -> None: """ Post init updates. @@ -259,7 +254,9 @@ def __post_init__(self): the PBE density matrix, which creates huge speed-ups. Rarely causes problems so it is done as a default here. """ - updates = {"activate_hybrid": {"hybrid_functional": self.hybrid_functional}} + updates: dict[str, dict[str, str | bool]] = { + "activate_hybrid": {"hybrid_functional": self.hybrid_functional} + } if self.initialize_with_pbe: updates["activate_hybrid"].update( {"screen_on_initial_p": True, "screen_p_forces": True} diff --git a/src/atomate2/cp2k/jobs/base.py b/src/atomate2/cp2k/jobs/base.py index 7019bc4fa8..eb28e89c96 100644 --- a/src/atomate2/cp2k/jobs/base.py +++ b/src/atomate2/cp2k/jobs/base.py @@ -33,8 +33,6 @@ if TYPE_CHECKING: from pymatgen.core import Structure -__all__ = ["BaseCp2kMaker", "cp2k_job"] - _DATA_OBJECTS = [ BandStructure, @@ -50,7 +48,7 @@ _FILES_TO_ZIP = ["cp2k.inp", "cp2k.out"] -def cp2k_job(method: Callable): +def cp2k_job(method: Callable) -> job: """ Decorate the ``make`` method of CP2K job makers. @@ -130,7 +128,9 @@ class BaseCp2kMaker(Maker): store_output_data: bool = True @cp2k_job - def make(self, structure: Structure, prev_cp2k_dir: str | Path | None = None): + def make( + self, structure: Structure, prev_cp2k_dir: str | Path | None = None + ) -> Response: """ Run a CP2K calculation. diff --git a/src/atomate2/cp2k/jobs/core.py b/src/atomate2/cp2k/jobs/core.py index ce93c0dcfb..568a97d5cb 100644 --- a/src/atomate2/cp2k/jobs/core.py +++ b/src/atomate2/cp2k/jobs/core.py @@ -39,18 +39,6 @@ logger = logging.getLogger(__name__) -__all__ = [ - "StaticMaker", - "RelaxMaker", - "CellOptMaker", - "HybridStaticMaker", - "HybridRelaxMaker", - "HybridCellOptMaker", - "NonSCFMaker", - "TransmuterMaker", - "MDMaker", -] - @dataclass class StaticMaker(BaseCp2kMaker): @@ -314,7 +302,7 @@ def make( structure: Structure, prev_cp2k_dir: str | Path | None, mode: str = "uniform", - ): + ) -> None: """ Run a non-scf CP2K job. @@ -389,7 +377,7 @@ def make( self, structure: Structure, prev_cp2k_dir: str | Path | None = None, - ): + ) -> None: """ Run a transmuter Cp2k job. diff --git a/src/atomate2/cp2k/powerups.py b/src/atomate2/cp2k/powerups.py index bf4cbcb325..35e696d781 100644 --- a/src/atomate2/cp2k/powerups.py +++ b/src/atomate2/cp2k/powerups.py @@ -47,7 +47,7 @@ def update_user_input_settings( # Convert nested dictionary updates for cp2k inpt settings # into dict_mod update format - def nested_to_dictmod(d, kk="input_set_generator->user_input_settings"): + def nested_to_dictmod(d, kk="input_set_generator->user_input_settings") -> dict: d2 = {} for k, v in d.items(): k2 = kk + f"->{k}" diff --git a/src/atomate2/cp2k/run.py b/src/atomate2/cp2k/run.py index 79e93b59f9..1693fdb34e 100644 --- a/src/atomate2/cp2k/run.py +++ b/src/atomate2/cp2k/run.py @@ -6,7 +6,7 @@ import shlex import subprocess from os.path import expandvars -from typing import TYPE_CHECKING, Any, Sequence +from typing import TYPE_CHECKING, Any from custodian import Custodian from custodian.cp2k.handlers import ( @@ -26,15 +26,12 @@ from atomate2 import SETTINGS if TYPE_CHECKING: + from collections.abc import Sequence + from custodian.custodian import ErrorHandler, Validator from atomate2.cp2k.schemas.task import TaskDocument -__all__ = [ - "JobType", - "run_cp2k", - "should_stop_children", -] _DEFAULT_HANDLERS = ( StdErrHandler(), @@ -71,7 +68,7 @@ def run_cp2k( validators: Sequence[Validator] = _DEFAULT_VALIDATORS, cp2k_job_kwargs: dict[str, Any] = None, custodian_kwargs: dict[str, Any] = None, -): +) -> None: """ Run CP2K. @@ -99,8 +96,8 @@ def run_cp2k( custodian_kwargs : dict Keyword arguments that are passed to :obj:`.Custodian`. """ - cp2k_job_kwargs = {} if cp2k_job_kwargs is None else cp2k_job_kwargs - custodian_kwargs = {} if custodian_kwargs is None else custodian_kwargs + cp2k_job_kwargs = cp2k_job_kwargs or {} + custodian_kwargs = custodian_kwargs or {} cp2k_cmd = expandvars(cp2k_cmd) split_cp2k_cmd = shlex.split(cp2k_cmd) @@ -113,7 +110,7 @@ def run_cp2k( if job_type == JobType.NORMAL: jobs = [Cp2kJob(split_cp2k_cmd, **cp2k_job_kwargs)] else: - raise ValueError(f"Unsupported job type: {job_type}") + raise ValueError(f"Unsupported {job_type=}") c = Custodian( handlers, @@ -164,4 +161,4 @@ def should_stop_children( "limit of electronic/ionic iterations)!" ) - raise RuntimeError(f"Unknown option for defuse_unsuccessful: {handle_unsuccessful}") + raise RuntimeError(f"Unknown option for {handle_unsuccessful=}") diff --git a/src/atomate2/cp2k/schemas/calc_types/_generate.py b/src/atomate2/cp2k/schemas/calc_types/_generate.py index ea2315d434..51a013a280 100644 --- a/src/atomate2/cp2k/schemas/calc_types/_generate.py +++ b/src/atomate2/cp2k/schemas/calc_types/_generate.py @@ -23,7 +23,7 @@ _RUN_TYPES.append(f"{rt}{vdw}{u}") # noqa: PERF401 -def get_enum_source(enum_name, doc, items): +def get_enum_source(enum_name, doc, items) -> str: header = f""" class {enum_name}(ValueEnum): \"\"\" {doc} \"\"\"\n @@ -52,7 +52,7 @@ class {enum_name}(ValueEnum): ) -def get_calc_type_key(rt): +def get_calc_type_key(rt) -> str: """Conveniece function for readability.""" s = "_".join(rt.split()) s = s.replace("+", "_").replace("-", "_").replace("(", "_").replace(")", "") diff --git a/src/atomate2/cp2k/schemas/calc_types/utils.py b/src/atomate2/cp2k/schemas/calc_types/utils.py index 5ec8929856..a22971088c 100644 --- a/src/atomate2/cp2k/schemas/calc_types/utils.py +++ b/src/atomate2/cp2k/schemas/calc_types/utils.py @@ -1,6 +1,6 @@ """Module to define various calculation types as Enums for CP2K.""" +from collections.abc import Iterable from pathlib import Path -from typing import Dict, Iterable from monty.serialization import loadfn from pymatgen.io.cp2k.inputs import Cp2kInput, Keyword, KeywordList @@ -10,7 +10,7 @@ _RUN_TYPE_DATA = loadfn(str(Path(__file__).parent.joinpath("run_types.yaml").resolve())) -def run_type(inputs: Dict) -> RunType: +def run_type(inputs: dict) -> RunType: """ Determine the run_type from the CP2K input dict. @@ -61,7 +61,7 @@ def _variant_equal(v1, v2) -> bool: return RunType(f"LDA{is_hubbard}") -def task_type(inputs: Dict) -> TaskType: +def task_type(inputs: dict) -> TaskType: """ Determine the task type. @@ -103,7 +103,7 @@ def task_type(inputs: Dict) -> TaskType: elif cp2k_run_type.upper() in ["GEO_OPT", "GEOMETRY_OPTIMIZATION", "CELL_OPT"]: calc_type.append("Structure Optimization") - elif cp2k_run_type.upper() in ["BAND"]: + elif cp2k_run_type.upper() == "BAND": calc_type.append("Band") elif cp2k_run_type.upper() in ["MOLECULAR_DYNAMICS", "MD"]: @@ -121,7 +121,7 @@ def task_type(inputs: Dict) -> TaskType: elif cp2k_run_type.upper() in ["ELECTRONIC_SPECTRA", "SPECTRA"]: calc_type.append("Electronic Spectra") - elif cp2k_run_type.upper() in ["NEGF"]: + elif cp2k_run_type.upper() == "NEGF": calc_type.append("Non-equilibrium Green's Function") elif cp2k_run_type.upper() in ["PINT", "DRIVER"]: @@ -130,20 +130,20 @@ def task_type(inputs: Dict) -> TaskType: elif cp2k_run_type.upper() in ["RT_PROPAGATION", "EHRENFEST_DYN"]: calc_type.append("Real-time propagation") - elif cp2k_run_type.upper() in ["BSSE"]: + elif cp2k_run_type.upper() == "BSSE": calc_type.append("Base set superposition error") - elif cp2k_run_type.upper() in ["DEBUG"]: + elif cp2k_run_type.upper() == "DEBUG": calc_type.append("Debug analysis") - elif cp2k_run_type.upper() in ["NONE"]: + elif cp2k_run_type.upper() == "NONE": calc_type.append("None") return TaskType(" ".join(calc_type)) def calc_type( - inputs: Dict, + inputs: dict, ) -> CalcType: """ Determine the calc type. diff --git a/src/atomate2/cp2k/schemas/calculation.py b/src/atomate2/cp2k/schemas/calculation.py index 41c1ec7b70..4a70dab8c9 100644 --- a/src/atomate2/cp2k/schemas/calculation.py +++ b/src/atomate2/cp2k/schemas/calculation.py @@ -2,13 +2,13 @@ import logging import os +from datetime import datetime from pathlib import Path from shutil import which -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import Any, Optional, Union from jobflow.utils import ValueEnum -from pydantic import BaseModel, Field, validator -from pydantic.datetime_parse import datetime +from pydantic import BaseModel, Field, field_validator from pymatgen.command_line.bader_caller import BaderAnalysis from pymatgen.core.structure import Molecule, Structure from pymatgen.core.trajectory import Trajectory @@ -31,15 +31,6 @@ logger = logging.getLogger(__name__) -__all__ = [ - "Status", - "Cp2kObject", - "CalculationInput", - "CalculationOutput", - "RunStatistics", - "Calculation", -] - # Can be expanded if support for other volumetric files is added __is_stored_in_Ha__ = ["v_hartree"] @@ -75,39 +66,41 @@ class CalculationInput(BaseModel): None, description="The input structure/molecule object" ) - atomic_kind_info: Dict = Field( + atomic_kind_info: dict = Field( None, description="Description of parameters used for each atomic kind" ) - cp2k_input: Dict = Field(None, description="The cp2k input used for this task") + cp2k_input: dict = Field(None, description="The cp2k input used for this task") - dft: Dict = Field( + dft: dict = Field( None, description="DFT parameters used in the last calc of this task.", ) - cp2k_global: Dict = Field( + cp2k_global: dict = Field( None, description="CP2K global parameters used in the last calc of this task.", ) - @validator("atomic_kind_info") - def remove_unnecessary(cls, atomic_kind_info): + @field_validator("atomic_kind_info", mode="before") + @classmethod + def remove_unnecessary(cls, atomic_kind_info) -> dict: """Remove unnecessary entry from atomic_kind_info.""" for k in atomic_kind_info: if "total_pseudopotential_energy" in atomic_kind_info[k]: del atomic_kind_info[k]["total_pseudopotential_energy"] return atomic_kind_info - @validator("dft") - def cleanup_dft(cls, dft): + @field_validator("dft", mode="before") + @classmethod + def cleanup_dft(cls, dft) -> dict: """Convert UKS strings to UKS=True.""" if any(v.upper() == "UKS" for v in dft.values()): dft["UKS"] = True return dft @classmethod - def from_cp2k_output(cls, output: Cp2kOutput): + def from_cp2k_output(cls, output: Cp2kOutput) -> "CalculationInput": """Initialize from Cp2kOutput object.""" return cls( structure=output.initial_structure, @@ -157,32 +150,34 @@ class CalculationOutput(BaseModel): structure: Union[Structure, Molecule] = Field( None, description="The final structure/molecule from the calculation" ) - efermi: float = Field( + efermi: Optional[float] = Field( None, description="The Fermi level from the calculation in eV" ) is_metal: bool = Field(None, description="Whether the system is metallic") - bandgap: float = Field(None, description="The band gap from the calculation in eV") - v_hartree: Union[Dict[int, List[float]], None] = Field( + bandgap: Optional[float] = Field( + None, description="The band gap from the calculation in eV" + ) + v_hartree: Union[dict[int, list[float]], None] = Field( None, description="Plane averaged electrostatic potential" ) - cbm: float = Field( + cbm: Optional[float] = Field( None, description="The conduction band minimum in eV (if system is not metallic)", ) - vbm: float = Field( + vbm: Optional[float] = Field( None, description="The valence band maximum in eV (if system is not metallic)" ) - ionic_steps: List[Dict[str, Any]] = Field( + ionic_steps: list[dict[str, Any]] = Field( None, description="Energy, forces, and structure for each ionic step" ) - locpot: Dict[int, List[float]] = Field( + locpot: dict[int, list[float]] = Field( None, description="Average of the local potential along the crystal axes" ) run_stats: RunStatistics = Field( None, description="Summary of runtime statistics for this calculation" ) - scf: List = Field(None, description="SCF optimization steps") + scf: Optional[list] = Field(None, description="SCF optimization steps") @classmethod def from_cp2k_output( @@ -273,12 +268,12 @@ class Calculation(BaseModel): task_name: str = Field( None, description="Name of task given by custodian (e.g., relax1, relax2)" ) - output_file_paths: Dict[str, str] = Field( + output_file_paths: dict[str, str] = Field( None, description="Paths (relative to dir_name) of the CP2K output files " "associated with this calculation", ) - bader: Dict = Field(None, description="Output from the bader software") + bader: Optional[dict] = Field(None, description="Output from the bader software") run_type: RunType = Field( None, description="Calculation run type (e.g., HF, HSE06, PBE)" ) @@ -295,7 +290,7 @@ def from_cp2k_files( dir_name: Union[Path, str], task_name: str, cp2k_output_file: Union[Path, str] = "cp2k.out", - volumetric_files: List[str] = None, + volumetric_files: list[str] = None, parse_dos: Union[str, bool] = False, parse_bandstructure: Union[str, bool] = False, average_v_hartree: bool = True, @@ -305,9 +300,9 @@ def from_cp2k_files( store_trajectory: bool = False, store_scf: bool = False, store_volumetric_data: Optional[ - Tuple[str] + tuple[str] ] = SETTINGS.CP2K_STORE_VOLUMETRIC_DATA, - ) -> Tuple["Calculation", Dict[Cp2kObject, Dict]]: + ) -> tuple["Calculation", dict[Cp2kObject, dict]]: """ Create a CP2K calculation document from a directory and file paths. @@ -374,7 +369,7 @@ def from_cp2k_files( completed_at = str(datetime.fromtimestamp(os.stat(cp2k_output_file).st_mtime)) output_file_paths = _get_output_file_paths(volumetric_files) - cp2k_objects: Dict[Cp2kObject, Any] = _get_volumetric_data( + cp2k_objects: dict[Cp2kObject, Any] = _get_volumetric_data( dir_name, output_file_paths, store_volumetric_data ) cp2k_objects.update(_get_basis_and_potential_files(dir_name)) @@ -383,13 +378,15 @@ def from_cp2k_files( if dos is not None: if strip_dos_projections: dos = Dos(dos.efermi, dos.energies, dos.densities) - cp2k_objects[Cp2kObject.DOS] = dos # type: ignore + cp2k_objects[Cp2kObject.DOS] = dos # type: ignore[index] bandstructure = _parse_bandstructure(parse_bandstructure, cp2k_output) if bandstructure is not None: if strip_bandstructure_projections: bandstructure.projections = {} - cp2k_objects[Cp2kObject.BANDSTRUCTURE] = bandstructure # type: ignore + cp2k_objects[ + Cp2kObject.BANDSTRUCTURE # type: ignore[index] + ] = bandstructure bader = None if run_bader and Cp2kObject.ELECTRON_DENSITY in output_file_paths: @@ -409,9 +406,11 @@ def from_cp2k_files( v_hartree = None if average_v_hartree: if Cp2kObject.v_hartree in cp2k_objects: - v_hartree = cp2k_objects[Cp2kObject.v_hartree] # type: ignore + v_hartree = cp2k_objects[Cp2kObject.v_hartree] # type: ignore[index] elif Cp2kObject.v_hartree in output_file_paths: - v_hartree_file = output_file_paths[Cp2kObject.v_hartree] # type: ignore + v_hartree_file = output_file_paths[ + Cp2kObject.v_hartree # type: ignore[index] + ] v_hartree = VolumetricData.from_cube(dir_name / v_hartree_file) v_hartree.scale(Ha_to_eV) @@ -424,7 +423,7 @@ def from_cp2k_files( if store_trajectory: traj = _parse_trajectory(cp2k_output=cp2k_output) - cp2k_objects[Cp2kObject.TRAJECTORY] = traj # type: ignore + cp2k_objects[Cp2kObject.TRAJECTORY] = traj # type: ignore[index] return ( cls( @@ -447,7 +446,7 @@ def from_cp2k_files( ) -def _get_output_file_paths(volumetric_files: List[str]) -> Dict[Cp2kObject, str]: +def _get_output_file_paths(volumetric_files: list[str]) -> dict[Cp2kObject, str]: """ Get the output file paths for CP2K output files from the list of volumetric files. @@ -458,18 +457,18 @@ def _get_output_file_paths(volumetric_files: List[str]) -> Dict[Cp2kObject, str] Returns ------- - Dict[Cp2kObject, str] + dict[Cp2kObject, str] A mapping between the CP2K object type and the file path. """ output_file_paths = {} - for cp2k_object in Cp2kObject: # type: ignore + for cp2k_object in Cp2kObject: # type: ignore[attr-defined] for volumetric_file in volumetric_files: if cp2k_object.name in str(volumetric_file): output_file_paths[cp2k_object] = str(volumetric_file) return output_file_paths -def _get_basis_and_potential_files(dir_name: Path) -> Dict[Cp2kObject, DataFile]: +def _get_basis_and_potential_files(dir_name: Path) -> dict[Cp2kObject, DataFile]: """ Get the path of the basis and potential files. @@ -478,13 +477,13 @@ def _get_basis_and_potential_files(dir_name: Path) -> Dict[Cp2kObject, DataFile] Calculation summaries will only have metadata (i.e. the name) that matches to the basis/potential contained in these files. """ - data: Dict[Cp2kObject, DataFile] = {} + data: dict[Cp2kObject, DataFile] = {} if Path.exists(dir_name / "BASIS"): - data[Cp2kObject.BASIS] = BasisFile.from_file( # type: ignore + data[Cp2kObject.BASIS] = BasisFile.from_file( # type: ignore[index] str(dir_name / "BASIS") ) if Path.exists(dir_name / "POTENTIAL"): - data[Cp2kObject.POTENTIAL] = PotentialFile.from_file( # type: ignore + data[Cp2kObject.POTENTIAL] = PotentialFile.from_file( # type: ignore[index] str(dir_name / "POTENTIAL") ) return data @@ -492,9 +491,9 @@ def _get_basis_and_potential_files(dir_name: Path) -> Dict[Cp2kObject, DataFile] def _get_volumetric_data( dir_name: Path, - output_file_paths: Dict[Cp2kObject, str], - store_volumetric_data: Optional[Tuple[str]], -) -> Dict[Cp2kObject, VolumetricData]: + output_file_paths: dict[Cp2kObject, str], + store_volumetric_data: Optional[tuple[str]], +) -> dict[Cp2kObject, VolumetricData]: """ Load volumetric data files from a directory. @@ -509,7 +508,7 @@ def _get_volumetric_data( Returns ------- - Dict[Cp2kObject, VolumetricData] + dict[Cp2kObject, VolumetricData] A dictionary mapping the CP2K object data type (`Cp2kObject.v_hartree`, `Cp2kObject.electron_density`, etc) to the volumetric data object. """ @@ -590,7 +589,7 @@ def _parse_trajectory(cp2k_output: Cp2kOutput) -> Optional[Trajectory]: if cp2k_output.filenames.get("ener") else None ) - data = parse_energy_file(ener) if ener else None + data = parse_energy_file(ener) or None constant_lattice = all( s.lattice == cp2k_output.initial_structure.lattice for s in cp2k_output.structures diff --git a/src/atomate2/cp2k/schemas/task.py b/src/atomate2/cp2k/schemas/task.py index 64884edf6f..8f8c0384b5 100644 --- a/src/atomate2/cp2k/schemas/task.py +++ b/src/atomate2/cp2k/schemas/task.py @@ -2,7 +2,7 @@ import logging from collections import OrderedDict from pathlib import Path -from typing import Any, Dict, List, Optional, Tuple, Type, TypeVar, Union +from typing import Any, Optional, TypeVar, Union import numpy as np from emmet.core.math import Matrix3D, Vector3D @@ -28,14 +28,6 @@ from atomate2.utils.datetime import datetime_str from atomate2.utils.path import get_uri -__all__ = [ - "AnalysisSummary", - "AtomicKindSummary", - "InputSummary", - "OutputSummary", - "TaskDocument", -] - logger = logging.getLogger(__name__) _T = TypeVar("_T", bound="TaskDocument") _VOLUMETRIC_FILES = ("v_hartree", "ELECTRON_DENSITY", "SPIN_DENSITY") @@ -49,11 +41,11 @@ class AnalysisSummary(BaseModel): None, description="Percentage change in volume" ) max_force: float = Field(None, description="Maximum force on the atoms") - warnings: List[str] = Field(None, description="Warnings from the VASP drone") - errors: List[str] = Field(None, description="Errors from the VASP drone") + warnings: list[str] = Field(None, description="Warnings from the VASP drone") + errors: list[str] = Field(None, description="Errors from the VASP drone") @classmethod - def from_cp2k_calc_docs(cls, calc_docs: List[Calculation]) -> "AnalysisSummary": + def from_cp2k_calc_docs(cls, calc_docs: list[Calculation]) -> "AnalysisSummary": """ Create analysis summary from CP2K calculation documents. @@ -112,7 +104,7 @@ class AtomicKind(BaseModel): potential: str = Field( None, description="Name of pseudopotential for this atom kind" ) - auxiliary_basis: str = Field( + auxiliary_basis: Optional[str] = Field( None, description="Auxiliary basis for this (if any) for this atom kind" ) ghost: bool = Field(None, description="Whether this atom kind is a ghost") @@ -121,14 +113,14 @@ class AtomicKind(BaseModel): class AtomicKindSummary(BaseModel): """A summary of pseudo-potential type and functional.""" - atomic_kinds: Dict[str, AtomicKind] = Field( - None, description="Dictionary mapping atomic kind labels to their info" + atomic_kinds: dict[str, AtomicKind] = Field( + None, description="dictionary mapping atomic kind labels to their info" ) @classmethod - def from_atomic_kind_info(cls, atomic_kind_info: dict): + def from_atomic_kind_info(cls, atomic_kind_info: dict) -> "AtomicKindSummary": """Initialize from the atomic_kind_info dictionary.""" - d: Dict[str, Dict[str, Any]] = {"atomic_kinds": {}} + d: dict[str, dict[str, Any]] = {"atomic_kinds": {}} for kind, info in atomic_kind_info.items(): d["atomic_kinds"][kind] = { "element": info["element"], @@ -194,13 +186,15 @@ class OutputSummary(BaseModel): energy_per_atom: float = Field( None, description="The final DFT energy per atom for the last calculation" ) - bandgap: float = Field(None, description="The DFT bandgap for the last calculation") - cbm: float = Field(None, description="CBM for this calculation") - vbm: float = Field(None, description="VBM for this calculation") - forces: List[Vector3D] = Field( + bandgap: Optional[float] = Field( + None, description="The DFT bandgap for the last calculation" + ) + cbm: Optional[float] = Field(None, description="CBM for this calculation") + vbm: Optional[float] = Field(None, description="VBM for this calculation") + forces: list[Vector3D] = Field( None, description="Forces on atoms from the last calculation" ) - stress: Matrix3D = Field( + stress: Optional[Matrix3D] = Field( None, description="Stress on the unit cell from the last calculation" ) @@ -240,51 +234,59 @@ def from_cp2k_calc_doc(cls, calc_doc: Calculation) -> "OutputSummary": class TaskDocument(StructureMetadata, MoleculeMetadata): """Definition of CP2K task document.""" - dir_name: str = Field(None, description="The directory for this CP2K task") + dir_name: Optional[str] = Field( + None, description="The directory for this CP2K task" + ) last_updated: str = Field( default_factory=datetime_str, description="Timestamp for this task document was last updated", ) - completed_at: str = Field( + completed_at: Optional[str] = Field( None, description="Timestamp for when this task was completed" ) - input: InputSummary = Field(None, description="The input to the first calculation") - output: OutputSummary = Field( + input: Optional[InputSummary] = Field( + None, description="The input to the first calculation" + ) + output: Optional[OutputSummary] = Field( None, description="The output of the final calculation" ) structure: Union[Structure, Molecule] = Field( None, description="Final output structure from the task" ) - state: Status = Field(None, description="State of this task") - included_objects: List[Cp2kObject] = Field( - None, description="List of CP2K objects included with this task document" + state: Optional[Status] = Field(None, description="State of this task") + included_objects: Optional[list[Cp2kObject]] = Field( + None, description="list of CP2K objects included with this task document" ) - cp2k_objects: Dict[Cp2kObject, Any] = Field( + cp2k_objects: Optional[dict[Cp2kObject, Any]] = Field( None, description="CP2K objects associated with this task" ) - entry: ComputedEntry = Field( + entry: Optional[ComputedEntry] = Field( None, description="The ComputedEntry from the task doc" ) - analysis: AnalysisSummary = Field( + analysis: Optional[AnalysisSummary] = Field( None, description="Summary of structural relaxation and forces" ) - run_stats: Dict[str, RunStatistics] = Field( + run_stats: Optional[dict[str, RunStatistics]] = Field( None, description="Summary of runtime statistics for each calculation in this task", ) - orig_inputs: Dict[str, Cp2kInput] = Field( + orig_inputs: Optional[dict[str, Cp2kInput]] = Field( None, description="Summary of the original CP2K inputs written by custodian" ) - task_label: str = Field(None, description="A description of the task") - tags: List[str] = Field(None, description="Metadata tags for this task document") - author: str = Field(None, description="Author extracted from transformations") - icsd_id: str = Field( + task_label: Optional[str] = Field(None, description="A description of the task") + tags: Optional[list[str]] = Field( + None, description="Metadata tags for this task document" + ) + author: Optional[str] = Field( + None, description="Author extracted from transformations" + ) + icsd_id: Optional[str] = Field( None, description="International crystal structure database id of the structure" ) - calcs_reversed: List[Calculation] = Field( + calcs_reversed: Optional[list[Calculation]] = Field( None, description="The inputs and outputs for all CP2K runs in this task." ) - transformations: Dict[str, Any] = Field( + transformations: Optional[dict[str, Any]] = Field( None, description="Information on the structural transformations, parsed from a " "transformations.json file", @@ -294,24 +296,22 @@ class TaskDocument(StructureMetadata, MoleculeMetadata): description="Information on the custodian settings used to run this " "calculation, parsed from a custodian.json file", ) - additional_json: Dict[str, Any] = Field( + additional_json: Optional[dict[str, Any]] = Field( None, description="Additional json loaded from the calculation directory" ) - _schema: str = Field( - __version__, - description="Version of atomate2 used to create the document", - alias="schema", + schema: str = Field( + __version__, description="Version of atomate2 used to create the document" ) @classmethod def from_directory( - cls: Type[_T], + cls: type[_T], dir_name: Union[Path, str], - volumetric_files: Tuple[str, ...] = _VOLUMETRIC_FILES, + volumetric_files: tuple[str, ...] = _VOLUMETRIC_FILES, store_additional_json: bool = SETTINGS.CP2K_STORE_ADDITIONAL_JSON, - additional_fields: Dict[str, Any] = None, + additional_fields: dict[str, Any] = None, **cp2k_calculation_kwargs, - ) -> _T: + ) -> "TaskDocument": """ Create a task document from a directory containing CP2K files. @@ -324,7 +324,7 @@ def from_directory( volumetric_files Volumetric files to search for. additional_fields - Dictionary of additional fields to add to output document. + dictionary of additional fields to add to output document. **cp2k_calculation_kwargs Additional parsing options that will be passed to the :obj:`.Calculation.from_cp2k_files` function. @@ -336,7 +336,7 @@ def from_directory( """ logger.info(f"Getting task doc in: {dir_name}") - additional_fields = {} if additional_fields is None else additional_fields + additional_fields = additional_fields or {} dir_name = Path(dir_name) task_files = _find_cp2k_files(dir_name, volumetric_files=volumetric_files) @@ -374,27 +374,26 @@ def from_directory( if cp2k_objects: included_objects = list(cp2k_objects) - if isinstance(calcs_reversed[-1].output.structure, Structure): + if isinstance(calcs_reversed[0].output.structure, Structure): attr = "from_structure" dat = { - "structure": calcs_reversed[-1].output.structure, - "meta_structure": calcs_reversed[-1].output.structure, + "structure": calcs_reversed[0].output.structure, + "meta_structure": calcs_reversed[0].output.structure, "include_structure": True, } - elif isinstance(calcs_reversed[-1].output.structure, Molecule): + elif isinstance(calcs_reversed[0].output.structure, Molecule): attr = "from_molecule" dat = { - "structure": calcs_reversed[-1].output.structure, - "meta_structure": calcs_reversed[-1].output.structure, - "molecule": calcs_reversed[-1].output.structure, + "structure": calcs_reversed[0].output.structure, + "meta_structure": calcs_reversed[0].output.structure, + "molecule": calcs_reversed[0].output.structure, "include_molecule": True, } doc = getattr(cls, attr)(**dat) - ddict = doc.dict() data = { - "structure": calcs_reversed[-1].output.structure, - "meta_structure": calcs_reversed[-1].output.structure, + "structure": calcs_reversed[0].output.structure, + "meta_structure": calcs_reversed[0].output.structure, "dir_name": dir_name, "calcs_reversed": calcs_reversed, "analysis": analysis, @@ -405,22 +404,22 @@ def from_directory( "icsd_id": icsd_id, "tags": tags, "author": author, - "completed_at": calcs_reversed[-1].completed_at, + "completed_at": calcs_reversed[0].completed_at, "input": InputSummary.from_cp2k_calc_doc(calcs_reversed[0]), - "output": OutputSummary.from_cp2k_calc_doc(calcs_reversed[-1]), + "output": OutputSummary.from_cp2k_calc_doc(calcs_reversed[0]), "state": _get_state(calcs_reversed, analysis), "entry": cls.get_entry(calcs_reversed), "run_stats": _get_run_stats(calcs_reversed), "cp2k_objects": cp2k_objects, "included_objects": included_objects, } - doc = cls(**ddict) + doc = cls(**doc.dict()) doc = doc.copy(update=data) return doc.copy(update=additional_fields) @staticmethod def get_entry( - calc_docs: List[Calculation], job_id: Optional[str] = None + calc_docs: list[Calculation], job_id: Optional[str] = None ) -> ComputedEntry: """ Get a computed entry from a list of CP2K calculation documents. @@ -455,8 +454,8 @@ def get_entry( def _find_cp2k_files( path: Union[str, Path], - volumetric_files: Tuple[str, ...] = _VOLUMETRIC_FILES, -) -> Dict[str, Any]: + volumetric_files: tuple[str, ...] = _VOLUMETRIC_FILES, +) -> dict[str, Any]: """ Find CP2K files in a directory. @@ -487,8 +486,8 @@ def _find_cp2k_files( path = Path(path) task_files = OrderedDict() - def _get_task_files(files, suffix=""): - cp2k_files = {} + def _get_task_files(files, suffix="") -> dict[str, Any]: + cp2k_files: dict[str, Any] = {} vol_files = [] for file in files: if file.match(f"*cp2k.out{suffix}*"): @@ -526,7 +525,7 @@ def _get_task_files(files, suffix=""): return task_files -def _parse_orig_inputs(dir_name: Path) -> Dict[str, Cp2kInput]: +def _parse_orig_inputs(dir_name: Path) -> dict[str, Cp2kInput]: """ Parse original input files. @@ -540,7 +539,7 @@ def _parse_orig_inputs(dir_name: Path) -> Dict[str, Cp2kInput]: Returns ------- - Dict[str, Cp2kInput] + dict[str, Cp2kInput] The original data. """ orig_inputs = {} @@ -577,15 +576,15 @@ def _get_max_force(calc_doc: Calculation) -> Optional[float]: return None -def _get_state(calc_docs: List[Calculation], analysis: AnalysisSummary) -> Status: +def _get_state(calc_docs: list[Calculation], analysis: AnalysisSummary) -> Status: """Get state from calculation documents and relaxation analysis.""" all_calcs_completed = all(c.has_cp2k_completed == Status.SUCCESS for c in calc_docs) if len(analysis.errors) == 0 and all_calcs_completed: - return Status.SUCCESS # type: ignore - return Status.FAILED # type: ignore + return Status.SUCCESS # type: ignore[return-value] + return Status.FAILED # type: ignore[return-value] -def _get_run_stats(calc_docs: List[Calculation]) -> Dict[str, RunStatistics]: +def _get_run_stats(calc_docs: list[Calculation]) -> dict[str, RunStatistics]: """Get summary of runtime statistics for each calculation in this task.""" run_stats = {} total = { diff --git a/src/atomate2/cp2k/sets/base.py b/src/atomate2/cp2k/sets/base.py index d02abd29c7..7aa3fdb0c5 100644 --- a/src/atomate2/cp2k/sets/base.py +++ b/src/atomate2/cp2k/sets/base.py @@ -32,8 +32,6 @@ _BASE_CP2K_SET = loadfn(resource_filename("atomate2.cp2k.sets", "BaseCp2kSet.yaml")) _BASE_GAPW_SET = loadfn(resource_filename("atomate2.cp2k.sets", "BaseAllSet.yaml")) -__all__ = ["Cp2kInputSet", "Cp2kInputGenerator", "Cp2kAllElectronInputGenerator"] - class Cp2kInputSet(InputSet): """A class to represent a set of CP2K inputs.""" @@ -42,7 +40,7 @@ def __init__( self, cp2k_input: Cp2kInput, optional_files: dict | None = None, - ): + ) -> None: """ Initialize the set. @@ -79,14 +77,14 @@ def __init__( """ self.cp2k_input = cp2k_input - self.optional_files = {} if optional_files is None else optional_files + self.optional_files = optional_files or {} def write_input( self, directory: str | Path, make_dir: bool = True, overwrite: bool = True, - ): + ) -> None: """ Write Cp2k input file to a directory. @@ -100,8 +98,8 @@ def write_input( Whether to overwrite an input file if it already exists. """ directory = Path(directory) - if make_dir and not directory.exists(): - os.makedirs(directory) + if make_dir: + os.makedirs(directory, exist_ok=True) inputs = { "input": {"filename": "cp2k.inp", "object": self.cp2k_input}, @@ -118,7 +116,9 @@ def write_input( raise FileExistsError(f"{directory / fn} already exists.") @staticmethod - def from_directory(directory: str | Path, optional_files: dict = None): + def from_directory( + directory: str | Path, optional_files: dict = None + ) -> Cp2kInputSet: """ Load a set of CP2K inputs from a directory. @@ -190,7 +190,7 @@ class Cp2kInputGenerator(InputGenerator): force_gamma: bool = False config_dict: dict = field(default_factory=lambda: _BASE_CP2K_SET) - def get_input_set( # type: ignore + def get_input_set( self, structure: Structure | Molecule = None, prev_dir: str | Path = None, @@ -249,7 +249,7 @@ def get_input_set( # type: ignore return Cp2kInputSet(cp2k_input=cp2k_input, optional_files=optional_files) - def get_input_updates(self, structure, prev_input) -> dict: + def get_input_updates(self, structure: Structure, prev_input: Cp2kInput) -> dict: """ Get updates to the cp2k input for this calculation type. @@ -272,9 +272,7 @@ def get_input_updates(self, structure, prev_input) -> dict: raise NotImplementedError def get_kpoints_updates( - self, - structure: Structure, - prev_input: Cp2kInput = None, + self, structure: Structure, prev_input: Cp2kInput = None ) -> dict: """ Get updates to the kpoints configuration for this calculation type. @@ -299,7 +297,9 @@ def get_kpoints_updates( """ return {} - def _get_previous(self, structure: Structure = None, prev_dir: str | Path = None): + def _get_previous( + self, structure: Structure = None, prev_dir: str | Path = None + ) -> tuple[Structure, Cp2kInput, Cp2kOutput]: """Load previous calculation outputs and decide which structure to use.""" if structure is None and prev_dir is None: raise ValueError("Either structure or prev_dir must be set.") @@ -317,7 +317,7 @@ def _get_previous(self, structure: Structure = None, prev_dir: str | Path = None return structure, prev_input, cp2k_output - def _get_structure(self, structure): + def _get_structure(self, structure) -> Structure: """Get the standardized structure.""" if self.sort_structure and hasattr(structure, "get_sorted_structure"): structure = structure.get_sorted_structure() @@ -329,10 +329,10 @@ def _get_input( kpoints: Kpoints | None = None, previous_input: Cp2kInput = None, input_updates: dict = None, - ): + ) -> Cp2kInput: """Get the input.""" - previous_input = {} if previous_input is None else previous_input - input_updates = {} if input_updates is None else input_updates + previous_input = previous_input or {} + input_updates = input_updates or {} input_settings = dict(self.config_dict["cp2k_input"]) # Generate base input but override with user input settings @@ -351,15 +351,15 @@ def _get_input( and input_settings[setting] and callable(getattr(cp2k_input, setting)) ): - subsettings = input_settings.get(setting) + sub_settings = input_settings.get(setting) getattr(cp2k_input, setting)( - **subsettings if isinstance(subsettings, dict) else {} + **sub_settings if isinstance(sub_settings, dict) else {} ) cp2k_input.update(overrides) return cp2k_input - def _get_basis_file(self, cp2k_input: Cp2kInput): + def _get_basis_file(self, cp2k_input: Cp2kInput) -> BasisFile: """ Get the basis sets for the input object and convert them to a basis file object. @@ -376,7 +376,7 @@ def _get_basis_file(self, cp2k_input: Cp2kInput): cp2k_input.safeset({"force_eval": {"dft": {"BASIS_SET_FILE_NAME": "BASIS"}}}) return BasisFile(objects=basis_sets) - def _get_potential_file(self, cp2k_input: Cp2kInput): + def _get_potential_file(self, cp2k_input: Cp2kInput) -> PotentialFile: """ Get the potentials and convert them to a potential file object. @@ -401,7 +401,7 @@ def _get_kpoints( kpoints_updates: dict[str, Any] | None, ) -> Kpoints | None: """Get the kpoints object.""" - kpoints_updates = {} if kpoints_updates is None else kpoints_updates + kpoints_updates = kpoints_updates or {} # use user setting if set otherwise default to base config settings if self.user_kpoints_settings != {}: @@ -535,7 +535,7 @@ def _get_kpoints( # TODO From `atomate2.vasp.sets.base`. Should possibly go in common. # only reservation is if, eventually, CP2K gets it own kpoint object version # instead of using the vasp kpoint objects. -def _combine_kpoints(*kpoints_objects: Kpoints): +def _combine_kpoints(*kpoints_objects: Kpoints) -> Kpoints: """Combine k-points files together.""" labels = [] kpoints = [] @@ -599,7 +599,7 @@ def _get_kpoints( return None -def recursive_update(d: dict, u: dict): +def recursive_update(d: dict, u: dict) -> dict: """ Update a dictionary recursively and return it. diff --git a/src/atomate2/cp2k/sets/core.py b/src/atomate2/cp2k/sets/core.py index a59badc25d..bc007ea2d5 100644 --- a/src/atomate2/cp2k/sets/core.py +++ b/src/atomate2/cp2k/sets/core.py @@ -18,18 +18,6 @@ logger = logging.getLogger(__name__) -__all__ = [ - "StaticSetGenerator", - "RelaxSetGenerator", - "CellOptSetGenerator", - "HybridStaticSetGenerator", - "HybridRelaxSetGenerator", - "HybridCellOptSetGenerator", - "NonSCFSetGenerator", - "MDSetGenerator", -] - - @dataclass class StaticSetGenerator(Cp2kInputGenerator): """Class to generate CP2K static input sets.""" @@ -76,7 +64,7 @@ def get_input_updates(self, *args, **kwargs) -> dict: class HybridStaticSetGenerator(Cp2kInputGenerator): """Class for generating static hybrid input sets.""" - def get_input_updates(self, structure, *args, **kwargs) -> dict: + def get_input_updates(self, structure: Structure, *args, **kwargs) -> dict: """Get input updates for a hybrid calculation.""" updates: dict = { "run_type": "ENERGY_FORCE", @@ -100,7 +88,7 @@ def get_input_updates(self, structure, *args, **kwargs) -> dict: class HybridRelaxSetGenerator(Cp2kInputGenerator): """Class for generating hybrid relaxation input sets.""" - def get_input_updates(self, structure, *args, **kwargs) -> dict: + def get_input_updates(self, structure: Structure, *args, **kwargs) -> dict: """Get input updates for a hybrid calculation.""" updates: dict = { "run_type": "GEO_OPT", @@ -125,7 +113,7 @@ def get_input_updates(self, structure, *args, **kwargs) -> dict: class HybridCellOptSetGenerator(Cp2kInputGenerator): """Class for generating hybrid cell optimization input sets.""" - def get_input_updates(self, structure, *args, **kwargs) -> dict: + def get_input_updates(self, structure: Structure, *args, **kwargs) -> dict: """Get input updates for a hybrid calculation.""" updates: dict = { "run_type": "CELL_OPT", @@ -171,7 +159,7 @@ class NonSCFSetGenerator(Cp2kInputGenerator): reciprocal_density: float = 100 line_density: float = 20 - def __post_init__(self): + def __post_init__(self) -> None: """Ensure mode is set correctly.""" self.mode = self.mode.lower() diff --git a/src/atomate2/forcefields/flows/elastic.py b/src/atomate2/forcefields/flows/elastic.py new file mode 100644 index 0000000000..b3d6533ef5 --- /dev/null +++ b/src/atomate2/forcefields/flows/elastic.py @@ -0,0 +1,86 @@ +"""Flows for calculating elastic constants.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import TYPE_CHECKING + +from atomate2 import SETTINGS +from atomate2.common.flows.elastic import BaseElasticMaker +from atomate2.forcefields.jobs import CHGNetRelaxMaker + +if TYPE_CHECKING: + from atomate2.forcefields.jobs import ForceFieldRelaxMaker + + +@dataclass +class ElasticMaker(BaseElasticMaker): + """ + Maker to calculate elastic constants. + + Calculate the elastic constant of a material. Initially, a tight structural + relaxation is performed to obtain the structure in a state of approximately zero + stress. Subsequently, perturbations are applied to the lattice vectors and the + resulting stress tensor is calculated from DFT, while allowing for relaxation of the + ionic degrees of freedom. Finally, constitutive relations from linear elasticity, + relating stress and strain, are employed to fit the full 6x6 elastic tensor. From + this, aggregate properties such as Voigt and Reuss bounds on the bulk and shear + moduli are derived. + + .. Note:: + It is heavily recommended to symmetrize the structure before passing it to + this flow. Otherwise, the symmetry reduction routines will not be as + effective at reducing the total number of deformations needed. + + Parameters + ---------- + name : str + Name of the flows produced by this maker. + order : int + Order of the tensor expansion to be determined. Can be either 2 or 3. + sym_reduce : bool + Whether to reduce the number of deformations using symmetry. + symprec : float + Symmetry precision to use in the reduction of symmetry. + bulk_relax_maker : .ForceFieldRelaxMaker or None + A maker to perform a tight relaxation on the bulk. Set to ``None`` to skip the + bulk relaxation. + elastic_relax_maker : .ForceFieldRelaxMaker + Maker used to generate elastic relaxations. + generate_elastic_deformations_kwargs : dict + Keyword arguments passed to :obj:`generate_elastic_deformations`. + fit_elastic_tensor_kwargs : dict + Keyword arguments passed to :obj:`fit_elastic_tensor`. + task_document_kwargs : dict + Additional keyword args passed to :obj:`.ElasticDocument.from_stresses()`. + """ + + name: str = "elastic" + order: int = 2 + sym_reduce: bool = True + symprec: float = SETTINGS.SYMPREC + bulk_relax_maker: ForceFieldRelaxMaker | None = field( + default_factory=lambda: CHGNetRelaxMaker( + relax_cell=True, relax_kwargs={"fmax": 0.00001} + ) + ) + elastic_relax_maker: ForceFieldRelaxMaker | None = field( + default_factory=lambda: CHGNetRelaxMaker( + relax_cell=False, relax_kwargs={"fmax": 0.00001} + ) + ) # constant volume relaxation + generate_elastic_deformations_kwargs: dict = field(default_factory=dict) + fit_elastic_tensor_kwargs: dict = field(default_factory=dict) + task_document_kwargs: dict = field(default_factory=dict) + + @property + def prev_calc_dir_argname(self): + """Name of argument informing static maker of previous calculation directory. + + As this differs between different DFT codes (e.g., VASP, CP2K), it + has been left as a property to be implemented by the inheriting class. + + Note: this is only applicable if a relax_maker is specified; i.e., two + calculations are performed for each ordering (relax -> static) + """ + return diff --git a/src/atomate2/forcefields/flows/phonons.py b/src/atomate2/forcefields/flows/phonons.py index 21a55de062..ad76c76eee 100644 --- a/src/atomate2/forcefields/flows/phonons.py +++ b/src/atomate2/forcefields/flows/phonons.py @@ -28,8 +28,6 @@ from atomate2.vasp.jobs.base import BaseVaspMaker -__all__ = ["PhononMaker"] - @dataclass class PhononMaker(Maker): @@ -155,7 +153,7 @@ def make( epsilon_static: Matrix3D | None = None, total_dft_energy_per_formula_unit: float | None = None, supercell_matrix: Matrix3D | None = None, - ): + ) -> Flow: """ Make flow to calculate the phonon properties. diff --git a/src/atomate2/forcefields/flows/relax.py b/src/atomate2/forcefields/flows/relax.py index b35da50622..27ca123927 100644 --- a/src/atomate2/forcefields/flows/relax.py +++ b/src/atomate2/forcefields/flows/relax.py @@ -35,7 +35,7 @@ class CHGNetVaspRelaxMaker(Maker): chgnet_maker: CHGNetRelaxMaker = field(default_factory=CHGNetRelaxMaker) vasp_maker: BaseVaspMaker = field(default_factory=RelaxMaker) - def make(self, structure: Structure): + def make(self, structure: Structure) -> Flow: """ Create a flow with a CHGNet (pre)relaxation followed by a VASP relaxation. @@ -76,7 +76,7 @@ class M3GNetVaspRelaxMaker(Maker): m3gnet_maker: M3GNetRelaxMaker = field(default_factory=M3GNetRelaxMaker) vasp_maker: BaseVaspMaker = field(default_factory=RelaxMaker) - def make(self, structure: Structure): + def make(self, structure: Structure) -> Flow: """ Create a flow with a M3GNet (pre)relaxation followed by a VASP relaxation. diff --git a/src/atomate2/forcefields/jobs.py b/src/atomate2/forcefields/jobs.py index 345dc941e4..8680c25e59 100644 --- a/src/atomate2/forcefields/jobs.py +++ b/src/atomate2/forcefields/jobs.py @@ -19,18 +19,6 @@ logger = logging.getLogger(__name__) -__all__ = [ - "ForceFieldStaticMaker", - "ForceFieldRelaxMaker", - "CHGNetStaticMaker", - "CHGNetRelaxMaker", - "M3GNetStaticMaker", - "M3GNetRelaxMaker", - "GAPRelaxMaker", - "GAPStaticMaker", -] - - @dataclass class ForceFieldRelaxMaker(Maker): """ @@ -41,7 +29,7 @@ class ForceFieldRelaxMaker(Maker): name : str The job name. force_field_name : str - The name of the forcefield. + The name of the force field. relax_cell : bool Whether to allow the cell shape/volume to change during relaxation. steps : int @@ -63,9 +51,9 @@ class ForceFieldRelaxMaker(Maker): task_document_kwargs: dict = field(default_factory=dict) @job(output_schema=ForceFieldTaskDocument) - def make(self, structure: Structure): + def make(self, structure: Structure) -> ForceFieldTaskDocument: """ - Perform a relaxation of a structure using a forcefield. + Perform a relaxation of a structure using a force field. Parameters ---------- @@ -90,7 +78,7 @@ def make(self, structure: Structure): **self.task_document_kwargs, ) - def _relax(self, structure): + def _relax(self, structure: Structure) -> dict: raise NotImplementedError @@ -114,9 +102,9 @@ class ForceFieldStaticMaker(ForceFieldRelaxMaker): task_document_kwargs: dict = field(default_factory=dict) @job(output_schema=ForceFieldTaskDocument) - def make(self, structure: Structure): + def make(self, structure: Structure) -> ForceFieldTaskDocument: """ - Perform a static evaluation using a forcefield. + Perform a static evaluation using a force field. Parameters ---------- @@ -134,14 +122,14 @@ def make(self, structure: Structure): return ForceFieldTaskDocument.from_ase_compatible_result( self.force_field_name, result, - False, - 1, - None, - None, + relax_cell=False, + steps=1, + relax_kwargs=None, + optimizer_kwargs=None, **self.task_document_kwargs, ) - def _evaluate_static(self, structure): + def _evaluate_static(self, structure: Structure) -> dict: raise NotImplementedError @@ -153,7 +141,7 @@ class CHGNetRelaxMaker(ForceFieldRelaxMaker): Parameters ---------- force_field_name : str - The name of the forcefield. + The name of the force field. relax_cell : bool Whether to allow the cell shape/volume to change during relaxation. steps : int @@ -174,7 +162,7 @@ class CHGNetRelaxMaker(ForceFieldRelaxMaker): optimizer_kwargs: dict = field(default_factory=dict) task_document_kwargs: dict = field(default_factory=dict) - def _relax(self, structure): + def _relax(self, structure: Structure) -> dict: from chgnet.model import StructOptimizer relaxer = StructOptimizer(**self.optimizer_kwargs) @@ -200,7 +188,7 @@ class CHGNetStaticMaker(ForceFieldStaticMaker): force_field_name = "CHGNet" task_document_kwargs: dict = field(default_factory=dict) - def _evaluate_static(self, structure): + def _evaluate_static(self, structure: Structure) -> dict: from chgnet.model import StructOptimizer relaxer = StructOptimizer() @@ -217,7 +205,7 @@ class M3GNetRelaxMaker(ForceFieldRelaxMaker): name : str The job name. force_field_name : str - The name of the forcefield. + The name of the force field. relax_cell : bool Whether to allow the cell shape/volume to change during relaxation. steps : int @@ -238,7 +226,7 @@ class M3GNetRelaxMaker(ForceFieldRelaxMaker): optimizer_kwargs: dict = field(default_factory=dict) task_document_kwargs: dict = field(default_factory=dict) - def _relax(self, structure): + def _relax(self, structure: Structure) -> dict: import matgl from matgl.ext.ase import Relaxer @@ -269,7 +257,7 @@ class M3GNetStaticMaker(ForceFieldStaticMaker): name : str The job name. force_field_name : str - The name of the forcefield. + The name of the force field. task_document_kwargs : dict Additional keyword args passed to :obj:`.ForceFieldTaskDocument()`. """ @@ -278,7 +266,7 @@ class M3GNetStaticMaker(ForceFieldStaticMaker): force_field_name: str = "M3GNet" task_document_kwargs: dict = field(default_factory=dict) - def _evaluate_static(self, structure): + def _evaluate_static(self, structure: Structure) -> dict: import matgl from matgl.ext.ase import Relaxer @@ -307,7 +295,7 @@ class GAPRelaxMaker(ForceFieldRelaxMaker): name : str The job name. force_field_name : str - The name of the forcefield. + The name of the force field. relax_cell : bool Whether to allow the cell shape/volume to change during relaxation. steps : int @@ -337,7 +325,7 @@ class GAPRelaxMaker(ForceFieldRelaxMaker): potential_param_file_name: str = "gap.xml" potential_kwargs: dict = field(default_factory=dict) - def _relax(self, structure): + def _relax(self, structure: Structure) -> dict: from quippy.potential import Potential calculator = Potential( @@ -359,7 +347,7 @@ class GAPStaticMaker(ForceFieldStaticMaker): name : str The job name. force_field_name : str - The name of the forcefield. + The name of the force field. task_document_kwargs : dict Additional keyword args passed to :obj:`.ForceFieldTaskDocument()`. potential_args_str: str @@ -376,7 +364,7 @@ class GAPStaticMaker(ForceFieldStaticMaker): potential_param_file_name: str | Path = "gap.xml" potential_kwargs: dict = field(default_factory=dict) - def _evaluate_static(self, structure): + def _evaluate_static(self, structure: Structure) -> dict: from quippy.potential import Potential calculator = Potential( diff --git a/src/atomate2/forcefields/schemas.py b/src/atomate2/forcefields/schemas.py index e3a9810398..4efb7f032b 100644 --- a/src/atomate2/forcefields/schemas.py +++ b/src/atomate2/forcefields/schemas.py @@ -1,19 +1,24 @@ """Job to prerelax a structure using an MD Potential.""" -from typing import List, Optional +from typing import Optional +from ase.stress import voigt_6_to_full_3x3_stress +from ase.units import GPa +from emmet.core.math import Matrix3D, Vector3D from emmet.core.structure import StructureMetadata from pydantic import BaseModel, Extra, Field from pymatgen.core.structure import Structure from pymatgen.io.ase import AseAtomsAdaptor -class IonicStep(BaseModel, extra=Extra.allow): # type: ignore +class IonicStep(BaseModel, extra=Extra.allow): # type: ignore[call-arg] """Document defining the information at each ionic step.""" energy: float = Field(None, description="The free energy.") - forces: List[List[float]] = Field(None, description="The forces on each atom.") - stress: List[float] = Field(None, description="The stress on the lattice.") + forces: Optional[list[list[float]]] = Field( + None, description="The forces on each atom." + ) + stress: Optional[Matrix3D] = Field(None, description="The stress on the lattice.") structure: Structure = Field(None, description="The structure at this step.") @@ -26,16 +31,13 @@ class InputDoc(BaseModel): description="Whether cell lattice was allowed to change during relaxation.", ) steps: int = Field( - None, - description="Maximum number of steps allowed during relaxation.", + None, description="Maximum number of steps allowed during relaxation." ) - relax_kwargs: dict = Field( - None, - description="Keyword arguments that passed to the relaxer function.", + relax_kwargs: Optional[dict] = Field( + None, description="Keyword arguments that passed to the relaxer function." ) - optimizer_kwargs: dict = Field( - None, - description="Keyword arguments passed to the relaxer's optimizer.", + optimizer_kwargs: Optional[dict] = Field( + None, description="Keyword arguments passed to the relaxer's optimizer." ) @@ -51,18 +53,18 @@ class OutputDoc(BaseModel): description="Energy per atom of the final structure in units of eV/atom.", ) - forces: List[List[float]] = Field( + forces: Optional[list[Vector3D]] = Field( None, description="The force on each atom in units of eV/A for the final structure.", ) # NOTE: units for stresses were converted to kbar (* -10 from standard output) # to comply with MP convention - stress: List[float] = Field( + stress: Optional[Matrix3D] = Field( None, description="The stress on the cell in units of kbar (in Voigt notation)." ) - ionic_steps: List[IonicStep] = Field( + ionic_steps: list[IonicStep] = Field( None, description="Step-by-step trajectory of the structural relaxation." ) @@ -110,14 +112,14 @@ def from_ase_compatible_result( relax_kwargs: dict = None, optimizer_kwargs: dict = None, ionic_step_data: tuple = ("energy", "forces", "magmoms", "stress", "structure"), - ): + ) -> "ForceFieldTaskDocument": """ Create a ForceFieldTaskDocument for a Task that has ASE-compatible outputs. Parameters ---------- forcefield_name : str - Name of the forcefield used. + Name of the force field used. result : dict The outputted results from the task. relax_cell : bool @@ -133,10 +135,13 @@ def from_ase_compatible_result( """ trajectory = result["trajectory"].__dict__ - # NOTE: units for stresses were converted to kbar (* -10 from standard output) - # to comply with MP convention + # NOTE: units for stresses were converted from ev/Angstrom³ to kbar + # (* -1 from standard output) + # and to 3x3 matrix to comply with MP convention for i in range(len(trajectory["stresses"])): - trajectory["stresses"][i] = trajectory["stresses"][i] * -10 + trajectory["stresses"][i] = voigt_6_to_full_3x3_stress( + trajectory["stresses"][i] * -10 / GPa + ) species = AseAtomsAdaptor.get_structure(trajectory["atoms"]).species diff --git a/src/atomate2/forcefields/utils.py b/src/atomate2/forcefields/utils.py index fd202f7c3f..0e99b606bd 100644 --- a/src/atomate2/forcefields/utils.py +++ b/src/atomate2/forcefields/utils.py @@ -25,6 +25,9 @@ from pymatgen.io.ase import AseAtomsAdaptor if TYPE_CHECKING: + from os import PathLike + from typing import Any + import numpy as np from ase import Atoms from ase.calculators.calculator import Calculator @@ -47,10 +50,9 @@ class TrajectoryObserver: """Trajectory observer. This is a hook in the relaxation process that saves the intermediate structures. - """ - def __init__(self, atoms: Atoms): + def __init__(self, atoms: Atoms) -> None: """ Initialize the Observer. @@ -69,7 +71,7 @@ def __init__(self, atoms: Atoms): self.atom_positions: list[np.ndarray] = [] self.cells: list[np.ndarray] = [] - def __call__(self): + def __call__(self) -> None: """Save the properties of an Atoms during the relaxation.""" # TODO: maybe include magnetic moments self.energies.append(self.compute_energy()) @@ -88,7 +90,7 @@ def compute_energy(self) -> float: """ return self.atoms.get_potential_energy() - def save(self, filename: str): + def save(self, filename: str | PathLike) -> None: """ Save the trajectory file. @@ -122,15 +124,15 @@ def __init__( calculator: Calculator, optimizer: Optimizer | str = "FIRE", relax_cell: bool = True, - ): + ) -> None: """ Initialize the Relaxer. Parameters ---------- - calculator (ase Calculatur): an ase calculator + calculator (ase Calculator): an ase calculator optimizer (str or ase Optimizer): the optimization algorithm. - relax_cell (bool): if True, cell parameters will be opitimized. + relax_cell (bool): if True, cell parameters will be optimized. """ self.calculator = calculator @@ -151,10 +153,10 @@ def relax( fmax: float = 0.1, steps: int = 500, traj_file: str = None, - interval=1, - verbose=False, + interval: int = 1, + verbose: bool = False, **kwargs, - ): + ) -> dict[str, Any]: """ Relax the structure. diff --git a/src/atomate2/lobster/files.py b/src/atomate2/lobster/files.py index 4ee47d325f..2121ce3255 100644 --- a/src/atomate2/lobster/files.py +++ b/src/atomate2/lobster/files.py @@ -47,7 +47,6 @@ "WAVECAR", "XDATCAR", ] -__all__ = ["copy_lobster_files"] logger = logging.getLogger(__name__) @@ -57,7 +56,7 @@ def copy_lobster_files( src_dir: Path | str, src_host: str | None = None, file_client: FileClient = None, -): +) -> None: """ Copy Lobster files to current directory. diff --git a/src/atomate2/lobster/jobs.py b/src/atomate2/lobster/jobs.py index dade03e967..af4aac1856 100644 --- a/src/atomate2/lobster/jobs.py +++ b/src/atomate2/lobster/jobs.py @@ -13,16 +13,18 @@ from atomate2 import SETTINGS from atomate2.common.files import gzip_output_folder -from atomate2.lobster.files import LOBSTEROUTPUT_FILES, copy_lobster_files +from atomate2.lobster.files import ( + LOBSTEROUTPUT_FILES, + VASP_OUTPUT_FILES, + copy_lobster_files, +) from atomate2.lobster.run import run_lobster from atomate2.lobster.schemas import LobsterTaskDocument -__all__ = ["LobsterMaker"] - logger = logging.getLogger(__name__) -_FILES_TO_ZIP = [*LOBSTEROUTPUT_FILES, "lobsterin"] +_FILES_TO_ZIP = [*LOBSTEROUTPUT_FILES, "lobsterin", *VASP_OUTPUT_FILES] @dataclass @@ -59,7 +61,7 @@ def make( self, wavefunction_dir: str | Path = None, basis_dict: dict | None = None, - ): + ) -> LobsterTaskDocument: """ Run a LOBSTER calculation. diff --git a/src/atomate2/lobster/run.py b/src/atomate2/lobster/run.py index 2ca4eb81b8..f3d4c19d9d 100644 --- a/src/atomate2/lobster/run.py +++ b/src/atomate2/lobster/run.py @@ -6,7 +6,7 @@ import shlex import subprocess from os.path import expandvars -from typing import TYPE_CHECKING, Any, Sequence +from typing import TYPE_CHECKING, Any from custodian import Custodian from custodian.lobster.handlers import EnoughBandsValidator, LobsterFilesValidator @@ -16,9 +16,10 @@ from atomate2 import SETTINGS if TYPE_CHECKING: + from collections.abc import Sequence + from custodian.custodian import Validator -__all__ = ["JobType", "run_lobster"] _DEFAULT_VALIDATORS = (LobsterFilesValidator(), EnoughBandsValidator()) _DEFAULT_HANDLERS = () @@ -46,7 +47,7 @@ def run_lobster( validators: Sequence[Validator] = _DEFAULT_VALIDATORS, lobster_job_kwargs: dict[str, Any] = None, custodian_kwargs: dict[str, Any] = None, -): +) -> None: """ Run Lobster. @@ -69,8 +70,8 @@ def run_lobster( custodian_kwargs : dict Keyword arguments that are passed to :obj:`.Custodian`. """ - lobster_job_kwargs = {} if lobster_job_kwargs is None else lobster_job_kwargs - custodian_kwargs = {} if custodian_kwargs is None else custodian_kwargs + lobster_job_kwargs = lobster_job_kwargs or {} + custodian_kwargs = custodian_kwargs or {} lobster_cmd = expandvars(lobster_cmd) split_lobster_cmd = shlex.split(lobster_cmd) @@ -84,7 +85,7 @@ def run_lobster( if job_type == JobType.NORMAL: jobs = [LobsterJob(split_lobster_cmd, **lobster_job_kwargs)] else: - raise ValueError(f"Unsupported job type: {job_type}") + raise ValueError(f"Unsupported {job_type=}") handlers: list = [] diff --git a/src/atomate2/lobster/schemas.py b/src/atomate2/lobster/schemas.py index 35d13fbcb8..876809baa2 100644 --- a/src/atomate2/lobster/schemas.py +++ b/src/atomate2/lobster/schemas.py @@ -3,7 +3,7 @@ import logging import time from pathlib import Path -from typing import Any, List, Optional, Union +from typing import Any, Optional, Union import numpy as np from emmet.core.structure import StructureMetadata @@ -34,14 +34,6 @@ Analysis = None Description = None -__all__ = [ - "LobsterTaskDocument", - "LobsteroutModel", - "LobsterinModel", - "CondensedBondingAnalysis", - "StrongestBonds", -] - logger = logging.getLogger(__name__) @@ -111,14 +103,14 @@ class LobsterinModel(BaseModel): ) cohpendenergy: float = Field(None, description="End energy for COHP computation") - gaussiansmearingwidth: float = Field( + gaussiansmearingwidth: Optional[float] = Field( None, description="Set the smearing width in eV,default is 0.2 (eV)" ) - usedecimalplaces: int = Field( + usedecimalplaces: Optional[int] = Field( None, description="Set the decimal places to print in output files, default is 5", ) - cohpsteps: float = Field( + cohpsteps: Optional[float] = Field( None, description="Number steps in COHPCAR; similar to NEDOS of VASP" ) basisset: str = Field(None, description="basis set of computation") @@ -129,7 +121,7 @@ class LobsterinModel(BaseModel): saveprojectiontofile: bool = Field( None, description="Save the results of projections" ) - lsodos: bool = Field( + lsodos: Optional[bool] = Field( None, description="Writes DOS output from the orthonormalized LCAO basis" ) basisfunctions: list = Field( @@ -203,7 +195,7 @@ def from_directory( save_cohp_plots: bool = True, plot_kwargs: dict = None, which_bonds: str = "all", - ): + ) -> tuple: """ Create a task document from a directory containing LOBSTER files. @@ -219,7 +211,7 @@ def from_directory( which_bonds: str mode for condensed bonding analysis: "cation-anion" and "all". """ - plot_kwargs = {} if plot_kwargs is None else plot_kwargs + plot_kwargs = plot_kwargs or {} dir_name = Path(dir_name) cohpcar_path = dir_name / "COHPCAR.lobster.gz" charge_path = dir_name / "CHARGE.lobster.gz" @@ -237,7 +229,7 @@ def from_directory( path_to_charge=charge_path, summed_spins=False, # we will always use spin polarization here cutoff_icohp=0.10, - whichbonds=which_bonds, + which_bonds=which_bonds, ) cba_run_time = time.time() - start # initialize lobsterpy condensed bonding analysis @@ -270,7 +262,7 @@ def from_directory( cohp_plot_data=cba_cohp_plot_data, cutoff_icohp=analyse.cutoff_icohp, summed_spins=False, - which_bonds=analyse.whichbonds, + which_bonds=analyse.which_bonds, final_dict_bonds=analyse.final_dict_bonds, final_dict_ions=analyse.final_dict_ions, run_time=cba_run_time, @@ -396,7 +388,7 @@ class LobsterTaskDocument(StructureMetadata): dos: LobsterCompleteDos = Field( None, description="pymatgen pymatgen.io.lobster.Doscar.completedos data" ) - lso_dos: LobsterCompleteDos = Field( + lso_dos: Optional[LobsterCompleteDos] = Field( None, description="pymatgen pymatgen.io.lobster.Doscar.completedos data" ) madelung_energies: dict = Field( @@ -417,16 +409,14 @@ class LobsterTaskDocument(StructureMetadata): "each site as a key and the gross population as a value.", ) - band_overlaps: dict = Field( + band_overlaps: Optional[dict] = Field( None, description="Band overlaps data for each k-point from" " bandOverlaps.lobster file if it exists", ) - _schema: str = Field( - __version__, - description="Version of atomate2 used to create the document", - alias="schema", + schema: str = Field( + __version__, description="Version of atomate2 used to create the document" ) @classmethod @@ -438,7 +428,7 @@ def from_directory( store_lso_dos: bool = False, save_cohp_plots: bool = True, plot_kwargs: dict = None, - ): + ) -> "LobsterTaskDocument": """ Create a task document from a directory containing LOBSTER files. @@ -461,7 +451,7 @@ def from_directory( LobsterTaskDocument A task document for the lobster calculation. """ - additional_fields = {} if additional_fields is None else additional_fields + additional_fields = additional_fields or {} dir_name = Path(dir_name) # Read in lobsterout and lobsterin @@ -649,7 +639,7 @@ def _identify_strongest_bonds( icobilist_path: Path, icohplist_path: Path, icooplist_path: Path, -): +) -> list[StrongestBonds]: """ Identify the strongest bonds and convert them into StrongestBonds objects. @@ -666,8 +656,8 @@ def _identify_strongest_bonds( Returns ------- - Tuple[StrongestBonds] - Tuple of StrongestBonds + list[StrongestBonds] + List of StrongestBonds """ data = [ (icohplist_path, False, False), @@ -693,7 +683,7 @@ def _identify_strongest_bonds( are_cobis=are_cobis, are_coops=are_coops, strongest_bonds=bond_dict, - which_bonds=analyse.whichbonds, + which_bonds=analyse.which_bonds, ) ) else: @@ -704,7 +694,7 @@ def _identify_strongest_bonds( # Don't we have this in pymatgen somewhere? def _get_strong_bonds( bondlist: dict, are_cobis: bool, are_coops: bool, relevant_bonds: dict -): +) -> dict: """ Identify the strongest bonds from a list of bonds. @@ -738,8 +728,8 @@ def _get_strong_bonds( lengths.append(length) bond_labels_unique = list(set(bonds)) - sep_icohp: List[List[float]] = [[] for _ in range(len(bond_labels_unique))] - sep_lengths: List[List[float]] = [[] for _ in range(len(bond_labels_unique))] + sep_icohp: list[list[float]] = [[] for _ in range(len(bond_labels_unique))] + sep_lengths: list[list[float]] = [[] for _ in range(len(bond_labels_unique))] for i, val in enumerate(bond_labels_unique): for j, val2 in enumerate(bonds): diff --git a/src/atomate2/settings.py b/src/atomate2/settings.py index e75b128bd5..8d21c8931f 100644 --- a/src/atomate2/settings.py +++ b/src/atomate2/settings.py @@ -1,15 +1,15 @@ """Settings for atomate2.""" +from __future__ import annotations import warnings from pathlib import Path -from typing import Literal, Optional, Tuple, Union +from typing import Literal, Optional, Union -from pydantic import BaseSettings, Field, root_validator +from pydantic import Field, model_validator +from pydantic_settings import BaseSettings, SettingsConfigDict _DEFAULT_CONFIG_FILE_PATH = "~/.atomate2.yaml" -__all__ = ["Atomate2Settings"] - class Atomate2Settings(BaseSettings): """ @@ -31,7 +31,12 @@ class Atomate2Settings(BaseSettings): SYMPREC: float = Field( 0.1, description="Symmetry precision for spglib symmetry finding." ) - CUSTODIAN_SCRATCH_DIR: str = Field( + BANDGAP_TOL: float = Field( + 1e-4, + description="Tolerance for determining if a material is a semiconductor or " + "metal", + ) + CUSTODIAN_SCRATCH_DIR: Optional[str] = Field( None, description="Path to scratch directory used by custodian." ) @@ -45,7 +50,9 @@ class Atomate2Settings(BaseSettings): VASP_NCL_CMD: str = Field( "vasp_ncl", description="Command to run non-collinear version of VASP." ) - VASP_VDW_KERNEL_DIR: str = Field(None, description="Path to VDW VASP kernel.") + VASP_VDW_KERNEL_DIR: Optional[str] = Field( + None, description="Path to VDW VASP kernel." + ) VASP_INCAR_UPDATES: dict = Field( default_factory=dict, description="Updates to apply to VASP INCAR files." ) @@ -64,16 +71,16 @@ class Atomate2Settings(BaseSettings): VASP_CUSTODIAN_MAX_ERRORS: int = Field( 5, description="Maximum number of errors to correct before custodian gives up" ) - VASP_STORE_VOLUMETRIC_DATA: Optional[Tuple[str]] = Field( + VASP_STORE_VOLUMETRIC_DATA: Optional[tuple[str]] = Field( None, description="Store data from these files in database if present" ) VASP_STORE_ADDITIONAL_JSON: bool = Field( - True, + default=True, description="Ingest any additional JSON data present into database when " "parsing VASP directories useful for storing duplicate of FW.json", ) VASP_RUN_BADER: bool = Field( - False, + default=False, description="Whether to run the Bader program when parsing VASP calculations." "Requires the bader executable to be on the path.", ) @@ -84,6 +91,14 @@ class Atomate2Settings(BaseSettings): "all the files are compressed. If 'atomate' only a selection of files related " "to the simulation will be compressed. If False no file is compressed.", ) + VASP_INHERIT_INCAR: bool = Field( + default=True, + description="Whether to inherit INCAR settings from previous calculation. " + "This might be useful to port Custodian fixes to child jobs but can also be " + "dangerous e.g. when switching from GGA to meta-GGA or relax to static jobs." + "Can be overridden on a per-job basis via the inherit_incar keyword of " + "VaspInputGenerator.", + ) LOBSTER_CMD: str = Field( default="lobster", description="Command to run standard version of VASP." @@ -104,7 +119,7 @@ class Atomate2Settings(BaseSettings): "cp2k.psmp", description="Command to run the MPI version of cp2k" ) CP2K_RUN_BADER: bool = Field( - False, + default=False, description="Whether to run the Bader program when parsing CP2K calculations." "Requires the bader executable to be on the path.", ) @@ -131,17 +146,17 @@ class Atomate2Settings(BaseSettings): CP2K_CUSTODIAN_MAX_ERRORS: int = Field( 5, description="Maximum number of errors to correct before custodian gives up" ) - CP2K_STORE_VOLUMETRIC_DATA: Optional[Tuple[str]] = Field( + CP2K_STORE_VOLUMETRIC_DATA: Optional[tuple[str]] = Field( None, description="Store data from these files in database if present" ) CP2K_STORE_ADDITIONAL_JSON: bool = Field( - True, + default=True, description="Ingest any additional JSON data present into database when " "parsing CP2K directories useful for storing duplicate of FW.json", ) CP2K_ZIP_FILES: Union[bool, Literal["atomate"]] = Field( - True, + default=True, description="Determine if the files in folder are being compressed. If True " "all the files are compressed. If 'atomate' only a selection of files related " "to the simulation will be compressed. If False no file is compressed.", @@ -153,17 +168,15 @@ class Atomate2Settings(BaseSettings): ) # AMSET settings - AMSET_SETTINGS_UPDATE: dict = Field( + AMSET_SETTINGS_UPDATE: Optional[dict] = Field( None, description="Additional settings applied to AMSET settings file." ) - class Config: - """Pydantic config settings.""" - - env_prefix = "atomate2_" + model_config = SettingsConfigDict(env_prefix="atomate2_") - @root_validator(pre=True) - def load_default_settings(cls, values): + @model_validator(mode="before") + @classmethod + def load_default_settings(cls, values) -> dict: """ Load settings from file or environment variables. @@ -174,11 +187,12 @@ def load_default_settings(cls, values): """ from monty.serialization import loadfn - config_file_path: str = values.get("CONFIG_FILE", _DEFAULT_CONFIG_FILE_PATH) + config_file_path = values.get("CONFIG_FILE", _DEFAULT_CONFIG_FILE_PATH) + config_file_path = Path(config_file_path).expanduser() new_values = {} - if Path(config_file_path).expanduser().exists(): - if Path(config_file_path).stat().st_size == 0: + if config_file_path.exists(): + if config_file_path.stat().st_size == 0: warnings.warn( f"Using atomate2 config file at {config_file_path} but it's empty", stacklevel=2, diff --git a/src/atomate2/utils/datetime.py b/src/atomate2/utils/datetime.py index 95f74a19e5..61ae600577 100644 --- a/src/atomate2/utils/datetime.py +++ b/src/atomate2/utils/datetime.py @@ -4,8 +4,6 @@ from datetime import datetime -__all__ = ["datetime_str"] - def datetime_str() -> str: """ diff --git a/src/atomate2/utils/file_client.py b/src/atomate2/utils/file_client.py index 5e4aca0ef3..7529feb408 100644 --- a/src/atomate2/utils/file_client.py +++ b/src/atomate2/utils/file_client.py @@ -16,8 +16,6 @@ from monty.io import zopen from paramiko import SFTPClient, SSHClient -__all__ = ["FileClient", "auto_fileclient"] - class FileClient: """ @@ -43,13 +41,13 @@ def __init__( self, key_filename: str | Path = "~/.ssh/id_rsa", config_filename: str | Path = "~/.ssh/config", - ): + ) -> None: self.key_filename = key_filename self.config_filename = config_filename self.connections: dict[str, dict[str, Any]] = {} - def connect(self, host: str): + def connect(self, host: str) -> None: """ Connect to a remote host. @@ -219,7 +217,7 @@ def copy( dest_filename: str | Path, src_host: str | None = None, dest_host: str | None = None, - ): + ) -> None: """ Copy a file from source to destination. @@ -259,7 +257,7 @@ def copy( "Copying between two different remote hosts is not supported." ) - def remove(self, path: str | Path, host: str | None = None): + def remove(self, path: str | Path, host: str | None = None) -> None: """ Remove a file (does not work on directories). @@ -281,7 +279,7 @@ def rename( old_path: str | Path, new_path: str | Path, host: str | None = None, - ): + ) -> None: """ Rename (move) a file. @@ -354,7 +352,7 @@ def gzip( host: str | None = None, compresslevel: int = 6, force: bool | str = False, - ): + ) -> None: """ Gzip a file. @@ -417,7 +415,7 @@ def gunzip( path: str | Path, host: str | None = None, force: bool | str = False, - ): + ) -> None: """ Ungzip a file. @@ -466,7 +464,7 @@ def gunzip( ssh = self.get_ssh(host) _, stdout, _ = ssh.exec_command(f"gunzip -f {path!s}") - def close(self): + def close(self) -> None: """Close all connections.""" for connection in self.connections.values(): connection["ssh"].close() @@ -477,7 +475,7 @@ def __enter__(self): """Support for "with" context.""" return self - def __exit__(self, exc_type, exc_val, exc_tb): + def __exit__(self, exc_type, exc_val, exc_tb) -> None: """Support for "with" context.""" self.close() diff --git a/src/atomate2/utils/log.py b/src/atomate2/utils/log.py index aecc8f3d2b..55471c5524 100644 --- a/src/atomate2/utils/log.py +++ b/src/atomate2/utils/log.py @@ -5,8 +5,6 @@ import logging import sys -__all__ = ["initialize_logger"] - def initialize_logger(level: int = logging.INFO) -> logging.Logger: """Initialize the default logger. diff --git a/src/atomate2/utils/path.py b/src/atomate2/utils/path.py index 684fbd4959..46764531d1 100644 --- a/src/atomate2/utils/path.py +++ b/src/atomate2/utils/path.py @@ -7,8 +7,6 @@ import socket from pathlib import Path -__all__ = ["get_uri", "strip_hostname"] - def get_uri(dir_name: str | Path) -> str: """ @@ -56,7 +54,9 @@ def strip_hostname(uri_path: str | Path) -> str: return dir_name -def find_recent_logfile(dir_name: Path | str, logfile_extensions: str | list[str]): +def find_recent_logfile( + dir_name: Path | str, logfile_extensions: str | list[str] +) -> str: """ Find the most recent logfile in a given directory. diff --git a/src/atomate2/vasp/builders/elastic.py b/src/atomate2/vasp/builders/elastic.py index 40bf4e6b13..ae1b38ff81 100644 --- a/src/atomate2/vasp/builders/elastic.py +++ b/src/atomate2/vasp/builders/elastic.py @@ -14,6 +14,8 @@ from atomate2.common.schemas.elastic import ElasticDocument if TYPE_CHECKING: + from collections.abc import Generator + from maggma.core import Store @@ -58,7 +60,7 @@ def __init__( fitting_method: str = SETTINGS.ELASTIC_FITTING_METHOD, structure_match_tol: float = 1e-5, **kwargs, - ): + ) -> None: self.tasks = tasks self.elasticity = elasticity self.query = query if query else {} @@ -69,14 +71,14 @@ def __init__( super().__init__(sources=[tasks], targets=[elasticity], **kwargs) - def ensure_indexes(self): + def ensure_indexes(self) -> None: """Ensure indices on the tasks and elasticity collections.""" self.tasks.ensure_index("output.formula_pretty") self.tasks.ensure_index("last_updated") self.elasticity.ensure_index("fitting_data.uuids.0") self.elasticity.ensure_index("last_updated") - def get_items(self): + def get_items(self) -> Generator: """ Get all items to process into elastic documents. @@ -155,7 +157,7 @@ def process_item(self, tasks: list[dict]) -> list[ElasticDocument]: return elastic_docs - def update_targets(self, items: list[ElasticDocument]): + def update_targets(self, items: list[ElasticDocument]) -> None: """ Insert new elastic documents into the elasticity store. @@ -164,11 +166,11 @@ def update_targets(self, items: list[ElasticDocument]): items : list of .ElasticDocument A list of elasticity documents. """ - items = chain.from_iterable(filter(bool, items)) # type: ignore + _items = chain.from_iterable(filter(bool, items)) - if len(items) > 0: - self.logger.info(f"Updating {len(items)} elastic documents") - self.elasticity.update(items, key="fitting_data.uuids.0") + if len(list(_items)) > 0: + self.logger.info(f"Updating {len(list(_items))} elastic documents") + self.elasticity.update(_items, key="fitting_data.uuids.0") else: self.logger.info("No items to update") diff --git a/src/atomate2/vasp/drones.py b/src/atomate2/vasp/drones.py index fd66ad8ad3..27bcd649bf 100644 --- a/src/atomate2/vasp/drones.py +++ b/src/atomate2/vasp/drones.py @@ -11,8 +11,6 @@ logger = logging.getLogger(__name__) -__all__ = ["VaspDrone"] - class VaspDrone(AbstractDrone): """ @@ -24,7 +22,7 @@ class VaspDrone(AbstractDrone): Additional keyword args passed to :obj:`.TaskDoc.from_directory`. """ - def __init__(self, **task_document_kwargs): + def __init__(self, **task_document_kwargs) -> None: self.task_document_kwargs = task_document_kwargs def assimilate(self, path: str | Path | None = None) -> TaskDoc: diff --git a/src/atomate2/vasp/files.py b/src/atomate2/vasp/files.py index 63d47a11e1..70f710b909 100644 --- a/src/atomate2/vasp/files.py +++ b/src/atomate2/vasp/files.py @@ -5,7 +5,7 @@ import logging import re from pathlib import Path -from typing import TYPE_CHECKING, Sequence +from typing import TYPE_CHECKING from atomate2 import SETTINGS from atomate2.common.files import copy_files, get_zfile, gunzip_files, rename_files @@ -13,12 +13,12 @@ from atomate2.utils.path import strip_hostname if TYPE_CHECKING: + from collections.abc import Sequence + from pymatgen.core import Structure from atomate2.vasp.sets.base import VaspInputGenerator -__all__ = ["copy_vasp_outputs", "get_largest_relax_extension"] - logger = logging.getLogger(__name__) @@ -31,7 +31,7 @@ def copy_vasp_outputs( contcar_to_poscar: bool = True, force_overwrite: bool | str = False, file_client: FileClient | None = None, -): +) -> None: """ Copy VASP output files to the current directory. @@ -84,7 +84,7 @@ def copy_vasp_outputs( # check at least one type of POTCAR file is included if len([f for f in optional_files if "POTCAR" in f.name]) == 0: - raise FileNotFoundError("Could not find POTCAR file to copy.") + raise FileNotFoundError(f"Could not find a POTCAR file in {src_dir!r} to copy") copy_files( src_dir, @@ -161,7 +161,7 @@ def write_vasp_input_set( potcar_spec: bool = False, clean_prev: bool = True, **kwargs, -): +) -> None: """ Write VASP input set. diff --git a/src/atomate2/vasp/flows/amset.py b/src/atomate2/vasp/flows/amset.py index 3620cda53e..30a21864cb 100644 --- a/src/atomate2/vasp/flows/amset.py +++ b/src/atomate2/vasp/flows/amset.py @@ -40,7 +40,6 @@ from atomate2.vasp.jobs.base import BaseVaspMaker -__all__ = ["VaspAmsetMaker", "DeformationPotentialMaker", "HSEVaspAmsetMaker"] _DEFAULT_DOPING = ( 1e16, @@ -89,7 +88,7 @@ def make( structure: Structure, prev_vasp_dir: str | Path | None = None, ibands: tuple[list[int], list[int]] = None, - ): + ) -> Flow: """ Make flow to calculate acoustic deformation potentials. @@ -207,7 +206,7 @@ def make( self, structure: Structure, prev_vasp_dir: str | Path | None = None, - ): + ) -> Flow: """ Make flow to calculate electronic transport properties using AMSET and VASP. @@ -237,7 +236,7 @@ def make( # elastic constant elastic = self.elastic_maker.make( static.output.structure, - prev_vasp_dir=static.output.dir_name, + prev_dir=static.output.dir_name, equilibrium_stress=static.output.output.stress, ) @@ -389,7 +388,7 @@ def make( self, structure: Structure, prev_vasp_dir: str | Path | None = None, - ): + ) -> Flow: """ Make flow to calculate electronic transport properties using AMSET and VASP. @@ -419,7 +418,7 @@ def make( # elastic constant elastic = self.elastic_maker.make( static.output.structure, - prev_vasp_dir=static.output.dir_name, + prev_dir=static.output.dir_name, equilibrium_stress=static.output.output.stress, ) diff --git a/src/atomate2/vasp/flows/core.py b/src/atomate2/vasp/flows/core.py index 1be66fbf85..7c6b02dc1e 100644 --- a/src/atomate2/vasp/flows/core.py +++ b/src/atomate2/vasp/flows/core.py @@ -21,25 +21,12 @@ if TYPE_CHECKING: from pathlib import Path + from jobflow import Job from pymatgen.core.structure import Structure from atomate2.vasp.jobs.base import BaseVaspMaker -__all__ = [ - "DoubleRelaxMaker", - "BandStructureMaker", - "UniformBandStructureMaker", - "LineModeBandStructureMaker", - "HSEBandStructureMaker", - "HSEUniformBandStructureMaker", - "HSELineModeBandStructureMaker", - "RelaxBandStructureMaker", - "OpticsMaker", - "HSEOpticsMaker", -] - - @dataclass class DoubleRelaxMaker(Maker): """ @@ -56,10 +43,12 @@ class DoubleRelaxMaker(Maker): """ name: str = "double relax" - relax_maker1: BaseVaspMaker = field(default_factory=RelaxMaker) + relax_maker1: BaseVaspMaker | None = field(default_factory=RelaxMaker) relax_maker2: BaseVaspMaker = field(default_factory=RelaxMaker) - def make(self, structure: Structure, prev_vasp_dir: str | Path | None = None): + def make( + self, structure: Structure, prev_vasp_dir: str | Path | None = None + ) -> Flow: """ Create a flow with two chained relaxations. @@ -75,18 +64,23 @@ def make(self, structure: Structure, prev_vasp_dir: str | Path | None = None): Flow A flow containing two relaxations. """ - relax1 = self.relax_maker1.make(structure, prev_vasp_dir=prev_vasp_dir) - relax1.name += " 1" - - relax2 = self.relax_maker2.make( - relax1.output.structure, prev_vasp_dir=relax1.output.dir_name - ) + jobs: list[Job] = [] + if self.relax_maker1: + # Run a pre-relaxation + relax1 = self.relax_maker1.make(structure, prev_vasp_dir=prev_vasp_dir) + relax1.name += " 1" + jobs += [relax1] + structure = relax1.output.structure + prev_vasp_dir = relax1.output.dir_name + + relax2 = self.relax_maker2.make(structure, prev_vasp_dir=prev_vasp_dir) relax2.name += " 2" + jobs += [relax2] - return Flow([relax1, relax2], relax2.output, name=self.name) + return Flow(jobs, output=relax2.output, name=self.name) @classmethod - def from_relax_maker(cls, relax_maker: BaseVaspMaker): + def from_relax_maker(cls, relax_maker: BaseVaspMaker) -> DoubleRelaxMaker: """ Instantiate the DoubleRelaxMaker with two relax makers of the same type. @@ -125,7 +119,9 @@ class BandStructureMaker(Maker): static_maker: BaseVaspMaker = field(default_factory=StaticMaker) bs_maker: BaseVaspMaker = field(default_factory=NonSCFMaker) - def make(self, structure: Structure, prev_vasp_dir: str | Path | None = None): + def make( + self, structure: Structure, prev_vasp_dir: str | Path | None = None + ) -> Flow: """ Create a band structure flow. @@ -145,7 +141,8 @@ def make(self, structure: Structure, prev_vasp_dir: str | Path | None = None): jobs = [static_job] outputs = {} - if self.bandstructure_type in ("both", "uniform"): + bandstructure_type = self.bandstructure_type + if bandstructure_type in ("both", "uniform"): uniform_job = self.bs_maker.make( static_job.output.structure, prev_vasp_dir=static_job.output.dir_name, @@ -159,7 +156,7 @@ def make(self, structure: Structure, prev_vasp_dir: str | Path | None = None): } outputs.update(output) - if self.bandstructure_type in ("both", "line"): + if bandstructure_type in ("both", "line"): line_job = self.bs_maker.make( static_job.output.structure, prev_vasp_dir=static_job.output.dir_name, @@ -173,10 +170,8 @@ def make(self, structure: Structure, prev_vasp_dir: str | Path | None = None): } outputs.update(output) - if self.bandstructure_type not in ("both", "line", "uniform"): - raise ValueError( - f"Unrecognised bandstructure type {self.bandstructure_type}" - ) + if bandstructure_type not in ("both", "line", "uniform"): + raise ValueError(f"Unrecognised {bandstructure_type=}") return Flow(jobs, outputs, name=self.name) @@ -203,7 +198,9 @@ class UniformBandStructureMaker(Maker): static_maker: BaseVaspMaker = field(default_factory=StaticMaker) bs_maker: BaseVaspMaker = field(default_factory=NonSCFMaker) - def make(self, structure: Structure, prev_vasp_dir: str | Path | None = None): + def make( + self, structure: Structure, prev_vasp_dir: str | Path | None = None + ) -> Flow: """ Create a uniform band structure flow. @@ -252,7 +249,9 @@ class LineModeBandStructureMaker(Maker): static_maker: BaseVaspMaker = field(default_factory=StaticMaker) bs_maker: BaseVaspMaker = field(default_factory=NonSCFMaker) - def make(self, structure: Structure, prev_vasp_dir: str | Path | None = None): + def make( + self, structure: Structure, prev_vasp_dir: str | Path | None = None + ) -> Flow: """ Create a line mode band structure flow. @@ -352,7 +351,7 @@ class HSELineModeBandStructureMaker(LineModeBandStructureMaker): @dataclass class RelaxBandStructureMaker(Maker): """ - Make to create a flow with a relaxation and then band structure calculations. + Maker to create a flow with a relaxation and then band structure calculations. By default, this workflow generates relaxations using the :obj:`.DoubleRelaxMaker`. @@ -370,7 +369,9 @@ class RelaxBandStructureMaker(Maker): relax_maker: BaseVaspMaker = field(default_factory=DoubleRelaxMaker) band_structure_maker: BaseVaspMaker = field(default_factory=BandStructureMaker) - def make(self, structure: Structure, prev_vasp_dir: str | Path | None = None): + def make( + self, structure: Structure, prev_vasp_dir: str | Path | None = None + ) -> Flow: """ Run a relaxation and then calculate the uniform and line mode band structures. @@ -428,7 +429,9 @@ class OpticsMaker(Maker): ) ) - def make(self, structure: Structure, prev_vasp_dir: str | Path | None = None): + def make( + self, structure: Structure, prev_vasp_dir: str | Path | None = None + ) -> Flow: """ Run a static and then a non-scf optics calculation. @@ -485,7 +488,9 @@ class HSEOpticsMaker(Maker): ) ) - def make(self, structure: Structure, prev_vasp_dir: str | Path | None = None): + def make( + self, structure: Structure, prev_vasp_dir: str | Path | None = None + ) -> Flow: """ Run a static and then a non-scf optics calculation. diff --git a/src/atomate2/vasp/flows/defect.py b/src/atomate2/vasp/flows/defect.py index 1993d317dc..e60918dc2e 100644 --- a/src/atomate2/vasp/flows/defect.py +++ b/src/atomate2/vasp/flows/defect.py @@ -62,9 +62,7 @@ user_kpoints_settings=SPECIAL_KPOINT ), task_document_kwargs={"store_volumetric_data": ["locpot"]}, - copy_vasp_kwargs={ - "additional_vasp_files": ("WAVECAR",), - }, + copy_vasp_kwargs={"additional_vasp_files": ("WAVECAR",)}, ), ) GRID_KEYS = ["NGX", "NGY", "NGZ", "NGXF", "NGYF", "NGZF"] @@ -171,7 +169,7 @@ class FormationEnergyMaker(defect_flows.FormationEnergyMaker): bulk_relax_maker: BaseVaspMaker | None = None name: str = "formation energy" - def structure_from_prv(self, previous_dir: str): + def structure_from_prv(self, previous_dir: str) -> Structure: """Copy the output structure from previous directory. Read the vasprun.xml file from the previous directory @@ -194,7 +192,7 @@ def structure_from_prv(self, previous_dir: str): vasprun = Vasprun(vasprun_file) return vasprun.final_structure - def validate_maker(self): + def validate_maker(self) -> None: """Check some key settings in the relax maker. Since this workflow is pretty complex but allows you to use any @@ -206,7 +204,7 @@ def validate_maker(self): `ISIF = 2` and `use_structure_charge = True` """ - def check_defect_relax_maker(relax_maker: RelaxMaker): + def check_defect_relax_maker(relax_maker: RelaxMaker) -> RelaxMaker: input_gen = relax_maker.input_set_generator if input_gen.use_structure_charge is False: raise ValueError("use_structure_charge should be set to True") @@ -251,7 +249,7 @@ class ConfigurationCoordinateMaker(defect_flows.ConfigurationCoordinateMaker): static_maker: BaseVaspMaker = field( default_factory=lambda: StaticMaker(input_set_generator=DEFECT_STATIC_GENERATOR) ) - name: str = "config. coordinate" + name: str = "config coordinate" @dataclass @@ -274,7 +272,7 @@ def make( structure: Structure, charge_state1: int, charge_state2: int, - ): + ) -> Flow: """Create the job for Non-Radiative defect capture. Make a job for the calculation of the configuration coordinate diagram. diff --git a/src/atomate2/vasp/flows/elastic.py b/src/atomate2/vasp/flows/elastic.py index 0041aa320e..800e565eb6 100644 --- a/src/atomate2/vasp/flows/elastic.py +++ b/src/atomate2/vasp/flows/elastic.py @@ -5,32 +5,18 @@ from dataclasses import dataclass, field from typing import TYPE_CHECKING -from jobflow import Flow, Maker, OnMissing -from pymatgen.symmetry.analyzer import SpacegroupAnalyzer - from atomate2 import SETTINGS +from atomate2.common.flows.elastic import BaseElasticMaker from atomate2.vasp.flows.core import DoubleRelaxMaker from atomate2.vasp.jobs.core import TightRelaxMaker -from atomate2.vasp.jobs.elastic import ( - ElasticRelaxMaker, - fit_elastic_tensor, - generate_elastic_deformations, - run_elastic_deformations, -) +from atomate2.vasp.jobs.elastic import ElasticRelaxMaker if TYPE_CHECKING: - from pathlib import Path - - from emmet.core.math import Matrix3D - from pymatgen.core.structure import Structure - from atomate2.vasp.jobs.base import BaseVaspMaker -__all__ = ["ElasticMaker"] - @dataclass -class ElasticMaker(Maker): +class ElasticMaker(BaseElasticMaker): """ Maker to calculate elastic constants. @@ -83,72 +69,14 @@ class ElasticMaker(Maker): fit_elastic_tensor_kwargs: dict = field(default_factory=dict) task_document_kwargs: dict = field(default_factory=dict) - def make( - self, - structure: Structure, - prev_vasp_dir: str | Path | None = None, - equilibrium_stress: Matrix3D = None, - conventional: bool = False, - ): - """ - Make flow to calculate the elastic constant. - - Parameters - ---------- - structure : .Structure - A pymatgen structure. - prev_vasp_dir : str or Path or None - A previous vasp calculation directory to use for copying outputs. - equilibrium_stress : tuple of tuple of float - The equilibrium stress of the (relaxed) structure, if known. - conventional : bool - Whether to transform the structure into the conventional cell. - """ - jobs = [] - - if self.bulk_relax_maker is not None: - # optionally relax the structure - bulk = self.bulk_relax_maker.make(structure, prev_vasp_dir=prev_vasp_dir) - jobs.append(bulk) - structure = bulk.output.structure - prev_vasp_dir = bulk.output.dir_name - if equilibrium_stress is None: - equilibrium_stress = bulk.output.output.stress - - if conventional: - sga = SpacegroupAnalyzer(structure, symprec=self.symprec) - structure = sga.get_conventional_standard_structure() + @property + def prev_calc_dir_argname(self): + """Name of argument informing static maker of previous calculation directory. - deformations = generate_elastic_deformations( - structure, - order=self.order, - sym_reduce=self.sym_reduce, - symprec=self.symprec, - **self.generate_elastic_deformations_kwargs, - ) - vasp_deformation_calcs = run_elastic_deformations( - structure, - deformations.output, - prev_vasp_dir=prev_vasp_dir, - elastic_relax_maker=self.elastic_relax_maker, - ) - fit_tensor = fit_elastic_tensor( - structure, - vasp_deformation_calcs.output, - equilibrium_stress=equilibrium_stress, - order=self.order, - symprec=self.symprec if self.sym_reduce else None, - **self.fit_elastic_tensor_kwargs, - **self.task_document_kwargs, - ) + As this differs between different DFT codes (e.g., VASP, CP2K), it + has been left as a property to be implemented by the inheriting class. - # allow some of the deformations to fail - fit_tensor.config.on_missing_references = OnMissing.NONE - - jobs += [deformations, vasp_deformation_calcs, fit_tensor] - - return Flow( - jobs=jobs, - output=fit_tensor.output, - name=self.name, - ) + Note: this is only applicable if a relax_maker is specified; i.e., two + calculations are performed for each ordering (relax -> static) + """ + return "prev_vasp_dir" diff --git a/src/atomate2/vasp/flows/elph.py b/src/atomate2/vasp/flows/elph.py index 6d36dd3c65..977b89184b 100644 --- a/src/atomate2/vasp/flows/elph.py +++ b/src/atomate2/vasp/flows/elph.py @@ -42,9 +42,6 @@ from atomate2.vasp.jobs.base import BaseVaspMaker -__all__ = ["ElectronPhononMaker"] - - @dataclass class ElectronPhononMaker(Maker): """ @@ -126,7 +123,9 @@ class ElectronPhononMaker(Maker): ) ) - def make(self, structure: Structure, prev_vasp_dir: str | Path | None = None): + def make( + self, structure: Structure, prev_vasp_dir: str | Path | None = None + ) -> Flow: """ Create a electron-phonon coupling workflow. diff --git a/src/atomate2/vasp/flows/lobster.py b/src/atomate2/vasp/flows/lobster.py index 3a8540e0cd..71a16452f9 100644 --- a/src/atomate2/vasp/flows/lobster.py +++ b/src/atomate2/vasp/flows/lobster.py @@ -25,7 +25,6 @@ from atomate2.vasp.jobs.base import BaseVaspMaker -__all__ = ["VaspLobsterMaker"] LOBSTER_UNIFORM_MAKER = UniformBandStructureMaker( name="uniform lobster structure", @@ -49,7 +48,8 @@ "LWAVE": True, "ISYM": 0, }, - ) + ), + task_document_kwargs={"parse_dos": False, "parse_bandstructure": False}, ), ) @@ -62,9 +62,8 @@ class VaspLobsterMaker(Maker): The calculations performed are: 1. Optional optimization. - 2. Optional static computation with symmetry to preconverge the wavefunction. - 3. Static calculation with ISYM=0. - 4. Several Lobster computations testing several basis sets are performed. + 2. Static calculation with ISYM=0. + 3. Several Lobster computations testing several basis sets are performed. .. Note:: @@ -77,9 +76,6 @@ class VaspLobsterMaker(Maker): relax_maker : .BaseVaspMaker or None A maker to perform a relaxation on the bulk. Set to ``None`` to skip the bulk relaxation. - preconverge_static_maker : .BaseVaspMaker or None - A maker to perform a preconvergence run before the wavefunction computation - without symmetry lobster_static_maker : .BaseVaspMaker A maker to perform the computation of the wavefunction before the static run. Cannot be skipped. It can be LOBSTERUNIFORM or LobsterStaticMaker() @@ -109,7 +105,7 @@ def make( self, structure: Structure, prev_vasp_dir: str | Path | None = None, - ): + ) -> Flow: """ Make flow to calculate bonding properties. diff --git a/src/atomate2/vasp/flows/matpes.py b/src/atomate2/vasp/flows/matpes.py new file mode 100644 index 0000000000..fcc747abea --- /dev/null +++ b/src/atomate2/vasp/flows/matpes.py @@ -0,0 +1,68 @@ +""" +Module defining MatPES flows. + +In case of questions, consult @janosh or @esoteric-ephemera. Makes PBE + r2SCAN +cheaper than running both separately. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import TYPE_CHECKING + +from jobflow import Flow, Maker + +from atomate2.vasp.jobs.matpes import MatPesGGAStaticMaker, MatPesMetaGGAStaticMaker + +if TYPE_CHECKING: + from pathlib import Path + + from pymatgen.core import Structure + + +@dataclass +class MatPesGGAPlusMetaGGAStaticMaker(Maker): + """MatPES flow doing a GGA static followed by meta-GGA static. + + Uses the GGA WAVECAR to speed up electronic convergence on the meta-GGA static. + + Parameters + ---------- + name : str + Name of the flows produced by this maker. + static1 : .BaseVaspMaker + Maker to generate the first VASP static. + static2 : .BaseVaspMaker + Maker to generate the second VASP static. + """ + + name: str = "MatPES GGA plus meta-GGA static" + static1: Maker | None = field(default_factory=MatPesGGAStaticMaker) + static2: Maker = field( + default_factory=lambda: MatPesMetaGGAStaticMaker( + # could copy CHGCAR from GGA to meta-GGA directory too but is redundant + # since VASP can reconstruct it from WAVECAR + copy_vasp_kwargs={"additional_vasp_files": ("WAVECAR",)} + ) + ) + + def make(self, structure: Structure, prev_vasp_dir: str | Path | None = None): + """ + Create a flow with two chained statics. + + Parameters + ---------- + structure : .Structure + A pymatgen structure object. + prev_vasp_dir : str or Path or None + A previous VASP calculation directory to copy output files from. + + Returns + ------- + Flow + A flow containing two statics. + """ + static1 = self.static1.make(structure, prev_vasp_dir=prev_vasp_dir) + static2 = self.static2.make(structure, prev_vasp_dir=static1.output.dir_name) + output = {"static1": static1.output, "static2": static2.output} + return Flow([static1, static2], output=output, name=self.name) diff --git a/src/atomate2/vasp/flows/mp.py b/src/atomate2/vasp/flows/mp.py new file mode 100644 index 0000000000..8d82968718 --- /dev/null +++ b/src/atomate2/vasp/flows/mp.py @@ -0,0 +1,200 @@ +""" +Module defining Materials Project workflows. + +Reference: https://doi.org/10.1103/PhysRevMaterials.6.013801 + +In case of questions, consult @Andrew-S-Rosen, @esoteric-ephemera or @janosh. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import TYPE_CHECKING + +from jobflow import Flow, Maker + +from atomate2.vasp.flows.core import DoubleRelaxMaker +from atomate2.vasp.jobs.mp import ( + MPGGARelaxMaker, + MPGGAStaticMaker, + MPMetaGGARelaxMaker, + MPMetaGGAStaticMaker, + MPPreRelaxMaker, +) + +if TYPE_CHECKING: + from pathlib import Path + + from jobflow import Job + from pymatgen.core.structure import Structure + + +@dataclass +class MPGGADoubleRelaxMaker(DoubleRelaxMaker): + """MP GGA double relaxation workflow. + + Parameters + ---------- + name : str + Name of the flows produced by this maker. + relax_maker1 : .BaseVaspMaker + Maker to generate the first relaxation. + relax_maker2 : .BaseVaspMaker + Maker to generate the second relaxation. + """ + + name: str = "MP GGA double relax" + relax_maker1: Maker | None = field(default_factory=MPGGARelaxMaker) + relax_maker2: Maker = field( + default_factory=lambda: MPGGARelaxMaker( + copy_vasp_kwargs={"additional_vasp_files": ("WAVECAR", "CHGCAR")} + ) + ) + + +@dataclass +class MPMetaGGADoubleRelaxMaker(DoubleRelaxMaker): + """MP meta-GGA double relaxation workflow. + + Parameters + ---------- + name : str + Name of the flows produced by this maker. + relax_maker1 : .BaseVaspMaker + Maker to generate the first relaxation. + relax_maker2 : .BaseVaspMaker + Maker to generate the second relaxation. + """ + + name: str = "MP meta-GGA double relax" + relax_maker1: Maker | None = field(default_factory=MPPreRelaxMaker) + relax_maker2: Maker = field( + default_factory=lambda: MPMetaGGARelaxMaker( + copy_vasp_kwargs={"additional_vasp_files": ("WAVECAR", "CHGCAR")} + ) + ) + + +@dataclass +class MPGGADoubleRelaxStaticMaker(Maker): + """ + Maker to perform a VASP GGA relaxation workflow with MP settings. + + Only the middle job performing a PBE relaxation is non-optional. + + Parameters + ---------- + name : str + Name of the flows produced by this maker. + relax_maker : .BaseVaspMaker + Maker to generate the relaxation. + static_maker : .BaseVaspMaker + Maker to generate the static calculation before the relaxation. + """ + + name: str = "MP GGA relax" + relax_maker: Maker = field(default_factory=MPGGADoubleRelaxMaker) + static_maker: Maker | None = field( + default_factory=lambda: MPGGAStaticMaker( + copy_vasp_kwargs={"additional_vasp_files": ("WAVECAR", "CHGCAR")} + ) + ) + + def make( + self, structure: Structure, prev_vasp_dir: str | Path | None = None + ) -> Flow: + """ + 1, 2 or 3-step flow with optional pre-relax and final static jobs. + + Parameters + ---------- + structure : .Structure + A pymatgen structure object. + prev_vasp_dir : str or Path or None + A previous VASP calculation directory to copy output files from. + + Returns + ------- + Flow + A flow containing the MP relaxation workflow. + """ + jobs: list[Job] = [] + + relax_flow = self.relax_maker.make( + structure=structure, prev_vasp_dir=prev_vasp_dir + ) + output = relax_flow.output + jobs += [relax_flow] + + if self.static_maker: + # Run a static calculation + static_job = self.static_maker.make( + structure=output.structure, prev_vasp_dir=output.dir_name + ) + output = static_job.output + jobs += [static_job] + + return Flow(jobs=jobs, output=output, name=self.name) + + +@dataclass +class MPMetaGGADoubleRelaxStaticMaker(MPGGADoubleRelaxMaker): + """ + Flow with optional pre-relax and final static jobs. + + Only the middle job performing a meta-GGA relaxation is non-optional. + + Parameters + ---------- + name : str + Name of the flows produced by this maker. + relax_maker : .BaseVaspMaker + Maker to generate the relaxation. + static_maker : .BaseVaspMaker + Maker to generate the static calculation before the relaxation. + """ + + name: str = "MP meta-GGA relax" + relax_maker: Maker = field(default_factory=MPMetaGGADoubleRelaxMaker) + static_maker: Maker | None = field( + default_factory=lambda: MPMetaGGAStaticMaker( + copy_vasp_kwargs={"additional_vasp_files": ("WAVECAR", "CHGCAR")} + ) + ) + + def make( + self, structure: Structure, prev_vasp_dir: str | Path | None = None + ) -> Flow: + """ + Create a 2-step flow with a cheap pre-relaxation followed by a high-quality one. + + An optional static calculation can be performed before the relaxation. + + Parameters + ---------- + structure : .Structure + A pymatgen structure object. + prev_vasp_dir : str or Path or None + A previous VASP calculation directory to copy output files from. + + Returns + ------- + Flow + A flow containing the MP relaxation workflow. + """ + jobs: list[Job] = [] + + relax_flow = self.relax_maker.make( + structure=structure, prev_vasp_dir=prev_vasp_dir + ) + output = relax_flow.output + jobs += [relax_flow] + if self.static_maker: + # Run a static calculation (typically r2SCAN) + static_job = self.static_maker.make( + structure=output.structure, prev_vasp_dir=output.dir_name + ) + output = static_job.output + jobs += [static_job] + + return Flow(jobs=jobs, output=output, name=self.name) diff --git a/src/atomate2/vasp/flows/phonons.py b/src/atomate2/vasp/flows/phonons.py index 51dc8d7b35..b6e50c55e2 100644 --- a/src/atomate2/vasp/flows/phonons.py +++ b/src/atomate2/vasp/flows/phonons.py @@ -29,9 +29,6 @@ from atomate2.vasp.jobs.base import BaseVaspMaker -__all__ = ["PhononMaker"] - - @dataclass class PhononMaker(Maker): """ @@ -161,7 +158,7 @@ def make( epsilon_static: Matrix3D | None = None, total_dft_energy_per_formula_unit: float | None = None, supercell_matrix: Matrix3D | None = None, - ): + ) -> Flow: """ Make flow to calculate the phonon properties. diff --git a/src/atomate2/vasp/jobs/amset.py b/src/atomate2/vasp/jobs/amset.py index f7386d1f2d..c9b15bac74 100644 --- a/src/atomate2/vasp/jobs/amset.py +++ b/src/atomate2/vasp/jobs/amset.py @@ -28,22 +28,13 @@ ) if TYPE_CHECKING: + from typing import Any + from emmet.core.math import Vector3D from pymatgen.core import Structure from atomate2.vasp.sets.base import VaspInputGenerator -__all__ = [ - "DenseUniformMaker", - "StaticDeformationMaker", - "HSEDenseUniformMaker", - "HSEStaticDeformationMaker", - "run_amset_deformations", - "calculate_deformation_potentials", - "calculate_polar_phonon_frequency", - "generate_wavefunction_coefficients", -] - @dataclass class DenseUniformMaker(NonSCFMaker): @@ -211,7 +202,7 @@ def run_amset_deformations( symprec: float = SETTINGS.SYMPREC, prev_vasp_dir: str | Path | None = None, static_deformation_maker: BaseVaspMaker | None = None, -): +) -> Response: """ Run amset deformations. @@ -271,7 +262,7 @@ def calculate_deformation_potentials( deformation_dirs: list[str], symprec: float = SETTINGS.SYMPREC, ibands: tuple[list[int], list[int]] = None, -): +) -> dict[str, str]: """ Generate the deformation.h5 (containing deformation potentials) using AMSET. @@ -333,7 +324,7 @@ def calculate_polar_phonon_frequency( frequencies: list[float], eigenvectors: list[Vector3D], born_effective_charges: list[Vector3D], -): +) -> dict[str, list[float]]: """ Calculate the polar phonon frequency using amset. @@ -373,7 +364,7 @@ def calculate_polar_phonon_frequency( @job -def generate_wavefunction_coefficients(dir_name: str): +def generate_wavefunction_coefficients(dir_name: str) -> dict[str, Any]: """ Generate wavefunction.h5 file using amset. diff --git a/src/atomate2/vasp/jobs/base.py b/src/atomate2/vasp/jobs/base.py index 1b7f2d6ee6..3122803e62 100644 --- a/src/atomate2/vasp/jobs/base.py +++ b/src/atomate2/vasp/jobs/base.py @@ -27,8 +27,6 @@ if TYPE_CHECKING: from pymatgen.core import Structure -__all__ = ["BaseVaspMaker", "vasp_job"] - _BADER_EXE_EXISTS = bool(which("bader") or which("bader.exe")) _DATA_OBJECTS = [ @@ -109,7 +107,7 @@ ) -def vasp_job(method: Callable): +def vasp_job(method: Callable) -> job: """ Decorate the ``make`` method of VASP job makers. @@ -183,7 +181,9 @@ class BaseVaspMaker(Maker): write_additional_data: dict = field(default_factory=dict) @vasp_job - def make(self, structure: Structure, prev_vasp_dir: str | Path | None = None): + def make( + self, structure: Structure, prev_vasp_dir: str | Path | None = None + ) -> Response: """ Run a VASP calculation. @@ -193,6 +193,11 @@ def make(self, structure: Structure, prev_vasp_dir: str | Path | None = None): A pymatgen structure object. prev_vasp_dir : str or Path or None A previous VASP calculation directory to copy output files from. + + Returns + ------- + Response: A response object containing the output, detours and stop + commands of the VASP run. """ # copy previous inputs from_prev = prev_vasp_dir is not None @@ -237,7 +242,7 @@ def make(self, structure: Structure, prev_vasp_dir: str | Path | None = None): def get_vasp_task_document( path: Path | str, **kwargs, -): +) -> TaskDoc: """Get VASP Task Document using atomate2 settings.""" kwargs.setdefault("store_additional_json", SETTINGS.VASP_STORE_ADDITIONAL_JSON) diff --git a/src/atomate2/vasp/jobs/core.py b/src/atomate2/vasp/jobs/core.py index a75e49d6f2..027b17e918 100644 --- a/src/atomate2/vasp/jobs/core.py +++ b/src/atomate2/vasp/jobs/core.py @@ -35,6 +35,7 @@ if TYPE_CHECKING: from pathlib import Path + from jobflow import Response from pymatgen.core.structure import Structure from atomate2.vasp.sets.base import VaspInputGenerator @@ -42,20 +43,6 @@ logger = logging.getLogger(__name__) -__all__ = [ - "StaticMaker", - "RelaxMaker", - "NonSCFMaker", - "DielectricMaker", - "HSEBSMaker", - "HSERelaxMaker", - "HSEStaticMaker", - "TightRelaxMaker", - "HSETightRelaxMaker", - "TransmuterMaker", - "MDMaker", -] - @dataclass class StaticMaker(BaseVaspMaker): @@ -196,7 +183,7 @@ def make( structure: Structure, prev_vasp_dir: str | Path | None, mode: str = "uniform", - ): + ) -> Response: """ Run a non-scf VASP job. @@ -370,7 +357,7 @@ def make( structure: Structure, prev_vasp_dir: str | Path | None = None, mode="uniform", - ): + ) -> Response: """ Run a HSE06 band structure VASP job. @@ -501,7 +488,7 @@ def make( self, structure: Structure, prev_vasp_dir: str | Path | None = None, - ): + ) -> Response: """ Run a transmuter VASP job. diff --git a/src/atomate2/vasp/jobs/defect.py b/src/atomate2/vasp/jobs/defect.py index f9bc78fe4b..175cedf183 100644 --- a/src/atomate2/vasp/jobs/defect.py +++ b/src/atomate2/vasp/jobs/defect.py @@ -9,7 +9,7 @@ from pymatgen.io.vasp.outputs import WSWQ from atomate2.common.files import copy_files, gunzip_files, gzip_files, rename_files -from atomate2.common.jobs.defect import ( +from atomate2.common.jobs.defect import ( # noqa: F401 bulk_supercell_calculation, get_ccd_documents, get_charged_structures, @@ -25,27 +25,16 @@ logger = logging.getLogger(__name__) -__all__ = [ - "bulk_supercell_calculation", - "calculate_finite_diff", - "get_ccd_documents", - "get_charged_structures", - "get_supercell_from_prv_calc", - "spawn_defect_q_jobs", - "spawn_energy_curve_calcs", -] -# sort the list above - @job(data=WSWQ, output_schema=FiniteDifferenceDocument) def calculate_finite_diff( distorted_calc_dirs: list[str], ref_calc_index: int, run_vasp_kwargs: dict | None = None, -): +) -> FiniteDifferenceDocument: """Run a post-processing VASP job for the finite difference overlap. - Reads the WAVECAR file and computs the desired quantities. This can be used in + Reads the WAVECAR file and computes the desired quantities. This can be used in cases where data from the same calculation is used multiple times. Since all of the standard outputs are presumably already stored in the database, @@ -62,7 +51,7 @@ def calculate_finite_diff( previous calculations). """ ref_calc_dir = distorted_calc_dirs[ref_calc_index] - run_vasp_kwargs = {} if run_vasp_kwargs is None else run_vasp_kwargs + run_vasp_kwargs = run_vasp_kwargs or {} fc = FileClient() copy_vasp_outputs(ref_calc_dir, additional_vasp_files=["WAVECAR"], file_client=fc) diff --git a/src/atomate2/vasp/jobs/elastic.py b/src/atomate2/vasp/jobs/elastic.py index a58491c98e..a83041d5c4 100644 --- a/src/atomate2/vasp/jobs/elastic.py +++ b/src/atomate2/vasp/jobs/elastic.py @@ -6,38 +6,14 @@ from dataclasses import dataclass, field from typing import TYPE_CHECKING -import numpy as np -from jobflow import Flow, Response, job -from pymatgen.alchemy.materials import TransformedStructure -from pymatgen.analysis.elasticity import Deformation, Strain, Stress -from pymatgen.core.tensors import symmetry_reduce -from pymatgen.transformations.standard_transformations import ( - DeformStructureTransformation, -) - -from atomate2 import SETTINGS -from atomate2.common.analysis.elastic import get_default_strain_states -from atomate2.common.schemas.elastic import ElasticDocument from atomate2.vasp.jobs.base import BaseVaspMaker from atomate2.vasp.sets.core import StaticSetGenerator if TYPE_CHECKING: - from pathlib import Path - - from emmet.core.math import Matrix3D - from pymatgen.core.structure import Structure - from atomate2.vasp.sets.base import VaspInputGenerator logger = logging.getLogger(__name__) -__all__ = [ - "ElasticRelaxMaker", - "generate_elastic_deformations", - "run_elastic_deformations", - "fit_elastic_tensor", -] - @dataclass class ElasticRelaxMaker(BaseVaspMaker): @@ -90,202 +66,3 @@ class ElasticRelaxMaker(BaseVaspMaker): }, ) ) - - -@job -def generate_elastic_deformations( - structure: Structure, - order: int = 2, - strain_states: list[tuple[int, int, int, int, int, int]] | None = None, - strain_magnitudes: list[float] | list[list[float]] | None = None, - symprec: float = SETTINGS.SYMPREC, - sym_reduce: bool = True, -): - """ - Generate elastic deformations. - - Parameters - ---------- - structure : Structure - A pymatgen structure object. - order : int - Order of the tensor expansion to be determined. Can be either 2 or 3. - strain_states : None or list of tuple of int - List of Voigt-notation strains, e.g. ``[(1, 0, 0, 0, 0, 0), (0, 1, 0, 0, 0, 0), - etc]``. - strain_magnitudes : None or list of float or list of list of float - A list of strain magnitudes to multiply by for each strain state, e.g. ``[-0.01, - -0.005, 0.005, 0.01]``. Alternatively, a list of lists can be specified, where - each inner list corresponds to a specific strain state. - - symprec : float - Symmetry precision. - sym_reduce : bool - Whether to reduce the number of deformations using symmetry. - - Returns - ------- - List[Deformation] - A list of deformations. - """ - if strain_states is None: - strain_states = get_default_strain_states(order) - - if strain_magnitudes is None: - strain_magnitudes = np.linspace(-0.01, 0.01, 5 + (order - 2) * 2) - - if np.array(strain_magnitudes).ndim == 1: - strain_magnitudes = [strain_magnitudes] * len(strain_states) # type: ignore - - strains = [] - for state, magnitudes in zip(strain_states, strain_magnitudes): - strains.extend( - [Strain.from_voigt(m * np.array(state)) for m in magnitudes] # type: ignore - ) - - # remove zero strains - strains = [strain for strain in strains if (abs(strain) > 1e-10).any()] - - if np.linalg.matrix_rank([strain.voigt for strain in strains]) < 6: - # TODO: check for sufficiency of input for nth order - raise ValueError("strain list is insufficient to fit an elastic tensor") - - if sym_reduce: - strain_mapping = symmetry_reduce(strains, structure, symprec=symprec) - logger.info( - f"Using symmetry to reduce number of strains from {len(strains)} to " - f"{len(list(strain_mapping.keys()))}" - ) - strains = list(strain_mapping.keys()) - - return [s.get_deformation_matrix() for s in strains] - - -@job -def run_elastic_deformations( - structure: Structure, - deformations: list[Deformation], - prev_vasp_dir: str | Path | None = None, - elastic_relax_maker: BaseVaspMaker = None, -): - """ - Run elastic deformations. - - Note, this job will replace itself with N relaxation calculations, where N is - the number of deformations. - - Parameters - ---------- - structure : Structure - A pymatgen structure. - deformations : list of Deformation - The deformations to apply. - prev_vasp_dir : str or Path or None - A previous VASP directory to use for copying VASP outputs. - elastic_relax_maker : .BaseVaspMaker - A VaspMaker to use to generate the elastic relaxation jobs. - """ - if elastic_relax_maker is None: - elastic_relax_maker = ElasticRelaxMaker() - - relaxations = [] - outputs = [] - for i, deformation in enumerate(deformations): - # deform the structure - dst = DeformStructureTransformation(deformation=deformation) - ts = TransformedStructure(structure, transformations=[dst]) - deformed_structure = ts.final_structure - - # write details of the transformation to the transformations.json file - # this file will automatically get added to the task document and allow - # the elastic builder to reconstruct the elastic document; note the ":" is - # automatically converted to a "." in the filename. - elastic_relax_maker.write_additional_data["transformations:json"] = ts - - # create the job - relax_job = elastic_relax_maker.make( - deformed_structure, prev_vasp_dir=prev_vasp_dir - ) - relax_job.append_name(f" {i + 1}/{len(deformations)}") - relaxations.append(relax_job) - - # extract the outputs we want - output = { - "stress": relax_job.output.output.stress, - "deformation": deformation, - "uuid": relax_job.output.uuid, - "job_dir": relax_job.output.dir_name, - } - - outputs.append(output) - - relax_flow = Flow(relaxations, outputs) - return Response(replace=relax_flow) - - -@job(output_schema=ElasticDocument) -def fit_elastic_tensor( - structure: Structure, - deformation_data: list[dict], - equilibrium_stress: Matrix3D | None = None, - order: int = 2, - fitting_method: str = SETTINGS.ELASTIC_FITTING_METHOD, - symprec: float = SETTINGS.SYMPREC, - allow_elastically_unstable_structs: bool = True, -): - """ - Analyze stress/strain data to fit the elastic tensor and related properties. - - Parameters - ---------- - structure : ~pymatgen.core.structure.Structure - A pymatgen structure. - deformation_data : list of dict - The deformation data, as a list of dictionaries, each containing the keys - "stress", "deformation". - equilibrium_stress : None or tuple of tuple of float - The equilibrium stress of the (relaxed) structure, if known. - order : int - Order of the tensor expansion to be fitted. Can be either 2 or 3. - fitting_method : str - The method used to fit the elastic tensor. See pymatgen for more details on the - methods themselves. The options are: - - - "finite_difference" (note this is required if fitting a 3rd order tensor) - - "independent" - - "pseudoinverse" - symprec : float - Symmetry precision for deriving symmetry equivalent deformations. If - ``symprec=None``, then no symmetry operations will be applied. - allow_elastically_unstable_structs : bool - Whether to allow the ElasticDocument to still complete in the event that - the structure is elastically unstable. - """ - stresses = [] - deformations = [] - uuids = [] - job_dirs = [] - for data in deformation_data: - # stress could be none if the deformation calculation failed - if data["stress"] is None: - continue - - stresses.append(Stress(data["stress"])) - deformations.append(Deformation(data["deformation"])) - uuids.append(data["uuid"]) - job_dirs.append(data["job_dir"]) - - logger.info("Analyzing stress/strain data") - - return ElasticDocument.from_stresses( - structure, - stresses, - deformations, - uuids, - job_dirs, - fitting_method=fitting_method, - order=order, - equilibrium_stress=equilibrium_stress, - symprec=symprec, - allow_elastically_unstable_structs=allow_elastically_unstable_structs, - ) diff --git a/src/atomate2/vasp/jobs/elph.py b/src/atomate2/vasp/jobs/elph.py index 104e960a28..a774f222d3 100644 --- a/src/atomate2/vasp/jobs/elph.py +++ b/src/atomate2/vasp/jobs/elph.py @@ -20,14 +20,6 @@ from pymatgen.core import Structure from pymatgen.electronic_structure.bandstructure import BandStructure -__all__ = [ - "DEFAULT_ELPH_TEMPERATURES", - "DEFAULT_MIN_SUPERCELL_LENGTH", - "SupercellElectronPhononDisplacedStructureMaker", - "run_elph_displacements", - "calculate_electron_phonon_renormalisation", -] - DEFAULT_ELPH_TEMPERATURES = (0, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000) DEFAULT_MIN_SUPERCELL_LENGTH = 15 @@ -98,7 +90,7 @@ def make( self, structure: Structure, prev_vasp_dir: str | Path | None = None, - ): + ) -> Response: """ Run a transmuter VASP job. @@ -129,7 +121,7 @@ def run_elph_displacements( prev_vasp_dir: str | Path | None = None, original_structure: Structure = None, supercell_structure: Structure = None, -): +) -> Response: """ Run electron phonon displaced structures. @@ -208,7 +200,7 @@ def calculate_electron_phonon_renormalisation( elph_uuid: str, elph_dir: str, original_structure: Structure, -): +) -> ElectronPhononRenormalisationDoc: """ Calculate the electron-phonon renormalisation of the band gap. diff --git a/src/atomate2/vasp/jobs/lobster.py b/src/atomate2/vasp/jobs/lobster.py index 3271370e7a..1a730874aa 100644 --- a/src/atomate2/vasp/jobs/lobster.py +++ b/src/atomate2/vasp/jobs/lobster.py @@ -24,13 +24,6 @@ from atomate2.vasp.sets.base import VaspInputGenerator -__all__ = [ - "LobsterStaticMaker", - "get_basis_infos", - "get_lobster_jobs", - "delete_lobster_wavecar", -] - logger = logging.getLogger(__name__) @@ -88,7 +81,7 @@ def get_basis_infos( vasp_maker: BaseVaspMaker, address_max_basis: str = None, address_min_basis: str = None, -): +) -> dict: """ Compute all relevant basis sets and maximum number of bands. @@ -143,7 +136,7 @@ def update_user_incar_settings_maker( nbands: int, structure: Structure, prev_vasp_dir: Path | str, -): +) -> Response: """ Update the INCAR settings of a maker. @@ -177,7 +170,7 @@ def get_lobster_jobs( optimization_uuid: str, static_dir: Path | str, static_uuid: str, -): +) -> Response: """ Create a list of Lobster jobs with different basis sets. @@ -231,7 +224,7 @@ def get_lobster_jobs( def delete_lobster_wavecar( dirs: list[Path | str], lobster_static_dir: Path | str = None, -): +) -> None: """ Delete all WAVECARs. diff --git a/src/atomate2/vasp/jobs/matpes.py b/src/atomate2/vasp/jobs/matpes.py new file mode 100644 index 0000000000..0031c675ac --- /dev/null +++ b/src/atomate2/vasp/jobs/matpes.py @@ -0,0 +1,91 @@ +""" +Module defining MatPES job makers. + +In case of questions, contact @janosh or @shyuep. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import TYPE_CHECKING + +from atomate2.vasp.jobs.base import BaseVaspMaker +from atomate2.vasp.sets.matpes import ( + MatPesGGAStaticSetGenerator, + MatPesMetaGGAStaticSetGenerator, +) + +if TYPE_CHECKING: + from atomate2.vasp.sets.base import VaspInputGenerator + + +@dataclass +class MatPesGGAStaticMaker(BaseVaspMaker): + """ + Maker to create VASP static job using r2SCAN by default. + + Parameters + ---------- + name : str + The job name. + input_set_generator : .VaspInputGenerator + A generator used to make the input set. + write_input_set_kwargs : dict + Keyword arguments that will get passed to :obj:`.write_vasp_input_set`. + copy_vasp_kwargs : dict + Keyword arguments that will get passed to :obj:`.copy_vasp_outputs`. + run_vasp_kwargs : dict + Keyword arguments that will get passed to :obj:`.run_vasp`. + task_document_kwargs : dict + Keyword arguments that will get passed to :obj:`.TaskDoc.from_directory`. + stop_children_kwargs : dict + Keyword arguments that will get passed to :obj:`.should_stop_children`. + write_additional_data : dict + Additional data to write to the current directory. Given as a dict of + {filename: data}. Note that if using FireWorks, dictionary keys cannot contain + the "." character which is typically used to denote file extensions. To avoid + this, use the ":" character, which will automatically be converted to ".". E.g. + ``{"my_file:txt": "contents of the file"}``. + """ + + name: str = "MatPES GGA static" + input_set_generator: VaspInputGenerator = field( + default_factory=MatPesGGAStaticSetGenerator + ) + inherit_incar: bool = False + + +@dataclass +class MatPesMetaGGAStaticMaker(BaseVaspMaker): + """ + Maker to create VASP static job using r2SCAN by default. + + Parameters + ---------- + name : str + The job name. + input_set_generator : .VaspInputGenerator + A generator used to make the input set. + write_input_set_kwargs : dict + Keyword arguments that will get passed to :obj:`.write_vasp_input_set`. + copy_vasp_kwargs : dict + Keyword arguments that will get passed to :obj:`.copy_vasp_outputs`. + run_vasp_kwargs : dict + Keyword arguments that will get passed to :obj:`.run_vasp`. + task_document_kwargs : dict + Keyword arguments that will get passed to :obj:`.TaskDoc.from_directory`. + stop_children_kwargs : dict + Keyword arguments that will get passed to :obj:`.should_stop_children`. + write_additional_data : dict + Additional data to write to the current directory. Given as a dict of + {filename: data}. Note that if using FireWorks, dictionary keys cannot contain + the "." character which is typically used to denote file extensions. To avoid + this, use the ":" character, which will automatically be converted to ".". E.g. + ``{"my_file:txt": "contents of the file"}``. + """ + + name: str = "MatPES meta-GGA static" + input_set_generator: VaspInputGenerator = field( + default_factory=MatPesMetaGGAStaticSetGenerator + ) + inherit_incar: bool = False diff --git a/src/atomate2/vasp/jobs/mp.py b/src/atomate2/vasp/jobs/mp.py new file mode 100644 index 0000000000..44e73f1860 --- /dev/null +++ b/src/atomate2/vasp/jobs/mp.py @@ -0,0 +1,206 @@ +""" +Module defining Materials Project job makers. + +Reference: https://doi.org/10.1103/PhysRevMaterials.6.013801 + +In case of questions, consult @Andrew-S-Rosen, @esoteric-ephemera or @janosh. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import TYPE_CHECKING + +from atomate2.vasp.jobs.base import BaseVaspMaker +from atomate2.vasp.sets.mp import ( + MPGGARelaxSetGenerator, + MPGGAStaticSetGenerator, + MPMetaGGARelaxSetGenerator, + MPMetaGGAStaticSetGenerator, +) + +if TYPE_CHECKING: + from atomate2.vasp.sets.base import VaspInputGenerator + + +@dataclass +class MPGGARelaxMaker(BaseVaspMaker): + """ + Maker to create VASP relaxation job using PBE GGA by default. + + Parameters + ---------- + name : str + The job name. + input_set_generator : .VaspInputGenerator + A generator used to make the input set. + write_input_set_kwargs : dict + Keyword arguments that will get passed to :obj:`.write_vasp_input_set`. + copy_vasp_kwargs : dict + Keyword arguments that will get passed to :obj:`.copy_vasp_outputs`. + run_vasp_kwargs : dict + Keyword arguments that will get passed to :obj:`.run_vasp`. + task_document_kwargs : dict + Keyword arguments that will get passed to :obj:`.TaskDoc.from_directory`. + stop_children_kwargs : dict + Keyword arguments that will get passed to :obj:`.should_stop_children`. + write_additional_data : dict + Additional data to write to the current directory. Given as a dict of + {filename: data}. Note that if using FireWorks, dictionary keys cannot contain + the "." character which is typically used to denote file extensions. To avoid + this, use the ":" character, which will automatically be converted to ".". E.g. + ``{"my_file:txt": "contents of the file"}``. + """ + + name: str = "MP GGA relax" + input_set_generator: VaspInputGenerator = field( + default_factory=MPGGARelaxSetGenerator + ) + + +@dataclass +class MPGGAStaticMaker(BaseVaspMaker): + """ + Maker to create VASP static job using PBE GGA by default. + + Parameters + ---------- + name : str + The job name. + input_set_generator : .VaspInputGenerator + A generator used to make the input set. + write_input_set_kwargs : dict + Keyword arguments that will get passed to :obj:`.write_vasp_input_set`. + copy_vasp_kwargs : dict + Keyword arguments that will get passed to :obj:`.copy_vasp_outputs`. + run_vasp_kwargs : dict + Keyword arguments that will get passed to :obj:`.run_vasp`. + task_document_kwargs : dict + Keyword arguments that will get passed to :obj:`.TaskDoc.from_directory`. + stop_children_kwargs : dict + Keyword arguments that will get passed to :obj:`.should_stop_children`. + write_additional_data : dict + Additional data to write to the current directory. Given as a dict of + {filename: data}. Note that if using FireWorks, dictionary keys cannot contain + the "." character which is typically used to denote file extensions. To avoid + this, use the ":" character, which will automatically be converted to ".". E.g. + ``{"my_file:txt": "contents of the file"}``. + """ + + name: str = "MP GGA static" + input_set_generator: VaspInputGenerator = field( + default_factory=MPGGAStaticSetGenerator + ) + + +@dataclass +class MPPreRelaxMaker(BaseVaspMaker): + """ + Maker to create VASP pre-relaxation job using PBEsol by default. + + Parameters + ---------- + name : str + The job name. + input_set_generator : .VaspInputGenerator + A generator used to make the input set. + write_input_set_kwargs : dict + Keyword arguments that will get passed to :obj:`.write_vasp_input_set`. + copy_vasp_kwargs : dict + Keyword arguments that will get passed to :obj:`.copy_vasp_outputs`. + run_vasp_kwargs : dict + Keyword arguments that will get passed to :obj:`.run_vasp`. + task_document_kwargs : dict + Keyword arguments that will get passed to :obj:`.TaskDoc.from_directory`. + stop_children_kwargs : dict + Keyword arguments that will get passed to :obj:`.should_stop_children`. + write_additional_data : dict + Additional data to write to the current directory. Given as a dict of + {filename: data}. Note that if using FireWorks, dictionary keys cannot contain + the "." character which is typically used to denote file extensions. To avoid + this, use the ":" character, which will automatically be converted to ".". E.g. + ``{"my_file:txt": "contents of the file"}``. + """ + + name: str = "MP pre-relax" + input_set_generator: VaspInputGenerator = field( + default_factory=lambda: MPMetaGGARelaxSetGenerator( + user_incar_settings={ + "EDIFFG": -0.05, + "GGA": "PS", + "LWAVE": True, + "LCHARG": True, + "METAGGA": None, + }, + ) + ) + + +@dataclass +class MPMetaGGARelaxMaker(BaseVaspMaker): + """ + Maker to create VASP relaxation job using r2SCAN by default. + + Parameters + ---------- + name : str + The job name. + input_set_generator : .VaspInputGenerator + A generator used to make the input set. + write_input_set_kwargs : dict + Keyword arguments that will get passed to :obj:`.write_vasp_input_set`. + copy_vasp_kwargs : dict + Keyword arguments that will get passed to :obj:`.copy_vasp_outputs`. + run_vasp_kwargs : dict + Keyword arguments that will get passed to :obj:`.run_vasp`. + task_document_kwargs : dict + Keyword arguments that will get passed to :obj:`.TaskDoc.from_directory`. + stop_children_kwargs : dict + Keyword arguments that will get passed to :obj:`.should_stop_children`. + write_additional_data : dict + Additional data to write to the current directory. Given as a dict of + {filename: data}. Note that if using FireWorks, dictionary keys cannot contain + the "." character which is typically used to denote file extensions. To avoid + this, use the ":" character, which will automatically be converted to ".". E.g. + ``{"my_file:txt": "contents of the file"}``. + """ + + name: str = "MP meta-GGA relax" + input_set_generator: VaspInputGenerator = field( + default_factory=MPMetaGGARelaxSetGenerator + ) + + +@dataclass +class MPMetaGGAStaticMaker(BaseVaspMaker): + """ + Maker to create VASP static job using r2SCAN by default. + + Parameters + ---------- + name : str + The job name. + input_set_generator : .VaspInputGenerator + A generator used to make the input set. + write_input_set_kwargs : dict + Keyword arguments that will get passed to :obj:`.write_vasp_input_set`. + copy_vasp_kwargs : dict + Keyword arguments that will get passed to :obj:`.copy_vasp_outputs`. + run_vasp_kwargs : dict + Keyword arguments that will get passed to :obj:`.run_vasp`. + task_document_kwargs : dict + Keyword arguments that will get passed to :obj:`.TaskDoc.from_directory`. + stop_children_kwargs : dict + Keyword arguments that will get passed to :obj:`.should_stop_children`. + write_additional_data : dict + Additional data to write to the current directory. Given as a dict of + {filename: data}. Note that if using FireWorks, dictionary keys cannot contain + the "." character which is typically used to denote file extensions. To avoid + this, use the ":" character, which will automatically be converted to ".". E.g. + ``{"my_file:txt": "contents of the file"}``. + """ + + name: str = "MP meta-GGA static" + input_set_generator: VaspInputGenerator = field( + default_factory=MPMetaGGAStaticSetGenerator + ) diff --git a/src/atomate2/vasp/powerups.py b/src/atomate2/vasp/powerups.py index e700c7cf72..a050654628 100644 --- a/src/atomate2/vasp/powerups.py +++ b/src/atomate2/vasp/powerups.py @@ -11,17 +11,14 @@ from atomate2.vasp.jobs.base import BaseVaspMaker -def update_user_incar_settings( +def update_vasp_input_generators( flow: Job | Flow | Maker, - incar_updates: dict[str, Any], + dict_mod_updates: dict[str, Any], name_filter: str | None = None, class_filter: type[Maker] | None = BaseVaspMaker, ) -> Job | Flow | Maker: """ - Update the user_incar_settings of any VaspInputGenerators in the flow. - - Alternatively, if a Maker is supplied, the user_incar_settings of the maker will - be updated. + Update any VaspInputGenerators or Makers in the flow. Note, this returns a copy of the original Job/Flow/Maker. I.e., the update does not happen in place. @@ -30,9 +27,9 @@ def update_user_incar_settings( ---------- flow : .Job or .Flow or .Maker A job, flow or Maker. - incar_updates : dict - The updates to apply. Existing keys in user_incar_settings will not be modified - unless explicitly specified in ``incar_updates``. + dict_mod_updates : dict + The updates to apply. Existing keys will not be modified unless explicitly + specified in ``dict_mod_updates``. name_filter : str or None A filter for the name of the jobs. class_filter : Maker or None @@ -44,10 +41,6 @@ def update_user_incar_settings( Job or Flow or Maker A copy of the input flow/job/maker modified to use the updated incar settings. """ - dict_mod_updates = { - f"input_set_generator->user_incar_settings->{k}": v - for k, v in incar_updates.items() - } updated_flow = deepcopy(flow) if isinstance(updated_flow, Maker): updated_flow = updated_flow.update_kwargs( @@ -66,6 +59,50 @@ def update_user_incar_settings( return updated_flow +def update_user_incar_settings( + flow: Job | Flow | Maker, + incar_updates: dict[str, Any], + name_filter: str | None = None, + class_filter: type[Maker] | None = BaseVaspMaker, +) -> Job | Flow | Maker: + """ + Update the user_incar_settings of any VaspInputGenerators in the flow. + + Alternatively, if a Maker is supplied, the user_incar_settings of the maker will + be updated. + + Note, this returns a copy of the original Job/Flow/Maker. I.e., the update does not + happen in place. + + Parameters + ---------- + flow : .Job or .Flow or .Maker + A job, flow or Maker. + incar_updates : dict + The updates to apply. Existing keys in user_incar_settings will not be modified + unless explicitly specified in ``incar_updates``. + name_filter : str or None + A filter for the name of the jobs. + class_filter : Maker or None + A filter for the VaspMaker class used to generate the flows. Note the class + filter will match any subclasses. + + Returns + ------- + Job or Flow or Maker + A copy of the input flow/job/maker modified to use the updated incar settings. + """ + return update_vasp_input_generators( + flow=flow, + dict_mod_updates={ + f"input_set_generator->user_incar_settings->{k}": v + for k, v in incar_updates.items() + }, + name_filter=name_filter, + class_filter=class_filter, + ) + + def update_user_potcar_settings( flow: Job | Flow | Maker, potcar_updates: dict[str, Any], @@ -99,26 +136,15 @@ def update_user_potcar_settings( Job or Flow or Maker A copy of the input flow/job/maker modified to use the updated potcar settings. """ - dict_mod_updates = { - f"input_set_generator->user_potcar_settings->{k}": v - for k, v in potcar_updates.items() - } - updated_flow = deepcopy(flow) - if isinstance(updated_flow, Maker): - updated_flow = updated_flow.update_kwargs( - {"_set": dict_mod_updates}, - name_filter=name_filter, - class_filter=class_filter, - dict_mod=True, - ) - else: - updated_flow.update_maker_kwargs( - {"_set": dict_mod_updates}, - name_filter=name_filter, - class_filter=class_filter, - dict_mod=True, - ) - return updated_flow + return update_vasp_input_generators( + flow=flow, + dict_mod_updates={ + f"input_set_generator->user_potcar_settings->{k}": v + for k, v in potcar_updates.items() + }, + name_filter=name_filter, + class_filter=class_filter, + ) def update_user_potcar_functional( @@ -153,25 +179,14 @@ def update_user_potcar_functional( Job or Flow or Maker A copy of the input flow/job/maker modified to use the updated potcar settings. """ - dict_mod_updates = { - "input_set_generator->user_potcar_functional": potcar_functional - } - updated_flow = deepcopy(flow) - if isinstance(updated_flow, Maker): - updated_flow = updated_flow.update_kwargs( - {"_set": dict_mod_updates}, - name_filter=name_filter, - class_filter=class_filter, - dict_mod=True, - ) - else: - updated_flow.update_maker_kwargs( - {"_set": dict_mod_updates}, - name_filter=name_filter, - class_filter=class_filter, - dict_mod=True, - ) - return updated_flow + return update_vasp_input_generators( + flow=flow, + dict_mod_updates={ + "input_set_generator->user_potcar_functional": potcar_functional + }, + name_filter=name_filter, + class_filter=class_filter, + ) def update_user_kpoints_settings( @@ -217,23 +232,12 @@ def update_user_kpoints_settings( f"input_set_generator->user_kpoints_settings->{k}": v for k, v in kpoints_updates.items() } - - updated_flow = deepcopy(flow) - if isinstance(updated_flow, Maker): - updated_flow = updated_flow.update_kwargs( - {"_set": dict_mod_updates}, - name_filter=name_filter, - class_filter=class_filter, - dict_mod=True, - ) - else: - updated_flow.update_maker_kwargs( - {"_set": dict_mod_updates}, - name_filter=name_filter, - class_filter=class_filter, - dict_mod=True, - ) - return updated_flow + return update_vasp_input_generators( + flow=flow, + dict_mod_updates=dict_mod_updates, + name_filter=name_filter, + class_filter=class_filter, + ) def use_auto_ispin( @@ -267,21 +271,9 @@ def use_auto_ispin( Job or Flow or Maker A copy of the input flow/job/maker but with auto_ispin set. """ - dict_mod_updates = {"input_set_generator->auto_ispin": value} - - updated_flow = deepcopy(flow) - if isinstance(updated_flow, Maker): - updated_flow = updated_flow.update_kwargs( - {"_set": dict_mod_updates}, - name_filter=name_filter, - class_filter=class_filter, - dict_mod=True, - ) - else: - updated_flow.update_maker_kwargs( - {"_set": dict_mod_updates}, - name_filter=name_filter, - class_filter=class_filter, - dict_mod=True, - ) - return updated_flow + return update_vasp_input_generators( + flow=flow, + dict_mod_updates={"input_set_generator->auto_ispin": value}, + name_filter=name_filter, + class_filter=class_filter, + ) diff --git a/src/atomate2/vasp/run.py b/src/atomate2/vasp/run.py index ac04b76044..0553980700 100644 --- a/src/atomate2/vasp/run.py +++ b/src/atomate2/vasp/run.py @@ -12,7 +12,7 @@ import shlex import subprocess from os.path import expandvars -from typing import TYPE_CHECKING, Any, Sequence +from typing import TYPE_CHECKING, Any from custodian import Custodian from custodian.vasp.handlers import ( @@ -35,16 +35,12 @@ from atomate2 import SETTINGS if TYPE_CHECKING: + from collections.abc import Sequence + from custodian.custodian import ErrorHandler, Validator from emmet.core.tasks import TaskDoc -__all__ = [ - "JobType", - "run_vasp", - "should_stop_children", -] - _DEFAULT_HANDLERS = ( VaspErrorHandler(), MeshSymmetryErrorHandler(), @@ -70,7 +66,7 @@ class JobType(ValueEnum): - ``NORMAL``: Normal custodian :obj:`.VaspJob`. - ``DOUBLE_RELAXATION``: Custodian double relaxation run from :obj:`.VaspJob.double_relaxation_run`. - - ``METAGGA_OPT``: Custodian metagga optimization run from + - ``METAGGA_OPT``: Custodian meta-GGA optimization run from :obj:`.VaspJob.metagga_opt_run`. - ``FULL_OPT``: Custodian full optimization run from :obj:`.VaspJob.full_opt_run`. @@ -94,7 +90,7 @@ def run_vasp( wall_time: int | None = None, vasp_job_kwargs: dict[str, Any] = None, custodian_kwargs: dict[str, Any] = None, -): +) -> None: """ Run VASP. @@ -124,8 +120,8 @@ def run_vasp( custodian_kwargs : dict Keyword arguments that are passed to :obj:`.Custodian`. """ - vasp_job_kwargs = {} if vasp_job_kwargs is None else vasp_job_kwargs - custodian_kwargs = {} if custodian_kwargs is None else custodian_kwargs + vasp_job_kwargs = vasp_job_kwargs or {} + custodian_kwargs = custodian_kwargs or {} vasp_cmd = expandvars(vasp_cmd) vasp_gamma_cmd = expandvars(vasp_gamma_cmd) @@ -134,7 +130,7 @@ def run_vasp( vasp_job_kwargs.setdefault("auto_npar", False) - vasp_job_kwargs.update({"gamma_vasp_cmd": split_vasp_gamma_cmd}) + vasp_job_kwargs.update(gamma_vasp_cmd=split_vasp_gamma_cmd) if job_type == JobType.DIRECT: logger.info(f"Running command: {vasp_cmd}") @@ -151,12 +147,12 @@ def run_vasp( elif job_type == JobType.FULL_OPT: jobs = VaspJob.full_opt_run(split_vasp_cmd, **vasp_job_kwargs) else: - raise ValueError(f"Unsupported job type: {job_type}") + raise ValueError(f"Unsupported {job_type=}") if wall_time is not None: handlers = [*handlers, WalltimeHandler(wall_time=wall_time)] - c = Custodian( + custodian_manager = Custodian( handlers, jobs, validators=validators, @@ -166,7 +162,7 @@ def run_vasp( ) logger.info("Running VASP using custodian.") - c.run() + custodian_manager.run() def should_stop_children( @@ -205,4 +201,4 @@ def should_stop_children( "limit of electronic/ionic iterations)!" ) - raise RuntimeError(f"Unknown option for defuse_unsuccessful: {handle_unsuccessful}") + raise RuntimeError(f"Unknown option for {handle_unsuccessful=}") diff --git a/src/atomate2/vasp/schemas/defect.py b/src/atomate2/vasp/schemas/defect.py index ef9b9fc91e..07cb590402 100644 --- a/src/atomate2/vasp/schemas/defect.py +++ b/src/atomate2/vasp/schemas/defect.py @@ -2,15 +2,13 @@ import logging from pathlib import Path -from typing import List, Optional, Union +from typing import Optional, Union from pydantic import BaseModel, Field from pymatgen.io.vasp.outputs import WSWQ logger = logging.getLogger(__name__) -__all__ = ["FiniteDifferenceDocument"] - class FiniteDifferenceDocument(BaseModel): """Collection of computed wavefunction overlap objects. @@ -19,7 +17,7 @@ class FiniteDifferenceDocument(BaseModel): from distorted structures. """ - wswqs: List[WSWQ] + wswqs: list[WSWQ] dir_name: str = Field( None, description="Directory where the WSWQ calculations are performed" @@ -27,7 +25,7 @@ class FiniteDifferenceDocument(BaseModel): ref_dir: str = Field( None, description="Directory where the reference W(0) wavefunction comes from" ) - distorted_dirs: List[str] = Field( + distorted_dirs: list[str] = Field( None, description="Directories where the distorted W(Q) wavefunctions come from", ) @@ -37,7 +35,7 @@ def from_directory( cls, directory: Union[str, Path], ref_dir: Optional[Union[str, Path]] = None, - distorted_dirs: Optional[List[str]] = None, + distorted_dirs: Optional[list[str]] = None, ) -> "FiniteDifferenceDocument": """ Read the FiniteDiff file. diff --git a/src/atomate2/vasp/schemas/elph.py b/src/atomate2/vasp/schemas/elph.py index f4ba9340ac..3b3751bc6d 100644 --- a/src/atomate2/vasp/schemas/elph.py +++ b/src/atomate2/vasp/schemas/elph.py @@ -1,7 +1,6 @@ """Schemas for electron-phonon renormalisation documents.""" import logging -from typing import Dict, List, Tuple import numpy as np from emmet.core.structure import StructureMetadata @@ -12,27 +11,25 @@ logger = logging.getLogger(__name__) -__all__ = ["RawElectronicData", "ElectronPhononRenormalisationDoc"] - class RawElectronicData(BaseModel): """Raw data used to fit electron-phonon renormalisation.""" - displacement_uuids: List[str] = Field( + displacement_uuids: list[str] = Field( None, description="UUIDs of the displacement band structure calculations" ) - displacement_dirs: List[str] = Field( + displacement_dirs: list[str] = Field( None, description="Directories of the displacement band structure calculations" ) - displacement_structures: List[Structure] = Field( + displacement_structures: list[Structure] = Field( None, description="The electron-phonon displaced structures at each temperature" ) - displacement_cbms: List[List[float]] = Field( + displacement_cbms: list[list[float]] = Field( None, description="Conduction band minima of the displaced structures, as an" "array with the shape (ntemps, ncbms)", ) - displacement_vbms: List[List[float]] = Field( + displacement_vbms: list[list[float]] = Field( None, description="Valence band maxima of the displaced structures, as an" "array with the shape (ntemps, nvbms)", @@ -50,12 +47,12 @@ class RawElectronicData(BaseModel): bulk_vbm: float = Field( None, description="Valence band maximum of the bulk (supercell) structure" ) - bulk_vbm_band_indices: Dict[str, List[int]] = Field( + bulk_vbm_band_indices: dict[str, list[int]] = Field( None, description="Indices of bands that are degenerate at the valence band " "maximum (zero indexed) in the bulk (supercell) structure", ) - bulk_cbm_band_indices: Dict[str, List[int]] = Field( + bulk_cbm_band_indices: dict[str, list[int]] = Field( None, description="Indices of bands that are degenerate at the conduction band " "minimum (zero indexed) in the bulk (supercell) structure", @@ -80,19 +77,19 @@ class ElectronPhononRenormalisationDoc(StructureMetadata): description="The primitive structure for which the electron-phonon was" " calculated", ) - temperatures: List[float] = Field( + temperatures: list[float] = Field( None, description="Temperatures at which electron-phonon coupling was obtained" ) - band_gaps: List[float] = Field( + band_gaps: list[float] = Field( None, description="Temperature renormalised band gaps" ) - vbms: List[float] = Field( + vbms: list[float] = Field( None, description="Temperature renormalised valence band maxima" ) - cbms: List[float] = Field( + cbms: list[float] = Field( None, description="Temperature renormalised conduction band minima" ) - delta_band_gaps: List[float] = Field( + delta_band_gaps: list[float] = Field( None, description="Change in band gap relative to the bulk structure" ) bulk_band_gap: float = Field( @@ -107,11 +104,11 @@ class ElectronPhononRenormalisationDoc(StructureMetadata): @classmethod def from_band_structures( cls, - temperatures: List[float], - displacement_band_structures: List[BandStructure], - displacement_structures: List[Structure], - displacement_uuids: List[str], - displacement_dirs: List[str], + temperatures: list[float], + displacement_band_structures: list[BandStructure], + displacement_structures: list[Structure], + displacement_uuids: list[str], + displacement_dirs: list[str], bulk_band_structure: BandStructure, bulk_structure: Structure, bulk_uuid: str, @@ -119,7 +116,7 @@ def from_band_structures( elph_uuid: str, elph_dir: str, original_structure: Structure, - ): + ) -> "ElectronPhononRenormalisationDoc": """ Calculate an electron-phonon renormalisation document from band structures. @@ -240,10 +237,10 @@ def from_band_structures( def _get_displacement_band_edges( - band_structures: List[BandStructure], - band_indices: Dict[Spin, List[int]], + band_structures: list[BandStructure], + band_indices: dict[Spin, list[int]], cbm: bool = True, -): +) -> np.ndarray: """Extract band edge energies based on a band structure and band indices.""" band_edges = [] for band_structure in band_structures: @@ -262,7 +259,7 @@ def _get_displacement_band_edges( def _get_band_edge_indices( band_structure: BandStructure, tol: float = 0.005, -) -> Tuple[Dict[Spin, List[int]], Dict[Spin, List[int]]]: +) -> tuple[dict[Spin, list[int]], dict[Spin, list[int]]]: """ Get indices of degenerate band edge states, within a tolerance. diff --git a/src/atomate2/vasp/sets/BaseMPGGASet.yaml b/src/atomate2/vasp/sets/BaseMPGGASet.yaml index 8a810320b0..5d94e66427 100644 --- a/src/atomate2/vasp/sets/BaseMPGGASet.yaml +++ b/src/atomate2/vasp/sets/BaseMPGGASet.yaml @@ -199,8 +199,9 @@ POTCAR: W: W_pv Xe: Xe Y: Y_sv - # 2023-05-02: change Yb_2 to Yb_3 as Yb_2 gives incorrect thermodynamics for most systems with Yb3+ + # 2023-05-02: Yb_3 would be better here but isn't available yet in the old PBE POTCARs as Yb_2 + # gives incorrect thermodynamics for most systems since Yb usually has oxidation state Yb3+ # https://github.com/materialsproject/pymatgen/issues/2968 - Yb: Yb_3 + Yb: Yb_2 Zn: Zn Zr: Zr_sv diff --git a/src/atomate2/vasp/sets/BaseMPR2SCANRelaxSet.yaml b/src/atomate2/vasp/sets/BaseMPR2SCANRelaxSet.yaml new file mode 100644 index 0000000000..11029bf447 --- /dev/null +++ b/src/atomate2/vasp/sets/BaseMPR2SCANRelaxSet.yaml @@ -0,0 +1,160 @@ +# Default VASP settings for calculations in the Materials Project +# using the Regularized-Restored Strongly Constrained and Appropriately +# Normed functional (r2SCAN). +INCAR: + ALGO: ALL + EDIFF: 1.e-05 + EDIFFG: -0.02 + ENAUG: 1360 + ENCUT: 680 + IBRION: 2 + ISIF: 3 + ISMEAR: 0 # included to have some reasonable default + ISPIN: 2 + KSPACING: 0.22 # included to have some reasonable default + LAECHG: True + LASPH: True + LCHARG: True + LELF: False # LELF = True restricts calculation to KPAR = 1 + LMIXTAU: True + LORBIT: 11 + LREAL: Auto + LVTOT: True + LWAVE: False + METAGGA: R2SCAN + NELM: 200 + NSW: 99 + PREC: Accurate + SIGMA: 0.05 # included to have some reasonable default + MAGMOM: + Ce: 5 + Ce3+: 1 + Co: 0.6 + Co3+: 0.6 + Co4+: 1 + Cr: 5 + Dy3+: 5 + Er3+: 3 + Eu: 10 + Eu2+: 7 + Eu3+: 6 + Fe: 5 + Gd3+: 7 + Ho3+: 4 + La3+: 0.6 + Lu3+: 0.6 + Mn: 5 + Mn3+: 4 + Mn4+: 3 + Mo: 5 + Nd3+: 3 + Ni: 5 + Pm3+: 4 + Pr3+: 2 + Sm3+: 5 + Tb3+: 6 + Tm3+: 2 + V: 5 + W: 5 + Yb3+: 1 +POTCAR_FUNCTIONAL: PBE_54 +POTCAR: + Ac: Ac + Ag: Ag + Al: Al + Am: Am + Ar: Ar + As: As + At: At + Au: Au + B: B + Ba: Ba_sv + Be: Be_sv + Bi: Bi + Br: Br + C: C + Ca: Ca_sv + Cd: Cd + Ce: Ce + Cf: Cf + Cl: Cl + Cm: Cm + Co: Co + Cr: Cr_pv + Cs: Cs_sv + Cu: Cu_pv + Dy: Dy_3 + Er: Er_3 + Eu: Eu + F: F + Fe: Fe_pv + Fr: Fr_sv + Ga: Ga_d + Gd: Gd + Ge: Ge_d + H: H + He: He + Hf: Hf_pv + Hg: Hg + Ho: Ho_3 + I: I + In: In_d + Ir: Ir + K: K_sv + Kr: Kr + La: La + Li: Li_sv + Lu: Lu_3 + Mg: Mg_pv + Mn: Mn_pv + Mo: Mo_pv + N: N + Na: Na_pv + Nb: Nb_pv + Nd: Nd_3 + Ne: Ne + Ni: Ni_pv + Np: Np + O: O + Os: Os_pv + P: P + Pa: Pa + Pb: Pb_d + Pd: Pd + Pm: Pm_3 + Po: Po_d + Pr: Pr_3 + Pt: Pt + Pu: Pu + Ra: Ra_sv + Rb: Rb_sv + Re: Re_pv + Rh: Rh_pv + Rn: Rn + Ru: Ru_pv + S: S + Sb: Sb + Sc: Sc_sv + Se: Se + Si: Si + Sm: Sm_3 + Sn: Sn_d + Sr: Sr_sv + Ta: Ta_pv + Tb: Tb_3 + Tc: Tc_pv + Te: Te + Th: Th + Ti: Ti_pv + Tl: Tl_d + Tm: Tm_3 + U: U + V: V_pv + W: W_sv + Xe: Xe + Y: Y_sv + # 2023-05-02: change Yb_2 to Yb_3 as Yb_2 gives incorrect thermodynamics for most systems with Yb3+ + # https://github.com/materialsproject/pymatgen/issues/2968 + Yb: Yb_3 + Zn: Zn + Zr: Zr_sv diff --git a/src/atomate2/vasp/sets/BaseVaspSet.yaml b/src/atomate2/vasp/sets/BaseVaspSet.yaml index 594ccdd5d3..cadc49b102 100644 --- a/src/atomate2/vasp/sets/BaseVaspSet.yaml +++ b/src/atomate2/vasp/sets/BaseVaspSet.yaml @@ -12,7 +12,7 @@ INCAR: ISMEAR: 0 LORBIT: 11 LASPH: True - LDAU: true + LDAU: True LDAUJ: F: Co: 0 diff --git a/src/atomate2/vasp/sets/base.py b/src/atomate2/vasp/sets/base.py index 6ba5484743..692367fe04 100644 --- a/src/atomate2/vasp/sets/base.py +++ b/src/atomate2/vasp/sets/base.py @@ -9,7 +9,7 @@ from dataclasses import dataclass, field from itertools import groupby from pathlib import Path -from typing import TYPE_CHECKING, Any, Sequence +from typing import TYPE_CHECKING, Any import numpy as np from monty.io import zopen @@ -29,13 +29,13 @@ from atomate2 import SETTINGS if TYPE_CHECKING: + from collections.abc import Sequence + from pymatgen.core import Structure _BASE_VASP_SET = loadfn(resource_filename("atomate2.vasp.sets", "BaseVaspSet.yaml")) -__all__ = ["VaspInputSet", "VaspInputGenerator"] - class VaspInputSet(InputSet): """ @@ -64,12 +64,12 @@ def __init__( potcar: Potcar | list[str], kpoints: Kpoints | None = None, optional_files: dict | None = None, - ): + ) -> None: self.incar = incar self.poscar = poscar self.potcar = potcar self.kpoints = kpoints - self.optional_files = {} if optional_files is None else optional_files + self.optional_files = optional_files or {} def write_input( self, @@ -77,7 +77,7 @@ def write_input( make_dir: bool = True, overwrite: bool = True, potcar_spec: bool = False, - ): + ) -> None: """ Write VASP input files to a directory. @@ -91,8 +91,8 @@ def write_input( Whether to overwrite an input file if it already exists. """ directory = Path(directory) - if make_dir and not directory.exists(): - os.makedirs(directory) + if make_dir: + os.makedirs(directory, exist_ok=True) inputs = { "INCAR": self.incar, @@ -118,7 +118,9 @@ def write_input( raise FileExistsError(f"{directory / k} already exists.") @staticmethod - def from_directory(directory: str | Path, optional_files: dict = None): + def from_directory( + directory: str | Path, optional_files: dict = None + ) -> VaspInputSet: """ Load a set of VASP inputs from a directory. @@ -262,6 +264,11 @@ class VaspInputGenerator(InputGenerator): If true and the system is metallic, try and use ``reciprocal_density_metal`` instead of ``reciprocal_density`` for metallic systems. Note, this only works when generating the input set from a previous VASP directory. + auto_kspacing + If true, automatically use the VASP recommended KSPACING based on bandgap, + i.e. higher kpoint spacing for insulators than metals. Can be boolean or float. + If float, then the value will interpreted as the bandgap in eV to use for the + KSPACING calculation. constrain_total_magmom Whether to constrain the total magmom (NUPDOWN in INCAR) to be the sum of the initial MAGMOM guess for all species. @@ -287,6 +294,10 @@ class VaspInputGenerator(InputGenerator): Tolerance for symmetry finding, used for line mode band structure k-points. config_dict The config dictionary to use containing the base input set settings. + inherit_incar + Whether to inherit INCAR settings from previous calculation. This might be + useful to port Custodian fixes to child jobs but can also be dangerous e.g. + when switching from GGA to meta-GGA or relax to static jobs. Defaults to True. """ user_incar_settings: dict = field(default_factory=dict) @@ -296,6 +307,7 @@ class VaspInputGenerator(InputGenerator): auto_ismear: bool = True auto_ispin: bool = False auto_lreal: bool = False + auto_kspacing: bool | float = False auto_metal_kpoints: bool = True constrain_total_magmom: bool = False validate_magmom: bool = True @@ -304,9 +316,12 @@ class VaspInputGenerator(InputGenerator): force_gamma: bool = True symprec: float = SETTINGS.SYMPREC vdw: str = None + # copy _BASE_VASP_SET to ensure each class instance has its own copy + # otherwise in-place changes can affect other instances config_dict: dict = field(default_factory=lambda: _BASE_VASP_SET) + inherit_incar: bool = None - def __post_init__(self): + def __post_init__(self) -> None: """Post init formatting of arguments.""" self.vdw = None if self.vdw is None else self.vdw.lower() @@ -365,7 +380,10 @@ def __post_init__(self): for k, v in self.user_potcar_settings.items(): self.config_dict["POTCAR"][k] = v - def get_input_set( # type: ignore + if self.inherit_incar is None: + self.inherit_incar = SETTINGS.VASP_INHERIT_INCAR + + def get_input_set( self, structure: Structure = None, prev_dir: str | Path = None, @@ -398,20 +416,16 @@ def get_input_set( # type: ignore structure, prev_incar, bandgap, ispin, vasprun, outcar = self._get_previous( structure, prev_dir ) - incar_updates = self.get_incar_updates( - structure, - prev_incar=prev_incar, - bandgap=bandgap, - vasprun=vasprun, - outcar=outcar, - ) - kpoints_updates = self.get_kpoints_updates( - structure, - prev_incar=prev_incar, - bandgap=bandgap, - vasprun=vasprun, - outcar=outcar, - ) + prev_incar = prev_incar if self.inherit_incar else {} + kwds = { + "structure": structure, + "prev_incar": prev_incar, + "bandgap": bandgap, + "vasprun": vasprun, + "outcar": outcar, + } + incar_updates = self.get_incar_updates(**kwds) + kpoints_updates = self.get_kpoints_updates(**kwds) kspacing = self._kspacing(incar_updates) kpoints = self._get_kpoints(structure, kpoints_updates, kspacing, bandgap) incar = self._get_incar( @@ -508,16 +522,17 @@ def get_nelect(self, structure: Structure) -> float: Number of electrons for the structure. """ potcar = self._get_potcar(structure, potcar_spec=False) - nelec = {p.element: p.nelectrons for p in potcar} + map_elem_electrons = {p.element: p.nelectrons for p in potcar} comp = structure.composition.element_composition - nelect = sum(num_atoms * nelec[str(el)] for el, num_atoms in comp.items()) - - if self.use_structure_charge: - return nelect - structure.charge + n_electrons = sum( + num_atoms * map_elem_electrons[str(el)] for el, num_atoms in comp.items() + ) - return nelect + return n_electrons - (structure.charge if self.use_structure_charge else 0) - def _get_previous(self, structure: Structure = None, prev_dir: str | Path = None): + def _get_previous( + self, structure: Structure = None, prev_dir: str | Path = None + ) -> tuple: """Load previous calculation outputs and decide which structure to use.""" if structure is None and prev_dir is None: raise ValueError("Either structure or prev_dir must be set.") @@ -563,7 +578,7 @@ def _get_previous(self, structure: Structure = None, prev_dir: str | Path = None return structure, prev_incar, bandgap, ispin, vasprun, outcar - def _get_structure(self, structure): + def _get_structure(self, structure: Structure) -> Structure: """Get the standardized structure.""" for site in structure: if "magmom" in site.properties and isinstance( @@ -579,7 +594,7 @@ def _get_structure(self, structure): get_valid_magmom_struct(structure, spin_mode="auto", inplace=True) return structure - def _get_potcar(self, structure, potcar_spec: bool = False): + def _get_potcar(self, structure: Structure, potcar_spec: bool = False) -> Potcar: """Get the POTCAR.""" elements = [a[0] for a in groupby([s.specie.symbol for s in structure])] potcar_symbols = [self.config_dict["POTCAR"].get(el, el) for el in elements] @@ -605,16 +620,16 @@ def _get_potcar(self, structure, potcar_spec: bool = False): def _get_incar( self, - structure, + structure: Structure, kpoints: Kpoints, previous_incar: dict = None, incar_updates: dict = None, bandgap: float = None, ispin: int = None, - ): + ) -> Incar: """Get the INCAR.""" - previous_incar = {} if previous_incar is None else previous_incar - incar_updates = {} if incar_updates is None else incar_updates + previous_incar = previous_incar or {} + incar_updates = incar_updates or {} incar_settings = dict(self.config_dict["INCAR"]) config_magmoms = incar_settings.get("MAGMOM", {}) auto_updates = {} @@ -667,24 +682,26 @@ def _get_incar( auto_updates["ISPIN"] = ispin if self.auto_ismear: + bandgap_tol = getattr(self, "bandgap_tol", SETTINGS.BANDGAP_TOL) if bandgap is None: # don't know if we are a metal or insulator so set ISMEAR and SIGMA to # be safe with the most general settings - auto_updates.update({"ISMEAR": 0, "SIGMA": 0.2}) - elif bandgap == 0: - auto_updates.update({"ISMEAR": 2, "SIGMA": 0.2}) # metal + auto_updates.update(ISMEAR=0, SIGMA=0.2) + elif bandgap <= bandgap_tol: + auto_updates.update(ISMEAR=2, SIGMA=0.2) # metal else: - auto_updates.update({"ISMEAR": -5, "SIGMA": 0.05}) # insulator + auto_updates.update(ISMEAR=-5, SIGMA=0.05) # insulator if self.auto_lreal: auto_updates["LREAL"] = _get_recommended_lreal(structure) - if kpoints is not None: - # unset KSPACING as we are using a KPOINTS file and ensure adequate number - # of KPOINTS are present for the tetrahedron method (ISMEAR=-5). - incar.pop("KSPACING", None) - if np.product(kpoints.kpts) < 4 and incar.get("ISMEAR", 0) == -5: - auto_updates["ISMEAR"] = 0 + if self.auto_kspacing is False: + bandgap = None # don't auto-set KSPACING based on bandgap + elif isinstance(self.auto_kspacing, float): + # interpret auto_kspacing as bandgap and set KSPACING based on user input + bandgap = self.auto_kspacing + + _set_kspacing(incar, incar_settings, self.user_incar_settings, bandgap, kpoints) # apply updates from auto options, careful not to override user_incar_settings _apply_incar_updates(incar, auto_updates, skip=list(self.user_incar_settings)) @@ -709,7 +726,7 @@ def _get_kpoints( bandgap: float | None, ) -> Kpoints | None: """Get the kpoints file.""" - kpoints_updates = {} if kpoints_updates is None else kpoints_updates + kpoints_updates = kpoints_updates or {} # use user setting if set otherwise default to base config settings if self.user_kpoints_settings != {}: @@ -860,7 +877,7 @@ def _get_kpoints( return _combine_kpoints(base_kpoints, zero_weighted_kpoints, added_kpoints) - def _kspacing(self, incar_updates): + def _kspacing(self, incar_updates) -> float | None: """Get KSPACING value based on the config dict, updates and user settings.""" if "KSPACING" in self.user_incar_settings: return self.user_incar_settings["KSPACING"] @@ -911,7 +928,7 @@ def _get_magmoms( return mag -def _get_u_param(lda_param, lda_config, structure): +def _get_u_param(lda_param, lda_config, structure: Structure) -> list[float]: """Get U parameters.""" comp = structure.composition elements = sorted((el for el in comp.elements if comp[el] > 0), key=lambda e: e.X) @@ -932,38 +949,36 @@ def _get_u_param(lda_param, lda_config, structure): ] -def _get_ediff(param, value, structure, incar_settings): +def _get_ediff(param, value, structure: Structure, incar_settings) -> float: """Get EDIFF.""" if incar_settings.get("EDIFF") is None and param == "EDIFF_PER_ATOM": return float(value) * structure.num_sites return float(incar_settings["EDIFF"]) -def _set_u_params(incar, incar_settings, structure): +def _set_u_params(incar: Incar, incar_settings, structure: Structure) -> None: """Modify INCAR for use with U parameters.""" - has_u = incar_settings.get("LDAU", False) and sum(incar["LDAUU"]) > 0 + has_u = incar_settings.get("LDAU") and sum(incar["LDAUU"]) > 0 if not has_u: - for key in list(incar): - if key.startswith("LDAU"): - del incar[key] + ldau_keys = [key for key in incar if key.startswith("LDAU")] + for key in ldau_keys: + incar.pop(key, None) # Modify LMAXMIX if you have d or f electrons present. Note that if the user - # explicitly sets LMAXMIX in settings it will override this logic. Previously, this - # was only set if Hubbard U was enabled as per the VASP manual but following an - # investigation it was determined that this would lead to a significant difference - # between SCF -> NonSCF even without Hubbard U enabled. Thanks to Andrew Rosen for - # investigating and reporting. - if "LMAXMIX" not in incar_settings: - # contains f-electrons - if any(el.Z > 56 for el in structure.composition): - incar["LMAXMIX"] = 6 - # contains d-electrons - elif any(el.Z > 20 for el in structure.composition): - incar["LMAXMIX"] = 4 - - -def _apply_incar_updates(incar, updates, skip: Sequence[str] = ()): + # explicitly sets LMAXMIX in settings it will override this logic (setdefault keeps + # current value). Previously, this was only set if Hubbard U was enabled as per the + # VASP manual but following an investigation it was determined that this would lead + # to a significant difference between SCF -> NonSCF even without Hubbard U enabled. + # Thanks to Andrew Rosen for investigating and reporting. + blocks = [site.specie.block for site in structure] + if "f" in blocks: # contains f-electrons + incar.setdefault("LMAXMIX", 6) + elif "d" in blocks: # contains d-electrons + incar.setdefault("LMAXMIX", 4) + + +def _apply_incar_updates(incar, updates, skip: Sequence[str] = ()) -> None: """ Apply updates to an INCAR file. @@ -986,7 +1001,7 @@ def _apply_incar_updates(incar, updates, skip: Sequence[str] = ()): incar[k] = v -def _remove_unused_incar_params(incar, skip: Sequence[str] = ()): +def _remove_unused_incar_params(incar, skip: Sequence[str] = ()) -> None: """ Remove INCAR parameters that are not actively used by VASP. @@ -1016,14 +1031,14 @@ def _remove_unused_incar_params(incar, skip: Sequence[str] = ()): incar.pop(ldau_flag, None) -def _combine_kpoints(*kpoints_objects: Kpoints): +def _combine_kpoints(*kpoints_objects: Kpoints) -> Kpoints: """Combine k-points files together.""" labels = [] kpoints = [] weights = [] for kpoints_object in filter(None, kpoints_objects): - if not kpoints_object.style == Kpoints.supported_modes.Reciprocal: + if kpoints_object.style != Kpoints.supported_modes.Reciprocal: raise ValueError( "Can only combine kpoints with style=Kpoints.supported_modes.Reciprocal" ) @@ -1048,7 +1063,7 @@ def _combine_kpoints(*kpoints_objects: Kpoints): ) -def _get_ispin(vasprun: Vasprun | None, outcar: Outcar | None): +def _get_ispin(vasprun: Vasprun | None, outcar: Outcar | None) -> int: """Get value of ISPIN depending on the magnetisation in the OUTCAR and vasprun.""" if outcar is not None and outcar.magnetization is not None: # Turn off spin when magmom for every site is smaller than 0.02. @@ -1059,8 +1074,64 @@ def _get_ispin(vasprun: Vasprun | None, outcar: Outcar | None): return 2 -def _get_recommended_lreal(structure: Structure): +def _get_recommended_lreal(structure: Structure) -> str | bool: """Get recommended LREAL flag based on the structure.""" - if structure.num_sites > 16: - return "Auto" - return False + return "Auto" if structure.num_sites > 16 else False + + +def _get_kspacing(bandgap: float, tol: float = 1e-4) -> float: + """Get KSPACING based on a band gap.""" + if bandgap <= tol: # metallic + return 0.22 + + rmin = max(1.5, 25.22 - 2.87 * bandgap) # Eq. 25 + kspacing = 2 * np.pi * 1.0265 / (rmin - 1.0183) # Eq. 29 + + # cap kspacing at a max of 0.44, per internal benchmarking + return min(kspacing, 0.44) + + +def _set_kspacing( + incar: Incar, + incar_settings: dict, + user_incar_settings: dict, + bandgap: float | None, + kpoints: Kpoints | None, +) -> Incar: + """ + Set KSPACING in an INCAR. + + if kpoints is not None then unset any KSPACING + if kspacing set in user_incar_settings then use that + if auto_kspacing then do that + if kspacing is set in config use that. + if from_prev is True, ISMEAR will be set according to the band gap. + """ + if kpoints is not None: + # unset KSPACING as we are using a KPOINTS file + incar.pop("KSPACING", None) + + # Ensure adequate number of KPOINTS are present for the tetrahedron method + # (ISMEAR=-5). If KSPACING is in the INCAR file the number of kpoints is not + # known before calling VASP, but a warning is raised when the KSPACING value is + # > 0.5 (2 reciprocal Angstrom). An error handler in Custodian is available to + # correct overly large KSPACING values (small number of kpoints) if necessary. + if np.prod(kpoints.kpts) < 4 and incar.get("ISMEAR", 0) == -5: + incar["ISMEAR"] = 0 + + elif "KSPACING" in user_incar_settings: + incar["KSPACING"] = user_incar_settings["KSPACING"] + + elif incar_settings.get("KSPACING") and isinstance(bandgap, (int, float)): + # will always default to 0.22 in first run as one + # cannot be sure if one treats a metal or + # semiconductor/insulator + incar["KSPACING"] = _get_kspacing(bandgap) + # This should default to ISMEAR=0 if band gap is not known (first computation) + # if not from_prev: + # # be careful to not override user_incar_settings + + elif incar_settings.get("KSPACING"): + incar["KSPACING"] = incar_settings["KSPACING"] + + return incar diff --git a/src/atomate2/vasp/sets/core.py b/src/atomate2/vasp/sets/core.py index f35589668d..a405eb9fd0 100644 --- a/src/atomate2/vasp/sets/core.py +++ b/src/atomate2/vasp/sets/core.py @@ -21,20 +21,6 @@ logger = logging.getLogger(__name__) -__all__ = [ - "RelaxSetGenerator", - "TightRelaxSetGenerator", - "StaticSetGenerator", - "NonSCFSetGenerator", - "HSERelaxSetGenerator", - "HSEStaticSetGenerator", - "HSEBSSetGenerator", - "HSETightRelaxSetGenerator", - "ElectronPhononSetGenerator", - "MDSetGenerator", -] - - @dataclass class RelaxSetGenerator(VaspInputGenerator): """Class to generate VASP relaxation input sets.""" @@ -166,13 +152,7 @@ def get_incar_updates( dict A dictionary of updates to apply. """ - updates = { - "NSW": 0, - "ISMEAR": -5, - "LCHARG": True, - "LORBIT": 11, - "LREAL": False, - } + updates = {"NSW": 0, "ISMEAR": -5, "LCHARG": True, "LORBIT": 11, "LREAL": False} if self.lepsilon: # LPEAD=T: numerical evaluation of overlap integral prevents LRF_COMMUTATOR # errors and can lead to better expt. agreement but produces slightly @@ -220,7 +200,7 @@ class NonSCFSetGenerator(VaspInputGenerator): nbands_factor: float = 1.2 auto_ispin: bool = True - def __post_init__(self): + def __post_init__(self) -> None: """Ensure mode is set correctly.""" super().__post_init__() self.mode = self.mode.lower() @@ -311,16 +291,16 @@ def get_incar_updates( } if vasprun is not None: - # set nbands - nbands = int(np.ceil(vasprun.parameters["NBANDS"] * self.nbands_factor)) - updates["NBANDS"] = nbands + # set NBANDS + n_bands = int(np.ceil(vasprun.parameters["NBANDS"] * self.nbands_factor)) + updates["NBANDS"] = n_bands if self.mode == "uniform": - # automatic setting of nedos using the energy range and the energy step - nedos = _get_nedos(vasprun, self.dedos) + # automatic setting of NEDOS using the energy range and the energy step + n_edos = _get_nedos(vasprun, self.dedos) # use tetrahedron method for DOS and optics calculations - updates.update({"ISMEAR": -5, "ISYM": 2, "NEDOS": nedos}) + updates.update({"ISMEAR": -5, "ISYM": 2, "NEDOS": n_edos}) elif self.mode in ("line", "boltztrap"): # if line mode or explicit k-points (boltztrap) can't use ISMEAR=-5 @@ -349,7 +329,6 @@ class HSERelaxSetGenerator(VaspInputGenerator): By default the hybrid input sets use ALGO = Normal which is only efficient for VASP 6.0 and higher. See https://www.vasp.at/wiki/index.php/LFOCKACE for more details. - """ def get_incar_updates( @@ -462,7 +441,6 @@ class HSEStaticSetGenerator(VaspInputGenerator): By default the hybrid input sets use ALGO = Normal which is only efficient for VASP 6.0 and higher. See https://www.vasp.at/wiki/index.php/LFOCKACE for more details. - """ def get_incar_updates( @@ -572,7 +550,7 @@ class HSEBSSetGenerator(VaspInputGenerator): added_kpoints: list[Vector3D] = field(default_factory=list) auto_ispin: bool = True - def __post_init__(self): + def __post_init__(self) -> None: """Ensure mode is set correctly.""" super().__post_init__() @@ -924,15 +902,13 @@ def _get_ensemble_defaults(structure: Structure, ensemble: str) -> dict[str, Any } try: - return defaults[ensemble.lower()] # type: ignore + return defaults[ensemble.lower()] # type: ignore[return-value] except KeyError as err: supported = tuple(defaults) - raise ValueError( - f"Expect `ensemble` to be one of {supported}; got {ensemble}." - ) from err + raise ValueError(f"Expect {ensemble=} to be one of {supported}") from err -def _get_nedos(vasprun: Vasprun | None, dedos: float): +def _get_nedos(vasprun: Vasprun | None, dedos: float) -> int: """Automatic setting of nedos using the energy range and the energy step.""" if vasprun is None: return 2000 diff --git a/src/atomate2/vasp/sets/matpes.py b/src/atomate2/vasp/sets/matpes.py new file mode 100644 index 0000000000..19bfdc5ee3 --- /dev/null +++ b/src/atomate2/vasp/sets/matpes.py @@ -0,0 +1,106 @@ +""" +Module defining MatPES input set generators. + +In case of questions, contact @janosh or @shyuep. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import TYPE_CHECKING + +from monty.serialization import loadfn +from pkg_resources import resource_filename + +from atomate2.vasp.sets.base import VaspInputGenerator + +if TYPE_CHECKING: + from pymatgen.core import Structure + from pymatgen.io.vasp import Outcar, Vasprun + + +# POTCAR section comes from PARENT but atomate2 does not support inheritance yet +_BASE_MATPES_PBE_STATIC_SET_NO_POTCAR = loadfn( + resource_filename("pymatgen.io.vasp", "MatPESStaticSet.yaml") +) +_BASE_PBE54_SET = loadfn(resource_filename("pymatgen.io.vasp", "PBE54Base.yaml")) +_BASE_MATPES_PBE_STATIC_SET = { + **_BASE_PBE54_SET, + **_BASE_MATPES_PBE_STATIC_SET_NO_POTCAR, +} + + +@dataclass +class MatPesGGAStaticSetGenerator(VaspInputGenerator): + """Class to generate MP-compatible VASP GGA static input sets.""" + + config_dict: dict = field(default_factory=lambda: _BASE_MATPES_PBE_STATIC_SET) + auto_ismear: bool = False + auto_kspacing: bool = False + + def get_incar_updates( + self, + structure: Structure, + prev_incar: dict = None, + bandgap: float = None, + vasprun: Vasprun = None, + outcar: Outcar = None, + ) -> dict: + """ + Get updates to the INCAR for this calculation type. + + Parameters + ---------- + structure + A structure. + prev_incar + An incar from a previous calculation. + bandgap + The band gap. + vasprun + A vasprun from a previous calculation. + outcar + An outcar from a previous calculation. + + Returns + ------- + dict + A dictionary of updates to apply. + """ + return {} + + +@dataclass +class MatPesMetaGGAStaticSetGenerator(MatPesGGAStaticSetGenerator): + """Class to generate MP-compatible VASP GGA static input sets.""" + + def get_incar_updates( + self, + structure: Structure, + prev_incar: dict = None, + bandgap: float = None, + vasprun: Vasprun = None, + outcar: Outcar = None, + ) -> dict: + """ + Get updates to the INCAR for this calculation type. + + Parameters + ---------- + structure + A structure. + prev_incar + An incar from a previous calculation. + bandgap + The band gap. + vasprun + A vasprun from a previous calculation. + outcar + An outcar from a previous calculation. + + Returns + ------- + dict + A dictionary of updates to apply. + """ + return {"METAGGA": "R2SCAN", "ALGO": "ALL", "GGA": None} # unset GGA diff --git a/src/atomate2/vasp/sets/mp.py b/src/atomate2/vasp/sets/mp.py new file mode 100644 index 0000000000..63ecbe9186 --- /dev/null +++ b/src/atomate2/vasp/sets/mp.py @@ -0,0 +1,183 @@ +""" +Module defining Materials Project input set generators. + +Reference: https://doi.org/10.1103/PhysRevMaterials.6.013801 + +In case of questions, consult @Andrew-S-Rosen, @esoteric-ephemera or @janosh. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import TYPE_CHECKING + +from monty.serialization import loadfn +from pkg_resources import resource_filename + +from atomate2.vasp.sets.core import RelaxSetGenerator, StaticSetGenerator + +if TYPE_CHECKING: + from pymatgen.core import Structure + from pymatgen.io.vasp import Outcar, Vasprun + +_BASE_MP_GGA_RELAX_SET = loadfn( + resource_filename("atomate2.vasp.sets", "BaseMPGGASet.yaml") +) +_BASE_MP_R2SCAN_RELAX_SET = loadfn( + resource_filename("atomate2.vasp.sets", "BaseMPR2SCANRelaxSet.yaml") +) + + +@dataclass +class MPGGARelaxSetGenerator(RelaxSetGenerator): + """Class to generate MP-compatible VASP GGA relaxation input sets.""" + + config_dict: dict = field(default_factory=lambda: _BASE_MP_GGA_RELAX_SET) + auto_kspacing: bool = True + inherit_incar: bool = False + + +@dataclass +class MPGGAStaticSetGenerator(StaticSetGenerator): + """Class to generate MP-compatible VASP GGA static input sets.""" + + config_dict: dict = field(default_factory=lambda: _BASE_MP_GGA_RELAX_SET) + auto_kspacing: bool = True + inherit_incar: bool = False + + def get_incar_updates( + self, + structure: Structure, + prev_incar: dict = None, + bandgap: float = None, + vasprun: Vasprun = None, + outcar: Outcar = None, + ) -> dict: + """ + Get updates to the INCAR for this calculation type. + + Parameters + ---------- + structure + A structure. + prev_incar + An incar from a previous calculation. + bandgap + The band gap. + vasprun + A vasprun from a previous calculation. + outcar + An outcar from a previous calculation. + + Returns + ------- + dict + A dictionary of updates to apply. + """ + return { + "ALGO": "FAST", + "NSW": 0, + "LCHARG": True, + "LWAVE": False, + "LREAL": False, + "ISMEAR": -5, + } + + +@dataclass +class MPMetaGGAStaticSetGenerator(StaticSetGenerator): + """Class to generate MP-compatible VASP GGA static input sets.""" + + config_dict: dict = field(default_factory=lambda: _BASE_MP_R2SCAN_RELAX_SET) + auto_kspacing: bool = True + inherit_incar: bool = False + + def get_incar_updates( + self, + structure: Structure, + prev_incar: dict = None, + bandgap: float = None, + vasprun: Vasprun = None, + outcar: Outcar = None, + ) -> dict: + """ + Get updates to the INCAR for this calculation type. + + Parameters + ---------- + structure + A structure. + prev_incar + An incar from a previous calculation. + bandgap + The band gap. + vasprun + A vasprun from a previous calculation. + outcar + An outcar from a previous calculation. + + Returns + ------- + dict + A dictionary of updates to apply. + """ + return { + "ALGO": "FAST", + "GGA": None, # unset GGA, shouldn't be set anyway but best be sure + "NSW": 0, + "LCHARG": True, + "LWAVE": False, + "LREAL": False, + "ISMEAR": -5, + } + + +@dataclass +class MPMetaGGARelaxSetGenerator(RelaxSetGenerator): + """Class to generate MP-compatible VASP metaGGA relaxation input sets. + + Parameters + ---------- + config_dict: dict + The config dict. + bandgap_tol: float + Tolerance for metallic bandgap. If bandgap < bandgap_tol, KSPACING will be 0.22, + otherwise it will increase with bandgap up to a max of 0.44. + """ + + config_dict: dict = field(default_factory=lambda: _BASE_MP_R2SCAN_RELAX_SET) + bandgap_tol: float = 1e-4 + auto_kspacing: bool = True + inherit_incar: bool = False + + def get_incar_updates( + self, + structure: Structure, + prev_incar: dict = None, + bandgap: float = None, + vasprun: Vasprun = None, + outcar: Outcar = None, + ) -> dict: + """ + Get updates to the INCAR for this calculation type. + + Parameters + ---------- + structure + A structure. + prev_incar + An incar from a previous calculation. + bandgap + The band gap. + vasprun + A vasprun from a previous calculation. + outcar + An outcar from a previous calculation. + + Returns + ------- + dict + A dictionary of updates to apply. + """ + # unset GGA, shouldn't be set anyway but doesn't hurt to be sure + return {"LCHARG": True, "LWAVE": True, "GGA": None} diff --git a/tests/common/jobs/__init__.py b/tests/common/jobs/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/vasp/jobs/test_elastic.py b/tests/common/jobs/test_elastic.py similarity index 95% rename from tests/vasp/jobs/test_elastic.py rename to tests/common/jobs/test_elastic.py index 8f2cb8b829..b152bc6b0d 100644 --- a/tests/vasp/jobs/test_elastic.py +++ b/tests/common/jobs/test_elastic.py @@ -5,8 +5,8 @@ from pymatgen.symmetry.analyzer import SpacegroupAnalyzer from atomate2 import SETTINGS +from atomate2.common.jobs.elastic import generate_elastic_deformations from atomate2.common.schemas.elastic import _expand_strains -from atomate2.vasp.jobs.elastic import generate_elastic_deformations @pytest.mark.parametrize("conventional", [False, True]) diff --git a/tests/common/schemas/test_cclib.py b/tests/common/schemas/test_cclib.py index 5239de14ea..2561316ad2 100644 --- a/tests/common/schemas/test_cclib.py +++ b/tests/common/schemas/test_cclib.py @@ -96,7 +96,8 @@ def test_cclib_taskdoc(test_dir): task.dict() # test document can be jsanitized - d = jsanitize(doc, enum_values=True) + dct = jsanitize(doc, enum_values=True) # and decoded - MontyDecoder().process_decoded(d) + json_str = MontyDecoder().process_decoded(dct) + assert "builder_meta=EmmetMeta" in json_str diff --git a/tests/common/test_jobs.py b/tests/common/test_jobs.py index 05cd06cfc4..08e4a40ab0 100644 --- a/tests/common/test_jobs.py +++ b/tests/common/test_jobs.py @@ -30,7 +30,7 @@ def test_structure_to_conventional(si_structure): @mark.skipif( - not os.environ.get("MP_API_KEY"), + not os.getenv("MP_API_KEY"), reason="Materials Project API key not set in environment.", ) def test_retrieve_structure_from_materials_project(): diff --git a/tests/common/test_settings.py b/tests/common/test_settings.py index a80e195377..004adbdacc 100644 --- a/tests/common/test_settings.py +++ b/tests/common/test_settings.py @@ -1,4 +1,5 @@ import pytest +from pydantic import ValidationError def test_empty_and_invalid_config_file(clean_dir): @@ -24,3 +25,24 @@ def test_empty_and_invalid_config_file(clean_dir): with pytest.raises(SyntaxError, match="atomate2 config file at"): Atomate2Settings() + + # test error if the file exists and contains invalid settings + with open(config_file_path, "w") as file: + file.write("VASP_CMD: 42") + + with pytest.raises( + ValidationError, + match="1 validation error for Atomate2Settings\nVASP_CMD\n " + "Input should be a valid string ", + ): + Atomate2Settings() + + with open(config_file_path, "w") as file: + file.write("BANDGAP_TOL: invalid") + + with pytest.raises( + ValidationError, + match="1 validation error for Atomate2Settings\nBANDGAP_TOL\n " + "Input should be a valid number", + ): + Atomate2Settings() diff --git a/tests/conftest.py b/tests/conftest.py index 747afa26c9..85951de8af 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -52,11 +52,11 @@ def tmp_dir(): import tempfile old_cwd = os.getcwd() - newpath = tempfile.mkdtemp() - os.chdir(newpath) + new_path = tempfile.mkdtemp() + os.chdir(new_path) yield os.chdir(old_cwd) - shutil.rmtree(newpath) + shutil.rmtree(new_path) @pytest.fixture(scope="session") diff --git a/tests/cp2k/conftest.py b/tests/cp2k/conftest.py index 4fc9f7feef..734b18aa8e 100644 --- a/tests/cp2k/conftest.py +++ b/tests/cp2k/conftest.py @@ -1,10 +1,15 @@ +from __future__ import annotations + import logging from hashlib import md5 from pathlib import Path -from typing import Literal, Sequence, Union +from typing import TYPE_CHECKING, Literal import pytest +if TYPE_CHECKING: + from collections.abc import Sequence + logger = logging.getLogger("atomate2") _VFILES = "cp2k.inp" @@ -126,7 +131,7 @@ def _run(ref_paths, fake_run_cp2k_kwargs=None): def fake_run_cp2k( - ref_path: Union[str, Path], + ref_path: str | Path, input_settings: Sequence[str] = (), check_inputs: Sequence[Literal["cp2k.inp"]] = _VFILES, clear_inputs: bool = True, @@ -170,8 +175,8 @@ def _check_input(ref_path, user_input): from pymatgen.io.cp2k.inputs import Cp2kInput ref = Cp2kInput.from_file(ref_path / "inputs" / "cp2k.inp") - user_input.verbosity(False) - ref.verbosity(False) + user_input.verbosity(verbosity=False) + ref.verbosity(verbosity=False) user_string = " ".join(user_input.get_string().lower().split()) user_hash = md5(user_string.encode("utf-8")).hexdigest() @@ -194,7 +199,7 @@ def clear_cp2k_inputs(): logger.info("Cleared cp2k inputs") -def copy_cp2k_outputs(ref_path: Union[str, Path]): +def copy_cp2k_outputs(ref_path: str | Path): import shutil output_path = ref_path / "outputs" diff --git a/tests/cp2k/jobs/test_core.py b/tests/cp2k/jobs/test_core.py index 8f74099963..c7afa23e05 100644 --- a/tests/cp2k/jobs/test_core.py +++ b/tests/cp2k/jobs/test_core.py @@ -29,7 +29,7 @@ def test_static_maker(tmp_path, mock_cp2k, si_structure, basis_and_potential): os.chdir(tmp_path) responses = run_locally(job, create_folders=True, ensure_success=True) - # validation the outputs of the job + # validate job outputs output1 = responses[job.uuid][1].output assert isinstance(output1, TaskDocument) assert output1.output.energy == approx(-214.23651374775685) @@ -63,11 +63,14 @@ def test_relax_maker(tmp_path, mock_cp2k, basis_and_potential, si_structure): os.chdir(tmp_path) responses = run_locally(job, create_folders=True, ensure_success=True) - # validation the outputs of the job + # validate job outputs output1 = responses[job.uuid][1].output assert isinstance(output1, TaskDocument) assert output1.output.energy == approx(-193.39161102270234) assert len(output1.calcs_reversed[0].output.ionic_steps) == 1 + assert output1.calcs_reversed[0].output.structure.lattice.abc == approx( + si_structure.lattice.abc + ) def test_transmuter(tmp_path, mock_cp2k, basis_and_potential, si_structure): diff --git a/tests/forcefields/flows/test_elastic.py b/tests/forcefields/flows/test_elastic.py new file mode 100644 index 0000000000..bd4c29848c --- /dev/null +++ b/tests/forcefields/flows/test_elastic.py @@ -0,0 +1,30 @@ +def test_elastic_wf(clean_dir, si_structure): + from jobflow import run_locally + from numpy.testing import assert_allclose + from pymatgen.symmetry.analyzer import SpacegroupAnalyzer + + from atomate2.common.schemas.elastic import ElasticDocument + from atomate2.forcefields.flows.elastic import ElasticMaker + from atomate2.forcefields.jobs import M3GNetRelaxMaker + + si_prim = SpacegroupAnalyzer(si_structure).get_primitive_standard_structure() + + # !!! Generate job + job = ElasticMaker( + bulk_relax_maker=M3GNetRelaxMaker( + relax_cell=True, relax_kwargs={"fmax": 0.00001} + ), + elastic_relax_maker=M3GNetRelaxMaker( + relax_cell=False, relax_kwargs={"fmax": 0.00001} + ), + ).make(si_prim) + + # run the flow or job and ensure that it finished running successfully + responses = run_locally(job, create_folders=True, ensure_success=True) + elastic_output = responses[job.jobs[-1].uuid][1].output + assert isinstance(elastic_output, ElasticDocument) + assert_allclose(elastic_output.derived_properties.k_voigt, 118.26914, atol=1e-1) + assert_allclose( + elastic_output.derived_properties.g_voigt, 17.327374125417816, atol=1e-1 + ) + assert elastic_output.chemsys == "Si" diff --git a/tests/forcefields/flows/test_phonon.py b/tests/forcefields/flows/test_phonon.py index faeec933b1..c18cb86622 100644 --- a/tests/forcefields/flows/test_phonon.py +++ b/tests/forcefields/flows/test_phonon.py @@ -1,4 +1,4 @@ -import numpy as np +from numpy.testing import assert_allclose from pymatgen.core.structure import Structure from pymatgen.phonon.bandstructure import PhononBandStructureSymmLine from pymatgen.phonon.dos import PhononDos @@ -21,7 +21,6 @@ def test_phonon_wf(clean_dir): coords=[[0, 0, 0], [0.25, 0.25, 0.25]], ) - # !!! Generate job job = PhononMaker( use_symmetrized_structure="conventional", create_thermal_displacements=False, @@ -33,22 +32,13 @@ def test_phonon_wf(clean_dir): # run the flow or job and ensure that it finished running successfully responses = run_locally(job, create_folders=True, ensure_success=True) - # !!! validation on the outputs + # validate the outputs assert isinstance(responses[job.jobs[-1].uuid][1].output, PhononBSDOSDoc) - assert np.allclose( - np.array(responses[job.jobs[-1].uuid][1].output.free_energies) / 1000.0, - np.array( - [ - 5058.45217527524, - 4907.495751683517, - 3966.5493299635937, - 2157.8178928940474, - -357.5054580420707, - ] - ) - / 1000.0, - 2, + assert_allclose( + responses[job.jobs[-1].uuid][1].output.free_energies, + [5058.4521752, 4907.4957516, 3966.5493299, 2157.8178928, -357.5054580], + rtol=0.08, ) assert isinstance( @@ -58,28 +48,25 @@ def test_phonon_wf(clean_dir): assert isinstance(responses[job.jobs[-1].uuid][1].output.phonon_dos, PhononDos) assert responses[job.jobs[-1].uuid][1].output.thermal_displacement_data is None assert isinstance(responses[job.jobs[-1].uuid][1].output.structure, Structure) - assert np.allclose( + assert_allclose( responses[job.jobs[-1].uuid][1].output.temperatures, [0, 100, 200, 300, 400] ) assert responses[job.jobs[-1].uuid][1].output.force_constants is None assert isinstance(responses[job.jobs[-1].uuid][1].output.jobdirs, PhononJobDirs) assert isinstance(responses[job.jobs[-1].uuid][1].output.uuids, PhononUUIDs) - assert np.isclose( + assert_allclose( responses[job.jobs[-1].uuid][1].output.total_dft_energy, -5.372457981109619, 4 ) assert responses[job.jobs[-1].uuid][1].output.born is None assert responses[job.jobs[-1].uuid][1].output.epsilon_static is None - assert np.allclose( + assert_allclose( responses[job.jobs[-1].uuid][1].output.supercell_matrix, [[4.0, 0.0, 0.0], [0.0, 4.0, 0.0], [0.0, 0.0, 4.0]], ) - assert np.allclose( + assert_allclose( responses[job.jobs[-1].uuid][1].output.primitive_matrix, - ( - (0, 0.5000000000000001, 0.5000000000000001), - (0.5000000000000001, 0.0, 0.5000000000000001), - (0.5000000000000001, 0.5000000000000001, 0.0), - ), + ((0, 0.5, 0.5), (0.5, 0.0, 0.5), (0.5, 0.5, 0.0)), + atol=1e-8, ) assert responses[job.jobs[-1].uuid][1].output.code == "vasp" assert isinstance( @@ -95,40 +82,19 @@ def test_phonon_wf(clean_dir): responses[job.jobs[-1].uuid][1].output.phonopy_settings.kpoint_density_dos == 7000 ) - assert np.allclose( + assert_allclose( responses[job.jobs[-1].uuid][1].output.entropies, - [ - 0.0, - 4.783939817386235, - 13.993186953791708, - 21.88641334781562, - 28.19110667148253, - ], - 3, + [0.0, 4.7839398173, 13.993186953, 21.886413347, 28.191106671], + rtol=0.05, ) - assert np.allclose( + assert_allclose( responses[job.jobs[-1].uuid][1].output.heat_capacities, - [ - 0.0, - 8.860605865667427, - 17.55758943495313, - 21.089039169564796, - 22.625872713428905, - ], - 3, + [0.0, 8.8606058656, 17.557589434, 21.089039169, 22.625872713], + rtol=0.05, ) - assert np.allclose( - np.array(responses[job.jobs[-1].uuid][1].output.internal_energies) / 1000.0, - np.array( - [ - 5058.441587914012, - 5385.880585798466, - 6765.198541655172, - 8723.78588089732, - 10919.019940938391, - ] - ) - / 1000.0, - 3, + assert_allclose( + responses[job.jobs[-1].uuid][1].output.internal_energies, + [5058.44158791, 5385.88058579, 6765.19854165, 8723.78588089, 10919.0199409], + rtol=0.05, ) diff --git a/tests/forcefields/test_jobs.py b/tests/forcefields/test_jobs.py index 74d80451fe..f78bd3a9d2 100644 --- a/tests/forcefields/test_jobs.py +++ b/tests/forcefields/test_jobs.py @@ -1,4 +1,6 @@ -from pytest import approx +from pytest import approx, importorskip + +importorskip("quippy") def test_chgnet_static_maker(si_structure): @@ -15,10 +17,10 @@ def test_chgnet_static_maker(si_structure): # run the flow or job and ensure that it finished running successfully responses = run_locally(job, ensure_success=True) - # validation the outputs of the job + # validate job outputs output1 = responses[job.uuid][1].output assert isinstance(output1, ForceFieldTaskDocument) - assert output1.output.energy == approx(-10.745277404785156, rel=1e-4) + assert output1.output.energy == approx(-10.7452, rel=1e-4) assert output1.output.ionic_steps[-1].magmoms is None assert output1.output.n_steps == 1 @@ -38,13 +40,11 @@ def test_chgnet_relax_maker(si_structure): # run the flow or job and ensure that it finished running successfully responses = run_locally(job, ensure_success=True) - # validating the outputs of the job + # validate job outputs output1 = responses[job.uuid][1].output assert isinstance(output1, ForceFieldTaskDocument) - assert output1.output.energy == approx(-10.745235443115234, rel=1e-4) - assert output1.output.ionic_steps[-1].magmoms[0] == approx( - 0.002112872898578644, rel=1e-4 - ) + assert output1.output.energy == approx(-10.74523544, rel=1e-4) + assert output1.output.ionic_steps[-1].magmoms[0] == approx(0.00211287, rel=1e-4) assert output1.output.n_steps == 12 @@ -62,10 +62,10 @@ def test_m3gnet_static_maker(si_structure): # run the flow or job and ensure that it finished running successfully responses = run_locally(job, ensure_success=True) - # validation the outputs of the job + # validate job outputs output1 = responses[job.uuid][1].output assert isinstance(output1, ForceFieldTaskDocument) - assert output1.output.energy == approx(-10.711267471313477, rel=1e-4) + assert output1.output.energy == approx(-10.8, abs=0.2) assert output1.output.n_steps == 1 @@ -84,10 +84,10 @@ def test_m3gnet_relax_maker(si_structure): # run the flow or job and ensure that it finished running successfully responses = run_locally(job, ensure_success=True) - # validating the outputs of the job + # validate job outputs output1 = responses[job.uuid][1].output assert isinstance(output1, ForceFieldTaskDocument) - assert output1.output.energy == approx(-10.710836410522461, rel=1e-4) + assert output1.output.energy == approx(-10.8, abs=0.2) assert output1.output.n_steps == 14 diff --git a/tests/test_data/lobster/lobsteroutputs/mp-2534/POTCAR.gz b/tests/test_data/lobster/lobsteroutputs/mp-2534/POTCAR.gz deleted file mode 100644 index 3d640ca34e..0000000000 Binary files a/tests/test_data/lobster/lobsteroutputs/mp-2534/POTCAR.gz and /dev/null differ diff --git a/tests/test_data/lobster/lobsteroutputs/mp-754354/POTCAR.gz b/tests/test_data/lobster/lobsteroutputs/mp-754354/POTCAR.gz deleted file mode 100644 index 5929fa2e0e..0000000000 Binary files a/tests/test_data/lobster/lobsteroutputs/mp-754354/POTCAR.gz and /dev/null differ diff --git a/tests/test_data/vasp/Si_hse_optics/hse_optics/inputs/INCAR b/tests/test_data/vasp/Si_hse_optics/hse_optics/inputs/INCAR index 24b0ac1bdc..9ac70e3177 100644 --- a/tests/test_data/vasp/Si_hse_optics/hse_optics/inputs/INCAR +++ b/tests/test_data/vasp/Si_hse_optics/hse_optics/inputs/INCAR @@ -1,4 +1,4 @@ -ALGO = All +ALGO = Normal EDIFF = 1e-05 ENAUG = 1360.0 ENCUT = 680.0 @@ -20,10 +20,12 @@ LVTOT = True LWAVE = False NBANDS = 11 NCORE = 4 -NEDOS = 5062 +NEDOS = 1265 NELM = 200 NELMIN = 5 NSW = 0 PREC = Accurate PRECFOCK = Fast SIGMA = 0.05 +CSHIFT = 1e-5 +LDAU = False diff --git a/tests/test_data/vasp/Si_hse_optics/hse_static/inputs/INCAR b/tests/test_data/vasp/Si_hse_optics/hse_static/inputs/INCAR index cbc162ec9a..4f8139f76b 100644 --- a/tests/test_data/vasp/Si_hse_optics/hse_static/inputs/INCAR +++ b/tests/test_data/vasp/Si_hse_optics/hse_static/inputs/INCAR @@ -1,4 +1,4 @@ -ALGO = All +ALGO = Normal EDIFF = 1e-05 ENAUG = 1360 ENCUT = 680 @@ -10,10 +10,11 @@ KSPACING = 0.5 LAECHG = True LASPH = True LCHARG = True -LELF = True +LELF = False LHFCALC = True LMIXTAU = True LORBIT = 11 +LDAU = False LREAL = False LVTOT = True LWAVE = False @@ -23,4 +24,4 @@ NELM = 200 NSW = 0 PREC = Accurate PRECFOCK = Fast -SIGMA = 0.05 +SIGMA = 0.2 diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/inputs/INCAR b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/inputs/INCAR new file mode 100644 index 0000000000..8368b4e030 --- /dev/null +++ b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/inputs/INCAR @@ -0,0 +1,17 @@ +ALGO = Fast +EDIFF = 0.0001 +ENCUT = 520 +IBRION = 2 +ISIF = 3 +ISMEAR = 0 +ISPIN = 2 +LASPH = True +LORBIT = 11 +LCHARG = False +LREAL = Auto +LWAVE = False +MAGMOM = 2*0.6 +NELM = 100 +NSW = 99 +PREC = Accurate +SIGMA = 0.2 diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/inputs/KPOINTS b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/inputs/KPOINTS new file mode 100644 index 0000000000..7f1ff746be --- /dev/null +++ b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/inputs/KPOINTS @@ -0,0 +1,4 @@ +pymatgen with grid density = 787 / number of atoms +0 +Gamma +7 7 7 diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/inputs/POSCAR b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/inputs/POSCAR new file mode 100644 index 0000000000..fcb5af8c91 --- /dev/null +++ b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/inputs/POSCAR @@ -0,0 +1,10 @@ +Si2 +1.0 + 3.3335729999999999 0.0000000000000000 1.9246390000000000 + 1.1111910000000000 3.1429239999999998 1.9246390000000000 + 0.0000000000000000 0.0000000000000000 3.8492780000000000 +Si +2 +direct + 0.8750000000000000 0.8750000000000000 0.8750000000000000 Si + 0.1250000000000000 0.1250000000000000 0.1250000000000000 Si diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/inputs/POTCAR.spec b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/inputs/POTCAR.spec new file mode 100644 index 0000000000..e267321d2c --- /dev/null +++ b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/inputs/POTCAR.spec @@ -0,0 +1 @@ +Si diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/CHGCAR b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/CHGCAR new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/CONTCAR.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/CONTCAR.gz new file mode 100644 index 0000000000..f132a3efc2 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/CONTCAR.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/DOSCAR.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/DOSCAR.gz new file mode 100644 index 0000000000..5d400ec26c Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/DOSCAR.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/EIGENVAL.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/EIGENVAL.gz new file mode 100644 index 0000000000..c1b419c7d3 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/EIGENVAL.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/IBZKPT.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/IBZKPT.gz new file mode 100644 index 0000000000..5d7141fbe7 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/IBZKPT.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/INCAR.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/INCAR.gz new file mode 100644 index 0000000000..62ebd1a136 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/INCAR.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/INCAR.orig.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/INCAR.orig.gz new file mode 100644 index 0000000000..c0106c2400 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/INCAR.orig.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/KPOINTS.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/KPOINTS.gz new file mode 100644 index 0000000000..f8cf2df94a Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/KPOINTS.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/KPOINTS.orig.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/KPOINTS.orig.gz new file mode 100644 index 0000000000..b5da720933 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/KPOINTS.orig.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/OSZICAR.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/OSZICAR.gz new file mode 100644 index 0000000000..f7f9d4bea8 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/OSZICAR.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/OUTCAR.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/OUTCAR.gz new file mode 100644 index 0000000000..09976dd2a0 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/OUTCAR.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/PCDAT.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/PCDAT.gz new file mode 100644 index 0000000000..ac5d893562 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/PCDAT.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/POSCAR.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/POSCAR.gz new file mode 100644 index 0000000000..afb71a324e Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/POSCAR.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/POSCAR.orig.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/POSCAR.orig.gz new file mode 100644 index 0000000000..1c53164bff Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/POSCAR.orig.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/POTCAR.orig.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/POTCAR.orig.gz new file mode 100644 index 0000000000..11c9113ace Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/POTCAR.orig.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/PROCAR.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/PROCAR.gz new file mode 100644 index 0000000000..2fcef42caa Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/PROCAR.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/REPORT.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/REPORT.gz new file mode 100644 index 0000000000..04dfd7cfb5 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/REPORT.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/WAVECAR b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/WAVECAR new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/XDATCAR.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/XDATCAR.gz new file mode 100644 index 0000000000..07830c5368 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/XDATCAR.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/custodian.json.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/custodian.json.gz new file mode 100644 index 0000000000..ed1e8debec Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/custodian.json.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/std_err.txt.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/std_err.txt.gz new file mode 100644 index 0000000000..d95e8e9bd9 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/std_err.txt.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/vasp.out.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/vasp.out.gz new file mode 100644 index 0000000000..89ed2c5341 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/vasp.out.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/vaspout.h5.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/vaspout.h5.gz new file mode 100644 index 0000000000..2b0da8d151 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/vaspout.h5.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/vasprun.xml.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/vasprun.xml.gz new file mode 100644 index 0000000000..4ea19dd753 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_1/outputs/vasprun.xml.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/inputs/INCAR b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/inputs/INCAR new file mode 100644 index 0000000000..0ea3ed916c --- /dev/null +++ b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/inputs/INCAR @@ -0,0 +1,17 @@ +ALGO = Fast +EDIFF = 0.0001 +ENCUT = 520.0 +IBRION = 2 +ISIF = 3 +ISMEAR = -5 +ISPIN = 2 +LASPH = True +LORBIT = 11 +LREAL = Auto +LWAVE = False +LCHARG = False +MAGMOM = 2*-0.0 +NELM = 100 +NSW = 99 +PREC = Accurate +SIGMA = 0.05 diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/inputs/KPOINTS b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/inputs/KPOINTS new file mode 100644 index 0000000000..34d32ff16d --- /dev/null +++ b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/inputs/KPOINTS @@ -0,0 +1,4 @@ +pymatgen with grid density = 776 / number of atoms +0 +Gamma +7 7 7 diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/inputs/POSCAR b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/inputs/POSCAR new file mode 100644 index 0000000000..51c423e318 --- /dev/null +++ b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/inputs/POSCAR @@ -0,0 +1,10 @@ +Si2 +1.0 + 3.3496744307422048 0.0000000000000000 1.9339351614767313 + 1.1165581435807350 3.1581045923790994 1.9339351614767313 + 0.0000000000000000 0.0000000000000000 3.8678703229534626 +Si +2 +direct + 0.8750000000000000 0.8750000000000000 0.8750000000000000 Si + 0.1250000000000000 0.1250000000000000 0.1250000000000000 Si diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/inputs/POTCAR.spec b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/inputs/POTCAR.spec new file mode 100644 index 0000000000..e267321d2c --- /dev/null +++ b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/inputs/POTCAR.spec @@ -0,0 +1 @@ +Si diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/CHGCAR b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/CHGCAR new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/CONTCAR.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/CONTCAR.gz new file mode 100644 index 0000000000..4d33f65526 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/CONTCAR.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/DOSCAR.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/DOSCAR.gz new file mode 100644 index 0000000000..aa046ffd7f Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/DOSCAR.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/EIGENVAL.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/EIGENVAL.gz new file mode 100644 index 0000000000..15e9abb170 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/EIGENVAL.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/IBZKPT.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/IBZKPT.gz new file mode 100644 index 0000000000..13a7b1d93f Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/IBZKPT.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/INCAR.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/INCAR.gz new file mode 100644 index 0000000000..f9c461407a Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/INCAR.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/INCAR.orig.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/INCAR.orig.gz new file mode 100644 index 0000000000..8564c1c48c Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/INCAR.orig.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/KPOINTS.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/KPOINTS.gz new file mode 100644 index 0000000000..dfe916e1da Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/KPOINTS.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/KPOINTS.orig.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/KPOINTS.orig.gz new file mode 100644 index 0000000000..3c21dd7f96 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/KPOINTS.orig.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/OSZICAR.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/OSZICAR.gz new file mode 100644 index 0000000000..0faeb76999 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/OSZICAR.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/OUTCAR.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/OUTCAR.gz new file mode 100644 index 0000000000..177193aa5c Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/OUTCAR.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/PCDAT.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/PCDAT.gz new file mode 100644 index 0000000000..75098b9574 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/PCDAT.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/POSCAR.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/POSCAR.gz new file mode 100644 index 0000000000..b4d1aaacb8 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/POSCAR.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/POSCAR.orig.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/POSCAR.orig.gz new file mode 100644 index 0000000000..8eeb8eb4ee Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/POSCAR.orig.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/POTCAR.orig.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/POTCAR.orig.gz new file mode 100644 index 0000000000..d2b241f5f6 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/POTCAR.orig.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/PROCAR.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/PROCAR.gz new file mode 100644 index 0000000000..a80572e949 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/PROCAR.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/REPORT.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/REPORT.gz new file mode 100644 index 0000000000..fa146a827d Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/REPORT.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/WAVECAR b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/WAVECAR new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/XDATCAR.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/XDATCAR.gz new file mode 100644 index 0000000000..41aa661541 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/XDATCAR.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/custodian.json.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/custodian.json.gz new file mode 100644 index 0000000000..4f9602ba20 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/custodian.json.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/std_err.txt.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/std_err.txt.gz new file mode 100644 index 0000000000..c670b6fd2d Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/std_err.txt.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/vasp.out.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/vasp.out.gz new file mode 100644 index 0000000000..0fb5046f6a Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/vasp.out.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/vaspout.h5.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/vaspout.h5.gz new file mode 100644 index 0000000000..102f3a0f78 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/vaspout.h5.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/vasprun.xml.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/vasprun.xml.gz new file mode 100644 index 0000000000..cb9a3ebb0b Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Relax_2/outputs/vasprun.xml.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/inputs/INCAR b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/inputs/INCAR new file mode 100644 index 0000000000..fbea5c5293 --- /dev/null +++ b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/inputs/INCAR @@ -0,0 +1,15 @@ +ALGO = Fast +EDIFF = 0.0001 +ENCUT = 520.0 +ISMEAR = -5 +ISPIN = 2 +LASPH = True +LORBIT = 11 +LREAL = False +LWAVE = False +LCHARG = True +MAGMOM = 2*0.0 +NELM = 100 +NSW = 0 +PREC = Accurate +SIGMA = 0.05 diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/inputs/KPOINTS b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/inputs/KPOINTS new file mode 100644 index 0000000000..34d32ff16d --- /dev/null +++ b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/inputs/KPOINTS @@ -0,0 +1,4 @@ +pymatgen with grid density = 776 / number of atoms +0 +Gamma +7 7 7 diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/inputs/POSCAR b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/inputs/POSCAR new file mode 100644 index 0000000000..51c423e318 --- /dev/null +++ b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/inputs/POSCAR @@ -0,0 +1,10 @@ +Si2 +1.0 + 3.3496744307422048 0.0000000000000000 1.9339351614767313 + 1.1165581435807350 3.1581045923790994 1.9339351614767313 + 0.0000000000000000 0.0000000000000000 3.8678703229534626 +Si +2 +direct + 0.8750000000000000 0.8750000000000000 0.8750000000000000 Si + 0.1250000000000000 0.1250000000000000 0.1250000000000000 Si diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/inputs/POTCAR.spec b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/inputs/POTCAR.spec new file mode 100644 index 0000000000..e267321d2c --- /dev/null +++ b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/inputs/POTCAR.spec @@ -0,0 +1 @@ +Si diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/CONTCAR.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/CONTCAR.gz new file mode 100644 index 0000000000..a3586bc133 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/CONTCAR.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/DOSCAR.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/DOSCAR.gz new file mode 100644 index 0000000000..415720c275 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/DOSCAR.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/EIGENVAL.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/EIGENVAL.gz new file mode 100644 index 0000000000..567f75e5e7 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/EIGENVAL.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/IBZKPT.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/IBZKPT.gz new file mode 100644 index 0000000000..746a58dedf Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/IBZKPT.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/INCAR.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/INCAR.gz new file mode 100644 index 0000000000..d8e970529d Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/INCAR.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/INCAR.orig.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/INCAR.orig.gz new file mode 100644 index 0000000000..39adbd8682 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/INCAR.orig.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/KPOINTS.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/KPOINTS.gz new file mode 100644 index 0000000000..432ee54ad1 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/KPOINTS.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/KPOINTS.orig.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/KPOINTS.orig.gz new file mode 100644 index 0000000000..3c4f8a5940 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/KPOINTS.orig.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/OSZICAR.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/OSZICAR.gz new file mode 100644 index 0000000000..96d59b459b Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/OSZICAR.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/OUTCAR.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/OUTCAR.gz new file mode 100644 index 0000000000..75308d6efa Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/OUTCAR.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/PCDAT.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/PCDAT.gz new file mode 100644 index 0000000000..06d8e87c10 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/PCDAT.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/POSCAR.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/POSCAR.gz new file mode 100644 index 0000000000..7133104b5f Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/POSCAR.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/POSCAR.orig.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/POSCAR.orig.gz new file mode 100644 index 0000000000..b11cf29719 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/POSCAR.orig.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/POTCAR.orig.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/POTCAR.orig.gz new file mode 100644 index 0000000000..b2d1508f7e Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/POTCAR.orig.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/PROCAR.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/PROCAR.gz new file mode 100644 index 0000000000..0d57adea0d Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/PROCAR.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/REPORT.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/REPORT.gz new file mode 100644 index 0000000000..6309621650 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/REPORT.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/XDATCAR.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/XDATCAR.gz new file mode 100644 index 0000000000..26126f098b Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/XDATCAR.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/custodian.json.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/custodian.json.gz new file mode 100644 index 0000000000..81fecd78a4 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/custodian.json.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/std_err.txt.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/std_err.txt.gz new file mode 100644 index 0000000000..19b32218b5 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/std_err.txt.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/vasp.out.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/vasp.out.gz new file mode 100644 index 0000000000..68867dca9a Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/vasp.out.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/vaspout.h5.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/vaspout.h5.gz new file mode 100644 index 0000000000..2c7bf72771 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/vaspout.h5.gz differ diff --git a/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/vasprun.xml.gz b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/vasprun.xml.gz new file mode 100644 index 0000000000..69ee06ca49 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_gga_relax/GGA_Static/outputs/vasprun.xml.gz differ diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/inputs/INCAR b/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/inputs/INCAR new file mode 100644 index 0000000000..f682301652 --- /dev/null +++ b/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/inputs/INCAR @@ -0,0 +1,25 @@ +ALGO = All +EDIFF = 1e-05 +EDIFFG = -0.05 +ENAUG = 1360.0 +ENCUT = 680.0 +GGA = Ps +IBRION = 2 +ISIF = 3 +ISMEAR = 0 +ISPIN = 2 +KSPACING = 0.22 +LAECHG = True +LASPH = True +LCHARG = True +LELF = False +LMIXTAU = True +LORBIT = 11 +LREAL = Auto +LVTOT = True +LWAVE = True +MAGMOM = 2*0.6 +NELM = 200 +NSW = 99 +PREC = Accurate +SIGMA = 0.2 diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/inputs/POSCAR b/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/inputs/POSCAR new file mode 100644 index 0000000000..fcb5af8c91 --- /dev/null +++ b/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/inputs/POSCAR @@ -0,0 +1,10 @@ +Si2 +1.0 + 3.3335729999999999 0.0000000000000000 1.9246390000000000 + 1.1111910000000000 3.1429239999999998 1.9246390000000000 + 0.0000000000000000 0.0000000000000000 3.8492780000000000 +Si +2 +direct + 0.8750000000000000 0.8750000000000000 0.8750000000000000 Si + 0.1250000000000000 0.1250000000000000 0.1250000000000000 Si diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/inputs/POTCAR.spec b/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/inputs/POTCAR.spec new file mode 100644 index 0000000000..e267321d2c --- /dev/null +++ b/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/inputs/POTCAR.spec @@ -0,0 +1 @@ +Si diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/outputs/CHGCAR b/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/outputs/CHGCAR new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/outputs/CONTCAR.gz b/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/outputs/CONTCAR.gz new file mode 100644 index 0000000000..927bf081ac Binary files /dev/null and b/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/outputs/CONTCAR.gz differ diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/outputs/DOSCAR.gz b/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/outputs/DOSCAR.gz new file mode 100644 index 0000000000..8fa5de6a99 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/outputs/DOSCAR.gz differ diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/outputs/INCAR.gz b/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/outputs/INCAR.gz new file mode 100644 index 0000000000..6e506b4916 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/outputs/INCAR.gz differ diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/outputs/INCAR.orig.gz b/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/outputs/INCAR.orig.gz new file mode 100644 index 0000000000..00fd364f47 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/outputs/INCAR.orig.gz differ diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/outputs/OUTCAR.gz b/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/outputs/OUTCAR.gz new file mode 100644 index 0000000000..6e91209428 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/outputs/OUTCAR.gz differ diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/outputs/PCDAT.gz b/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/outputs/PCDAT.gz new file mode 100644 index 0000000000..eaaea1716a Binary files /dev/null and b/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/outputs/PCDAT.gz differ diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/outputs/POSCAR.gz b/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/outputs/POSCAR.gz new file mode 100644 index 0000000000..9967c2c448 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/outputs/POSCAR.gz differ diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/outputs/POSCAR.orig.gz b/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/outputs/POSCAR.orig.gz new file mode 100644 index 0000000000..15a6a8cb69 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/outputs/POSCAR.orig.gz differ diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/outputs/POTCAR.orig.gz b/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/outputs/POTCAR.orig.gz new file mode 100644 index 0000000000..058d73c89e Binary files /dev/null and b/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/outputs/POTCAR.orig.gz differ diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/outputs/POTCAR.spec b/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/outputs/POTCAR.spec new file mode 100644 index 0000000000..e267321d2c --- /dev/null +++ b/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/outputs/POTCAR.spec @@ -0,0 +1 @@ +Si diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/outputs/WAVECAR b/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/outputs/WAVECAR new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/outputs/custodian.json.gz b/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/outputs/custodian.json.gz new file mode 100644 index 0000000000..dd9e0babbb Binary files /dev/null and b/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/outputs/custodian.json.gz differ diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/outputs/std_err.txt.gz b/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/outputs/std_err.txt.gz new file mode 100644 index 0000000000..32aa47ab25 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/outputs/std_err.txt.gz differ diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/outputs/vasprun.xml.gz b/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/outputs/vasprun.xml.gz new file mode 100644 index 0000000000..25e58cf3df Binary files /dev/null and b/tests/test_data/vasp/Si_mp_meta_gga_relax/pbesol_pre_relax/outputs/vasprun.xml.gz differ diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/inputs/INCAR b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/inputs/INCAR new file mode 100644 index 0000000000..48adb4a956 --- /dev/null +++ b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/inputs/INCAR @@ -0,0 +1,22 @@ +ALGO = Fast +EDIFF = 1e-05 +ENAUG = 1360.0 +ENCUT = 680.0 +ISMEAR = -5 +ISPIN = 2 +KSPACING = 0.29539340980039036 +LAECHG = True +LASPH = True +LCHARG = True +LELF = False +LMIXTAU = True +LORBIT = 11 +LREAL = False +LVTOT = True +LWAVE = False +MAGMOM = 2*0.0 +METAGGA = R2scan +NELM = 200 +NSW = 0 +PREC = Accurate +SIGMA = 0.05 diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/inputs/POSCAR b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/inputs/POSCAR new file mode 100644 index 0000000000..fcb5af8c91 --- /dev/null +++ b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/inputs/POSCAR @@ -0,0 +1,10 @@ +Si2 +1.0 + 3.3335729999999999 0.0000000000000000 1.9246390000000000 + 1.1111910000000000 3.1429239999999998 1.9246390000000000 + 0.0000000000000000 0.0000000000000000 3.8492780000000000 +Si +2 +direct + 0.8750000000000000 0.8750000000000000 0.8750000000000000 Si + 0.1250000000000000 0.1250000000000000 0.1250000000000000 Si diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/inputs/POTCAR.spec b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/inputs/POTCAR.spec new file mode 100644 index 0000000000..e267321d2c --- /dev/null +++ b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/inputs/POTCAR.spec @@ -0,0 +1 @@ +Si diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/outputs/CONTCAR.gz b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/outputs/CONTCAR.gz new file mode 100644 index 0000000000..46d84c72aa Binary files /dev/null and b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/outputs/CONTCAR.gz differ diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/outputs/DOSCAR.gz b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/outputs/DOSCAR.gz new file mode 100644 index 0000000000..f0df584e4f Binary files /dev/null and b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/outputs/DOSCAR.gz differ diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/outputs/INCAR.gz b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/outputs/INCAR.gz new file mode 100644 index 0000000000..a949748e60 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/outputs/INCAR.gz differ diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/outputs/INCAR.orig.gz b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/outputs/INCAR.orig.gz new file mode 100644 index 0000000000..21bc542b95 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/outputs/INCAR.orig.gz differ diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/outputs/OUTCAR.gz b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/outputs/OUTCAR.gz new file mode 100644 index 0000000000..ab4b9de01e Binary files /dev/null and b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/outputs/OUTCAR.gz differ diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/outputs/PCDAT.gz b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/outputs/PCDAT.gz new file mode 100644 index 0000000000..c3bb7a3be7 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/outputs/PCDAT.gz differ diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/outputs/POSCAR.gz b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/outputs/POSCAR.gz new file mode 100644 index 0000000000..1b9f23d6cd Binary files /dev/null and b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/outputs/POSCAR.gz differ diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/outputs/POSCAR.orig.gz b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/outputs/POSCAR.orig.gz new file mode 100644 index 0000000000..679fadbe24 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/outputs/POSCAR.orig.gz differ diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/outputs/POTCAR.orig.gz b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/outputs/POTCAR.orig.gz new file mode 100644 index 0000000000..889bba3fe7 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/outputs/POTCAR.orig.gz differ diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/outputs/POTCAR.spec b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/outputs/POTCAR.spec new file mode 100644 index 0000000000..e267321d2c --- /dev/null +++ b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/outputs/POTCAR.spec @@ -0,0 +1 @@ +Si diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/outputs/custodian.json.gz b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/outputs/custodian.json.gz new file mode 100644 index 0000000000..1272af75ea Binary files /dev/null and b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/outputs/custodian.json.gz differ diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/outputs/std_err.txt.gz b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/outputs/std_err.txt.gz new file mode 100644 index 0000000000..38edd14d6a Binary files /dev/null and b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/outputs/std_err.txt.gz differ diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/outputs/vasprun.xml.gz b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/outputs/vasprun.xml.gz new file mode 100644 index 0000000000..1ba052021c Binary files /dev/null and b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_final_static/outputs/vasprun.xml.gz differ diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/inputs/INCAR b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/inputs/INCAR new file mode 100644 index 0000000000..7a2c6583db --- /dev/null +++ b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/inputs/INCAR @@ -0,0 +1,25 @@ +ALGO = All +EDIFF = 1e-05 +EDIFFG = -0.02 +ENAUG = 1360.0 +ENCUT = 680.0 +IBRION = 2 +ISIF = 3 +ISMEAR = -5 +ISPIN = 2 +KSPACING = 0.28253269576667883 +LAECHG = True +LASPH = True +LCHARG = True +LELF = False +LMIXTAU = True +LORBIT = 11 +LREAL = Auto +LVTOT = True +LWAVE = True +MAGMOM = 2*0.0 +METAGGA = R2scan +NELM = 200 +NSW = 99 +PREC = Accurate +SIGMA = 0.05 diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/inputs/POSCAR b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/inputs/POSCAR new file mode 100644 index 0000000000..fcb5af8c91 --- /dev/null +++ b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/inputs/POSCAR @@ -0,0 +1,10 @@ +Si2 +1.0 + 3.3335729999999999 0.0000000000000000 1.9246390000000000 + 1.1111910000000000 3.1429239999999998 1.9246390000000000 + 0.0000000000000000 0.0000000000000000 3.8492780000000000 +Si +2 +direct + 0.8750000000000000 0.8750000000000000 0.8750000000000000 Si + 0.1250000000000000 0.1250000000000000 0.1250000000000000 Si diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/inputs/POTCAR.spec b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/inputs/POTCAR.spec new file mode 100644 index 0000000000..e267321d2c --- /dev/null +++ b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/inputs/POTCAR.spec @@ -0,0 +1 @@ +Si diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/outputs/CHGCAR b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/outputs/CHGCAR new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/outputs/CONTCAR.gz b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/outputs/CONTCAR.gz new file mode 100644 index 0000000000..7acb2e7a22 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/outputs/CONTCAR.gz differ diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/outputs/DOSCAR.gz b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/outputs/DOSCAR.gz new file mode 100644 index 0000000000..f8a27f90b1 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/outputs/DOSCAR.gz differ diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/outputs/FW.json.gz b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/outputs/FW.json.gz new file mode 100644 index 0000000000..fc5fd64e6c Binary files /dev/null and b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/outputs/FW.json.gz differ diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/outputs/INCAR.gz b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/outputs/INCAR.gz new file mode 100644 index 0000000000..bed1ca292f Binary files /dev/null and b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/outputs/INCAR.gz differ diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/outputs/INCAR.orig.gz b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/outputs/INCAR.orig.gz new file mode 100644 index 0000000000..a3c994ff50 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/outputs/INCAR.orig.gz differ diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/outputs/OUTCAR.gz b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/outputs/OUTCAR.gz new file mode 100644 index 0000000000..7c3346e936 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/outputs/OUTCAR.gz differ diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/outputs/PCDAT.gz b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/outputs/PCDAT.gz new file mode 100644 index 0000000000..da98cb9fae Binary files /dev/null and b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/outputs/PCDAT.gz differ diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/outputs/POSCAR.gz b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/outputs/POSCAR.gz new file mode 100644 index 0000000000..e1a1d3e30d Binary files /dev/null and b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/outputs/POSCAR.gz differ diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/outputs/POSCAR.orig.gz b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/outputs/POSCAR.orig.gz new file mode 100644 index 0000000000..5eaa906923 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/outputs/POSCAR.orig.gz differ diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/outputs/POTCAR.orig.gz b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/outputs/POTCAR.orig.gz new file mode 100644 index 0000000000..d83c70620c Binary files /dev/null and b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/outputs/POTCAR.orig.gz differ diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/outputs/POTCAR.spec b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/outputs/POTCAR.spec new file mode 100644 index 0000000000..e267321d2c --- /dev/null +++ b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/outputs/POTCAR.spec @@ -0,0 +1 @@ +Si diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/outputs/WAVECAR b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/outputs/WAVECAR new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/outputs/custodian.json.gz b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/outputs/custodian.json.gz new file mode 100644 index 0000000000..41f686f7a6 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/outputs/custodian.json.gz differ diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/outputs/std_err.txt.gz b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/outputs/std_err.txt.gz new file mode 100644 index 0000000000..01356fb606 Binary files /dev/null and b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/outputs/std_err.txt.gz differ diff --git a/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/outputs/vasprun.xml.gz b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/outputs/vasprun.xml.gz new file mode 100644 index 0000000000..ad6846684c Binary files /dev/null and b/tests/test_data/vasp/Si_mp_meta_gga_relax/r2scan_relax/outputs/vasprun.xml.gz differ diff --git a/tests/test_data/vasp/Si_old_double_relax/outputs/IBZKPT.relax1.gz b/tests/test_data/vasp/Si_old_double_relax/outputs/IBZKPT.relax1.gz deleted file mode 100644 index 669c8d41ba..0000000000 Binary files a/tests/test_data/vasp/Si_old_double_relax/outputs/IBZKPT.relax1.gz and /dev/null differ diff --git a/tests/test_data/vasp/Si_old_double_relax/outputs/IBZKPT.relax2.gz b/tests/test_data/vasp/Si_old_double_relax/outputs/IBZKPT.relax2.gz deleted file mode 100644 index ea0494365b..0000000000 Binary files a/tests/test_data/vasp/Si_old_double_relax/outputs/IBZKPT.relax2.gz and /dev/null differ diff --git a/tests/test_data/vasp/Si_old_double_relax/outputs/POTCAR.gz b/tests/test_data/vasp/Si_old_double_relax/outputs/POTCAR.gz index 9106d6a249..e69de29bb2 100644 Binary files a/tests/test_data/vasp/Si_old_double_relax/outputs/POTCAR.gz and b/tests/test_data/vasp/Si_old_double_relax/outputs/POTCAR.gz differ diff --git a/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/inputs/INCAR b/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/inputs/INCAR new file mode 100644 index 0000000000..fc405a62d1 --- /dev/null +++ b/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/inputs/INCAR @@ -0,0 +1,20 @@ +ALGO = Normal +EDIFF = 1e-05 +ENAUG = 1360 +ENCUT = 680 +GGA = Pe +ISMEAR = 0 +ISPIN = 2 +KSPACING = 0.22 +LAECHG = True +LASPH = True +LCHARG = True +LMAXMIX = 6 +LMIXTAU = True +LORBIT = 11 +LREAL = False +LWAVE = False +NELM = 200 +NSW = 0 +PREC = Accurate +SIGMA = 0.05 diff --git a/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/inputs/POSCAR b/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/inputs/POSCAR new file mode 100644 index 0000000000..41a38bcf0c --- /dev/null +++ b/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/inputs/POSCAR @@ -0,0 +1,10 @@ +Si2 +1.0 + 3.3488982826904379 0.0000000000000000 1.9334873250000004 + 1.1162994275634794 3.1573715802591895 1.9334873250000004 + 0.0000000000000000 0.0000000000000000 3.8669746500000000 +Si +2 +direct + 0.2500000000000000 0.2500000000000000 0.2500000000000000 Si + 0.0000000000000000 0.0000000000000000 0.0000000000000000 Si diff --git a/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/inputs/POTCAR.spec b/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/inputs/POTCAR.spec new file mode 100644 index 0000000000..e267321d2c --- /dev/null +++ b/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/inputs/POTCAR.spec @@ -0,0 +1 @@ +Si diff --git a/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/CONTCAR.gz b/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/CONTCAR.gz new file mode 100644 index 0000000000..a291cdaa45 Binary files /dev/null and b/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/CONTCAR.gz differ diff --git a/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/DOSCAR.gz b/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/DOSCAR.gz new file mode 100644 index 0000000000..1c8dbc6e84 Binary files /dev/null and b/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/DOSCAR.gz differ diff --git a/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/EIGENVAL.gz b/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/EIGENVAL.gz new file mode 100644 index 0000000000..e8ba0e6c9a Binary files /dev/null and b/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/EIGENVAL.gz differ diff --git a/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/IBZKPT.gz b/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/IBZKPT.gz new file mode 100644 index 0000000000..4a28403396 Binary files /dev/null and b/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/IBZKPT.gz differ diff --git a/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/INCAR.gz b/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/INCAR.gz new file mode 100644 index 0000000000..e194a98027 Binary files /dev/null and b/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/INCAR.gz differ diff --git a/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/INCAR.orig.gz b/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/INCAR.orig.gz new file mode 100644 index 0000000000..7231321d8a Binary files /dev/null and b/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/INCAR.orig.gz differ diff --git a/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/OSZICAR.gz b/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/OSZICAR.gz new file mode 100644 index 0000000000..b56624fc70 Binary files /dev/null and b/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/OSZICAR.gz differ diff --git a/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/OUTCAR.gz b/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/OUTCAR.gz new file mode 100644 index 0000000000..288d3c694e Binary files /dev/null and b/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/OUTCAR.gz differ diff --git a/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/PCDAT.gz b/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/PCDAT.gz new file mode 100644 index 0000000000..e37eb60809 Binary files /dev/null and b/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/PCDAT.gz differ diff --git a/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/POSCAR.gz b/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/POSCAR.gz new file mode 100644 index 0000000000..be4cb34257 Binary files /dev/null and b/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/POSCAR.gz differ diff --git a/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/POSCAR.orig.gz b/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/POSCAR.orig.gz new file mode 100644 index 0000000000..092a657813 Binary files /dev/null and b/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/POSCAR.orig.gz differ diff --git a/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/PROCAR.gz b/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/PROCAR.gz new file mode 100644 index 0000000000..e09bd22996 Binary files /dev/null and b/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/PROCAR.gz differ diff --git a/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/REPORT.gz b/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/REPORT.gz new file mode 100644 index 0000000000..7cffaa5a69 Binary files /dev/null and b/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/REPORT.gz differ diff --git a/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/WAVECAR.gz b/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/WAVECAR.gz new file mode 100644 index 0000000000..c9dd3a45eb Binary files /dev/null and b/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/WAVECAR.gz differ diff --git a/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/XDATCAR.gz b/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/XDATCAR.gz new file mode 100644 index 0000000000..ad61d95c91 Binary files /dev/null and b/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/XDATCAR.gz differ diff --git a/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/custodian.json.gz b/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/custodian.json.gz new file mode 100644 index 0000000000..38f34ee5b5 Binary files /dev/null and b/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/custodian.json.gz differ diff --git a/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/std_err.txt b/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/std_err.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/vasp.out.gz b/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/vasp.out.gz new file mode 100644 index 0000000000..ae1a9bb3ed Binary files /dev/null and b/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/vasp.out.gz differ diff --git a/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/vasprun.xml.gz b/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/vasprun.xml.gz new file mode 100644 index 0000000000..35c9aafcf3 Binary files /dev/null and b/tests/test_data/vasp/matpes_pbe_r2scan_flow/pbe_static/outputs/vasprun.xml.gz differ diff --git a/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/inputs/INCAR b/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/inputs/INCAR new file mode 100644 index 0000000000..53538e161a --- /dev/null +++ b/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/inputs/INCAR @@ -0,0 +1,20 @@ +ALGO = All +EDIFF = 1e-05 +ENAUG = 1360.0 +ENCUT = 680 +ISMEAR = 0 +ISPIN = 2 +KSPACING = 0.22 +LAECHG = True +LASPH = True +LCHARG = True +LMAXMIX = 6 +LMIXTAU = True +LORBIT = 11 +LREAL = False +LWAVE = False +METAGGA = R2scan +NELM = 200 +NSW = 0 +PREC = Accurate +SIGMA = 0.05 diff --git a/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/inputs/POSCAR b/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/inputs/POSCAR new file mode 100644 index 0000000000..41a38bcf0c --- /dev/null +++ b/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/inputs/POSCAR @@ -0,0 +1,10 @@ +Si2 +1.0 + 3.3488982826904379 0.0000000000000000 1.9334873250000004 + 1.1162994275634794 3.1573715802591895 1.9334873250000004 + 0.0000000000000000 0.0000000000000000 3.8669746500000000 +Si +2 +direct + 0.2500000000000000 0.2500000000000000 0.2500000000000000 Si + 0.0000000000000000 0.0000000000000000 0.0000000000000000 Si diff --git a/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/inputs/POTCAR.spec b/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/inputs/POTCAR.spec new file mode 100644 index 0000000000..e267321d2c --- /dev/null +++ b/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/inputs/POTCAR.spec @@ -0,0 +1 @@ +Si diff --git a/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/CONTCAR.gz b/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/CONTCAR.gz new file mode 100644 index 0000000000..0d7d802fbf Binary files /dev/null and b/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/CONTCAR.gz differ diff --git a/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/DOSCAR.gz b/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/DOSCAR.gz new file mode 100644 index 0000000000..ae48ca14ce Binary files /dev/null and b/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/DOSCAR.gz differ diff --git a/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/EIGENVAL.gz b/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/EIGENVAL.gz new file mode 100644 index 0000000000..28c6a9450c Binary files /dev/null and b/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/EIGENVAL.gz differ diff --git a/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/IBZKPT.gz b/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/IBZKPT.gz new file mode 100644 index 0000000000..d7f4651770 Binary files /dev/null and b/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/IBZKPT.gz differ diff --git a/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/INCAR.gz b/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/INCAR.gz new file mode 100644 index 0000000000..cafcc38eee Binary files /dev/null and b/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/INCAR.gz differ diff --git a/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/INCAR.orig.gz b/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/INCAR.orig.gz new file mode 100644 index 0000000000..eda590b230 Binary files /dev/null and b/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/INCAR.orig.gz differ diff --git a/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/OSZICAR.gz b/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/OSZICAR.gz new file mode 100644 index 0000000000..1ec4bc20ac Binary files /dev/null and b/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/OSZICAR.gz differ diff --git a/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/OUTCAR.gz b/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/OUTCAR.gz new file mode 100644 index 0000000000..e0ed6f8386 Binary files /dev/null and b/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/OUTCAR.gz differ diff --git a/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/PCDAT.gz b/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/PCDAT.gz new file mode 100644 index 0000000000..a082493eeb Binary files /dev/null and b/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/PCDAT.gz differ diff --git a/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/POSCAR.gz b/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/POSCAR.gz new file mode 100644 index 0000000000..5190cb285d Binary files /dev/null and b/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/POSCAR.gz differ diff --git a/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/POSCAR.orig.gz b/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/POSCAR.orig.gz new file mode 100644 index 0000000000..208f991a5d Binary files /dev/null and b/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/POSCAR.orig.gz differ diff --git a/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/PROCAR.gz b/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/PROCAR.gz new file mode 100644 index 0000000000..2b2bcf3c16 Binary files /dev/null and b/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/PROCAR.gz differ diff --git a/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/REPORT.gz b/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/REPORT.gz new file mode 100644 index 0000000000..20b5c3e295 Binary files /dev/null and b/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/REPORT.gz differ diff --git a/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/WAVECAR.gz b/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/WAVECAR.gz new file mode 100644 index 0000000000..dfab5905d6 Binary files /dev/null and b/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/WAVECAR.gz differ diff --git a/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/XDATCAR.gz b/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/XDATCAR.gz new file mode 100644 index 0000000000..63e83b71ca Binary files /dev/null and b/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/XDATCAR.gz differ diff --git a/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/custodian.json.gz b/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/custodian.json.gz new file mode 100644 index 0000000000..efae41aa18 Binary files /dev/null and b/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/custodian.json.gz differ diff --git a/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/std_err.txt b/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/std_err.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/vasp.out.gz b/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/vasp.out.gz new file mode 100644 index 0000000000..43fe8a6a1b Binary files /dev/null and b/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/vasp.out.gz differ diff --git a/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/vasprun.xml.gz b/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/vasprun.xml.gz new file mode 100644 index 0000000000..ad63b118dd Binary files /dev/null and b/tests/test_data/vasp/matpes_pbe_r2scan_flow/r2scan_static/outputs/vasprun.xml.gz differ diff --git a/tests/vasp/conftest.py b/tests/vasp/conftest.py index 9ddd9d8cdc..37d6d39a32 100644 --- a/tests/vasp/conftest.py +++ b/tests/vasp/conftest.py @@ -1,14 +1,20 @@ +from __future__ import annotations + import logging from pathlib import Path -from typing import Dict, Final, Literal, Sequence, Union +from typing import TYPE_CHECKING, Any, Callable, Final, Literal import pytest +from pytest import MonkeyPatch + +if TYPE_CHECKING: + from collections.abc import Generator, Sequence logger = logging.getLogger("atomate2") _VFILES: Final = ("incar", "kpoints", "potcar", "poscar") -_REF_PATHS: Dict[str, Union[str, Path]] = {} -_FAKE_RUN_VASP_KWARGS: Dict[str, dict] = {} +_REF_PATHS: dict[str, str | Path] = {} +_FAKE_RUN_VASP_KWARGS: dict[str, dict] = {} @pytest.fixture(scope="session") @@ -22,7 +28,9 @@ def lobster_test_dir(test_dir): @pytest.fixture() -def mock_vasp(monkeypatch, vasp_test_dir): +def mock_vasp( + monkeypatch: MonkeyPatch, vasp_test_dir: Path +) -> Generator[Callable[[Any, Any], Any], None, None]: """ This fixture allows one to mock (fake) running VASP. @@ -74,7 +82,14 @@ def mock_run_vasp(*args, **kwargs): from jobflow import CURRENT_JOB name = CURRENT_JOB.job.name - ref_path = vasp_test_dir / _REF_PATHS[name] + try: + ref_path = vasp_test_dir / _REF_PATHS[name] + except KeyError: + raise ValueError( + f"no reference directory found for job {name!r}; " + f"reference paths received={_REF_PATHS}" + ) from None + fake_run_vasp(ref_path, **_FAKE_RUN_VASP_KWARGS.get(name, {})) get_input_set_orig = VaspInputGenerator.get_input_set @@ -93,11 +108,8 @@ def mock_get_nelect(*_, **__): monkeypatch.setattr(VaspInputGenerator, "get_nelect", mock_get_nelect) def _run(ref_paths, fake_run_vasp_kwargs=None): - if fake_run_vasp_kwargs is None: - fake_run_vasp_kwargs = {} - _REF_PATHS.update(ref_paths) - _FAKE_RUN_VASP_KWARGS.update(fake_run_vasp_kwargs) + _FAKE_RUN_VASP_KWARGS.update(fake_run_vasp_kwargs or {}) yield _run @@ -108,7 +120,7 @@ def _run(ref_paths, fake_run_vasp_kwargs=None): def fake_run_vasp( ref_path: Path, - incar_settings: Sequence[str] = (), + incar_settings: Sequence[str] = None, check_inputs: Sequence[Literal["incar", "kpoints", "poscar", "potcar"]] = _VFILES, clear_inputs: bool = True, ): @@ -121,7 +133,8 @@ def fake_run_vasp( Path to reference directory with VASP input files in the folder named 'inputs' and output files in the folder named 'outputs'. incar_settings - A list of INCAR settings to check. + A list of INCAR settings to check. Defaults to None which checks all settings. + Empty list or tuple means no settings will be checked. check_inputs A list of vasp input files to check. Supported options are "incar", "kpoints", "poscar", "potcar", "wavecar". @@ -160,14 +173,17 @@ def fake_run_vasp( def check_incar(ref_path: Path, incar_settings: Sequence[str]): from pymatgen.io.vasp import Incar - user = Incar.from_file("INCAR") - ref = Incar.from_file(ref_path / "inputs" / "INCAR") + user_incar = Incar.from_file("INCAR") + ref_incar_path = ref_path / "inputs" / "INCAR" + ref_incar = Incar.from_file(ref_incar_path) defaults = {"ISPIN": 1, "ISMEAR": 1, "SIGMA": 0.2} - for p in incar_settings: - if user.get(p, defaults.get(p)) != ref.get(p, defaults.get(p)): + for key in list(user_incar) if incar_settings is None else incar_settings: + user_val = user_incar.get(key, defaults.get(key)) + ref_val = ref_incar.get(key, defaults.get(key)) + if user_val != ref_val: raise ValueError( - f"INCAR value of {p} is inconsistent: " - f"{user.get(p, defaults.get(p))} != {ref.get(p, defaults.get(p))}" + f"\n\nINCAR value of {key} is inconsistent: expected {ref_val}, " + f"got {user_val} \nin ref file {ref_incar_path}" ) @@ -188,18 +204,27 @@ def check_kpoints(ref_path: Path): "a KPOINTS file" ) if user_kpoints_exists and ref_kpoints_exists: - user = Kpoints.from_file("KPOINTS") - ref = Kpoints.from_file(ref_path / "inputs" / "KPOINTS") - if user.style != ref.style or user.num_kpts != ref.num_kpts: - raise ValueError("KPOINTS files are inconsistent") + user_kpts = Kpoints.from_file("KPOINTS") + ref_kpt_path = ref_path / "inputs" / "KPOINTS" + ref_kpts = Kpoints.from_file(ref_kpt_path) + if user_kpts.style != ref_kpts.style or user_kpts.num_kpts != ref_kpts.num_kpts: + raise ValueError( + f"\n\nKPOINTS files are inconsistent: {user_kpts.style} != " + f"{ref_kpts.style} or {user_kpts.num_kpts} != {ref_kpts.num_kpts}\nin " + f"ref file {ref_kpt_path}" + ) else: # check k-spacing - user = Incar.from_file("INCAR") - ref = Incar.from_file(ref_path / "inputs" / "INCAR") + user_incar = Incar.from_file("INCAR") + ref_incar_path = ref_path / "inputs" / "INCAR" + ref_incar = Incar.from_file(ref_incar_path) - user_ksp, ref_ksp = user.get("KSPACING"), ref.get("KSPACING") + user_ksp, ref_ksp = user_incar.get("KSPACING"), ref_incar.get("KSPACING") if user_ksp != ref_ksp: - raise ValueError(f"KSPACING is not consistent: {user_ksp} != {ref_ksp}") + raise ValueError( + f"\n\nKSPACING is inconsistent: expected {ref_ksp}, got {user_ksp} " + f"\nin ref file {ref_incar_path}" + ) def check_poscar(ref_path: Path): @@ -207,39 +232,47 @@ def check_poscar(ref_path: Path): from pymatgen.io.vasp import Poscar from pymatgen.util.coord import pbc_diff - user = Poscar.from_file("POSCAR") - ref = Poscar.from_file(ref_path / "inputs" / "POSCAR") + user_poscar = Poscar.from_file("POSCAR") + ref_poscar = Poscar.from_file(ref_path / "inputs" / "POSCAR") + user_frac_coords = user_poscar.structure.frac_coords + ref_frac_coords = ref_poscar.structure.frac_coords if ( - user.natoms != ref.natoms - or user.site_symbols != ref.site_symbols - or np.any( - np.abs(pbc_diff(user.structure.frac_coords, ref.structure.frac_coords)) - > 1e-3 - ) + user_poscar.natoms != ref_poscar.natoms + or user_poscar.site_symbols != ref_poscar.site_symbols + or np.any(np.abs(pbc_diff(user_frac_coords, ref_frac_coords)) > 1e-3) ): - raise ValueError("POSCAR files are inconsistent") + ref_poscar_path = ref_path / "inputs" / "POSCAR" + user_poscar_path = Path("POSCAR").absolute() + raise ValueError( + f"POSCAR files are inconsistent\n\n{ref_poscar_path!s}\n{ref_poscar}" + f"\n\n{user_poscar_path!s}\n{user_poscar}" + ) def check_potcar(ref_path: Path): from pymatgen.io.vasp import Potcar if Path(ref_path / "inputs" / "POTCAR").exists(): - ref = Potcar.from_file(ref_path / "inputs" / "POTCAR").symbols + ref_potcar = Potcar.from_file(ref_path / "inputs" / "POTCAR").symbols elif Path(ref_path / "inputs" / "POTCAR.spec").exists(): - ref = Path(ref_path / "inputs" / "POTCAR.spec").read_text().strip().split("\n") + ref_potcar = ( + Path(ref_path / "inputs" / "POTCAR.spec").read_text().strip().split("\n") + ) else: raise FileNotFoundError("no reference POTCAR or POTCAR.spec file found") if Path("POTCAR").exists(): - user = Potcar.from_file("POTCAR").symbols + user_potcar = Potcar.from_file("POTCAR").symbols elif Path("POTCAR.spec").exists(): - user = Path("POTCAR.spec").read_text().strip().split("\n") + user_potcar = Path("POTCAR.spec").read_text().strip().split("\n") else: raise FileNotFoundError("no POTCAR or POTCAR.spec file found") - if user != ref: - raise ValueError(f"POTCAR files are inconsistent: {user} != {ref}") + if user_potcar != ref_potcar: + raise ValueError( + f"POTCAR files are inconsistent: {user_potcar} != {ref_potcar}" + ) def clear_vasp_inputs(): diff --git a/tests/vasp/flows/test_defect.py b/tests/vasp/flows/test_defect.py index 6c2def79a3..a78fe897db 100644 --- a/tests/vasp/flows/test_defect.py +++ b/tests/vasp/flows/test_defect.py @@ -44,11 +44,7 @@ def test_ccd_maker(mock_vasp, clean_dir, test_dir): flow = ccd_maker.make(si_defect, charge_state1=0, charge_state2=1) # run the flow and ensure that it finished running successfully - responses = run_locally( - flow, - create_folders=True, - ensure_success=True, - ) + responses = run_locally(flow, create_folders=True, ensure_success=True) ccd: CCDDocument = responses[flow.jobs[-1].uuid][1].output @@ -166,7 +162,10 @@ def test_formation_energy_maker(mock_vasp, clean_dir, test_dir, monkeypatch): # rmaker = RelaxMaker(input_set_generator=ChargeStateRelaxSetGenerator()) maker = FormationEnergyMaker( - relax_radius="auto", perturb=0.1, collect_defect_entry_data=True + relax_radius="auto", + perturb=0.1, + collect_defect_entry_data=True, + validate_charge=False, ) flow = maker.make( defects[0], @@ -182,9 +181,8 @@ def test_formation_energy_maker(mock_vasp, clean_dir, test_dir, monkeypatch): ) def _check_plnr_locpot(name): - plnr_locpot = SETTINGS.JOB_STORE.query_one({"output.task_label": name})[ - "output" - ]["calcs_reversed"][0]["output"]["locpot"] + job = SETTINGS.JOB_STORE.query_one({"output.task_label": name}) + plnr_locpot = job["output"]["calcs_reversed"][0]["output"]["locpot"] assert set(plnr_locpot) == {"0", "1", "2"} for k in ref_paths: diff --git a/tests/vasp/flows/test_elastic.py b/tests/vasp/flows/test_elastic.py index b8956470f9..1a3448b8d4 100644 --- a/tests/vasp/flows/test_elastic.py +++ b/tests/vasp/flows/test_elastic.py @@ -1,6 +1,6 @@ def test_elastic(mock_vasp, clean_dir, si_structure): - import numpy as np from jobflow import run_locally + from numpy.testing import assert_allclose from pymatgen.symmetry.analyzer import SpacegroupAnalyzer from atomate2.common.schemas.elastic import ElasticDocument @@ -51,7 +51,7 @@ def test_elastic(mock_vasp, clean_dir, si_structure): # validation on the outputs elastic_output = responses[flow.jobs[-1].uuid][1].output assert isinstance(elastic_output, ElasticDocument) - assert np.allclose( + assert_allclose( elastic_output.elastic_tensor.ieee_format, [ [155.7923, 54.8871, 54.8871, 0.0, 0.0, 0.0], diff --git a/tests/vasp/flows/test_elph.py b/tests/vasp/flows/test_elph.py index 9f856c1993..9273cc7124 100644 --- a/tests/vasp/flows/test_elph.py +++ b/tests/vasp/flows/test_elph.py @@ -1,3 +1,6 @@ +import pytest + + def test_elph_renormalisation(mock_vasp, clean_dir, si_structure): from jobflow import run_locally @@ -65,8 +68,5 @@ def test_elph_renormalisation(mock_vasp, clean_dir, si_structure): # validation on the outputs renorm_output = responses[flow.output.uuid][1].output assert isinstance(renorm_output, ElectronPhononRenormalisationDoc) - assert set(renorm_output.delta_band_gaps) == { - -0.488900000000001, - -0.48850000000000104, - } + assert renorm_output.delta_band_gaps == pytest.approx([-0.4889, -0.4885], rel=1e-3) assert renorm_output.chemsys == "Si" diff --git a/tests/vasp/flows/test_matpes.py b/tests/vasp/flows/test_matpes.py new file mode 100644 index 0000000000..0df8f100d8 --- /dev/null +++ b/tests/vasp/flows/test_matpes.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +import pytest +from emmet.core.tasks import TaskDoc +from jobflow import run_locally +from pymatgen.core import Structure + +from atomate2.vasp.flows.matpes import MatPesGGAPlusMetaGGAStaticMaker + + +def test_matpes_gga_plus_meta_gga_static_maker(mock_vasp, clean_dir, vasp_test_dir): + # map from job name to directory containing reference output files + pre_relax_dir = "matpes_pbe_r2scan_flow/pbe_static" + ref_paths = { + "MatPES GGA static": pre_relax_dir, + "MatPES meta-GGA static": "matpes_pbe_r2scan_flow/r2scan_static", + } + si_struct = Structure.from_file(f"{vasp_test_dir}/{pre_relax_dir}/inputs/POSCAR") + + mock_vasp(ref_paths) + + # generate flow + flow = MatPesGGAPlusMetaGGAStaticMaker().make(si_struct) + + assert flow.name == "MatPES GGA plus meta-GGA static" + assert len(flow) == 2 + assert [job.name for job in flow] == list(ref_paths) + + # ensure flow runs successfully + responses = run_locally(flow, create_folders=True, ensure_success=True) + + # validate output + pbe_doc = responses[flow.jobs[0].uuid][1].output + r2scan_doc = responses[flow.jobs[-1].uuid][1].output + assert isinstance(r2scan_doc, TaskDoc) + assert r2scan_doc.output.energy == pytest.approx(-17.53895666) + assert r2scan_doc.output.bandgap == pytest.approx(0.8087999) + + assert isinstance(pbe_doc, TaskDoc) + assert pbe_doc.output.energy == pytest.approx(-10.84940729) + assert pbe_doc.output.bandgap == pytest.approx(0.6172, abs=1e-3) + + assert isinstance(flow.output, dict) + assert {*flow.output} == {"static1", "static2"} diff --git a/tests/vasp/flows/test_mp.py b/tests/vasp/flows/test_mp.py new file mode 100644 index 0000000000..8741959937 --- /dev/null +++ b/tests/vasp/flows/test_mp.py @@ -0,0 +1,139 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest +from pymatgen.core import Structure + +from atomate2.vasp.jobs.mp import ( + MPMetaGGARelaxMaker, + MPPreRelaxMaker, +) +from atomate2.vasp.sets.mp import MPMetaGGARelaxSetGenerator + +if TYPE_CHECKING: + from jobflow import Maker + + +@pytest.mark.parametrize("name", ["test", None]) +@pytest.mark.parametrize( + "relax_maker, static_maker", + [ + (MPPreRelaxMaker(), MPMetaGGARelaxMaker()), + (MPPreRelaxMaker(), None), + (None, MPMetaGGARelaxMaker()), + (None, None), # shouldn't raise without optional makers + ], +) +def test_mp_meta_gga_relax_custom_values( + name: str, relax_maker: Maker | None, static_maker: Maker | None +): + from atomate2.vasp.flows.mp import MPMetaGGADoubleRelaxStaticMaker + + kwargs = {} + if name: + kwargs["name"] = name + flow = MPMetaGGADoubleRelaxStaticMaker( + relax_maker=relax_maker, static_maker=static_maker, **kwargs + ) + assert isinstance(flow.relax_maker, type(relax_maker)) + if relax_maker: + assert flow.relax_maker.name == "MP pre-relax" + + assert isinstance(flow.static_maker, type(static_maker)) + if static_maker: + assert flow.static_maker.name == "MP meta-GGA relax" + + assert flow.name == (name or "MP meta-GGA relax") + + +def test_mp_meta_gga_double_relax_static(mock_vasp, clean_dir, vasp_test_dir): + from emmet.core.tasks import TaskDoc + from jobflow import run_locally + + from atomate2.vasp.flows.mp import MPMetaGGADoubleRelaxStaticMaker + + # map from job name to directory containing reference output files + pre_relax_dir = "Si_mp_meta_gga_relax/pbesol_pre_relax" + ref_paths = { + "MP pre-relax 1": pre_relax_dir, + "MP meta-GGA relax 2": "Si_mp_meta_gga_relax/r2scan_relax", + "MP meta-GGA static": "Si_mp_meta_gga_relax/r2scan_final_static", + } + si_struct = Structure.from_file(f"{vasp_test_dir}/{pre_relax_dir}/inputs/POSCAR") + + mock_vasp(ref_paths) + + # generate flow + flow = MPMetaGGADoubleRelaxStaticMaker( + relax_maker2=MPMetaGGARelaxMaker( + # TODO write better test for bandgap_tol since it isn't actually used by + # mock_vasp. this just tests it can be passed without error + input_set_generator=MPMetaGGARelaxSetGenerator(bandgap_tol=0.1) + ) + ).make(si_struct) + + # ensure flow runs successfully + responses = run_locally(flow, create_folders=True, ensure_success=True) + + # validate output + task_doc = responses[flow.jobs[-1].uuid][1].output + assert isinstance(task_doc, TaskDoc) + assert task_doc.output.energy == pytest.approx(-46.8613738) + + +def test_mp_gga_double_relax_static(mock_vasp, clean_dir, vasp_test_dir): + from emmet.core.tasks import TaskDoc + from jobflow import run_locally + + from atomate2.vasp.flows.mp import MPGGADoubleRelaxStaticMaker + + # map from job name to directory containing reference output files + pre_relax_dir = "Si_mp_gga_relax/GGA_Relax_1" + ref_paths = { + "MP GGA relax 1": pre_relax_dir, + "MP GGA relax 2": "Si_mp_gga_relax/GGA_Relax_2", + "MP GGA static": "Si_mp_gga_relax/GGA_Static", + } + si_struct = Structure.from_file(f"{vasp_test_dir}/{pre_relax_dir}/inputs/POSCAR") + + mock_vasp(ref_paths) + + # generate flow + flow = MPGGADoubleRelaxStaticMaker().make(si_struct) + + # ensure flow runs successfully + responses = run_locally(flow, create_folders=True, ensure_success=True) + + # validate output + task_doc = responses[flow.jobs[-1].uuid][1].output + assert isinstance(task_doc, TaskDoc) + assert task_doc.output.energy == pytest.approx(-10.84060922) + + +def test_mp_gga_double_relax(mock_vasp, clean_dir, vasp_test_dir): + from emmet.core.tasks import TaskDoc + from jobflow import run_locally + + from atomate2.vasp.flows.mp import MPGGADoubleRelaxMaker + + # map from job name to directory containing reference output files + pre_relax_dir = "Si_mp_gga_relax/GGA_Relax_1" + ref_paths = { + "MP GGA relax 1": pre_relax_dir, + "MP GGA relax 2": "Si_mp_gga_relax/GGA_Relax_2", + } + si_struct = Structure.from_file(f"{vasp_test_dir}/{pre_relax_dir}/inputs/POSCAR") + + mock_vasp(ref_paths) + + # generate flow + flow = MPGGADoubleRelaxMaker().make(si_struct) + + # ensure flow runs successfully + responses = run_locally(flow, create_folders=True, ensure_success=True) + + # validate output + task_doc = responses[flow.jobs[-1].uuid][1].output + assert isinstance(task_doc, TaskDoc) + assert task_doc.output.energy == pytest.approx(-10.84145656) diff --git a/tests/vasp/flows/test_phonons.py b/tests/vasp/flows/test_phonons.py index a441ac819a..1c794cc89f 100644 --- a/tests/vasp/flows/test_phonons.py +++ b/tests/vasp/flows/test_phonons.py @@ -1,5 +1,6 @@ import numpy as np import pytest +from numpy.testing import assert_allclose from pymatgen.core.structure import Structure from pymatgen.phonon.bandstructure import PhononBandStructureSymmLine from pymatgen.phonon.dos import PhononDos @@ -38,7 +39,6 @@ def test_phonon_wf_only_displacements3(mock_vasp, clean_dir): # automatically use fake VASP and write POTCAR.spec during the test mock_vasp(ref_paths, fake_run_vasp_kwargs) - # !!! Generate job job = PhononMaker( min_length=3.0, bulk_relax_maker=None, @@ -53,18 +53,12 @@ def test_phonon_wf_only_displacements3(mock_vasp, clean_dir): # run the flow or job and ensure that it finished running successfully responses = run_locally(job, create_folders=True, ensure_success=True) - # !!! validation on the outputs + # validate the outputs assert isinstance(responses[job.jobs[-1].uuid][1].output, PhononBSDOSDoc) - assert np.allclose( + assert_allclose( responses[job.jobs[-1].uuid][1].output.free_energies, - [ - 5774.603553771463, - 5616.334060911681, - 4724.766198084037, - 3044.208072582665, - 696.3373193497828, - ], + [5774.60355377, 5616.33406091, 4724.76619808, 3044.20807258, 696.337319349], ) assert isinstance( @@ -74,31 +68,26 @@ def test_phonon_wf_only_displacements3(mock_vasp, clean_dir): assert isinstance(responses[job.jobs[-1].uuid][1].output.phonon_dos, PhononDos) assert responses[job.jobs[-1].uuid][1].output.thermal_displacement_data is None assert isinstance(responses[job.jobs[-1].uuid][1].output.structure, Structure) - assert np.allclose( + assert_allclose( responses[job.jobs[-1].uuid][1].output.temperatures, [0, 100, 200, 300, 400] ) - assert np.allclose( - responses[job.jobs[-1].uuid][1].output.has_imaginary_modes, False - ) + assert responses[job.jobs[-1].uuid][1].output.has_imaginary_modes is False assert responses[job.jobs[-1].uuid][1].output.force_constants is None assert isinstance(responses[job.jobs[-1].uuid][1].output.jobdirs, PhononJobDirs) assert isinstance(responses[job.jobs[-1].uuid][1].output.uuids, PhononUUIDs) - assert np.isclose( + assert_allclose( responses[job.jobs[-1].uuid][1].output.total_dft_energy, -5.74555232 ) assert responses[job.jobs[-1].uuid][1].output.born is None assert responses[job.jobs[-1].uuid][1].output.epsilon_static is None - assert np.allclose( + assert_allclose( responses[job.jobs[-1].uuid][1].output.supercell_matrix, [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]], ) - assert np.allclose( + assert_allclose( responses[job.jobs[-1].uuid][1].output.primitive_matrix, - ( - (0, 0.5000000000000001, 0.5000000000000001), - (0.5000000000000001, 0.0, 0.5000000000000001), - (0.5000000000000001, 0.5000000000000001, 0.0), - ), + ((0, 0.5, 0.5), (0.5, 0, 0.5), (0.5, 0.5, 0)), + atol=1e-8, ) assert responses[job.jobs[-1].uuid][1].output.code == "vasp" assert isinstance( @@ -114,7 +103,7 @@ def test_phonon_wf_only_displacements3(mock_vasp, clean_dir): responses[job.jobs[-1].uuid][1].output.phonopy_settings.kpoint_density_dos == 7000 ) - assert np.allclose( + assert_allclose( responses[job.jobs[-1].uuid][1].output.entropies, [ 0.0, @@ -124,7 +113,7 @@ def test_phonon_wf_only_displacements3(mock_vasp, clean_dir): 26.39830736008501, ], ) - assert np.allclose( + assert_allclose( responses[job.jobs[-1].uuid][1].output.heat_capacities, [ 0.0, @@ -135,7 +124,7 @@ def test_phonon_wf_only_displacements3(mock_vasp, clean_dir): ], ) - assert np.allclose( + assert_allclose( responses[job.jobs[-1].uuid][1].output.internal_energies, [ 5774.603553771463, @@ -175,7 +164,6 @@ def test_phonon_wf_only_displacements_no_structural_transformation( # automatically use fake VASP and write POTCAR.spec during the test mock_vasp(ref_paths, fake_run_vasp_kwargs) - # !!! Generate job job = PhononMaker( min_length=3.0, bulk_relax_maker=None, @@ -190,10 +178,10 @@ def test_phonon_wf_only_displacements_no_structural_transformation( # run the flow or job and ensure that it finished running successfully responses = run_locally(job, create_folders=True, ensure_success=True) - # !!! validation on the outputs + # validate the outputs assert isinstance(responses[job.jobs[-1].uuid][1].output, PhononBSDOSDoc) - assert np.allclose( + assert_allclose( responses[job.jobs[-1].uuid][1].output.free_energies, [ 5774.566996471001, @@ -203,7 +191,7 @@ def test_phonon_wf_only_displacements_no_structural_transformation( 696.3435315493462, ], ) - assert np.allclose( + assert_allclose( responses[job.jobs[-1].uuid][1].output.entropies, [ 0.0, @@ -213,7 +201,7 @@ def test_phonon_wf_only_displacements_no_structural_transformation( 26.398072464162844, ], ) - assert np.allclose( + assert_allclose( responses[job.jobs[-1].uuid][1].output.heat_capacities, [ 0.0, @@ -224,7 +212,7 @@ def test_phonon_wf_only_displacements_no_structural_transformation( ], ) - assert np.allclose( + assert_allclose( responses[job.jobs[-1].uuid][1].output.internal_energies, [ 5774.566996471001, @@ -242,25 +230,23 @@ def test_phonon_wf_only_displacements_no_structural_transformation( assert isinstance(responses[job.jobs[-1].uuid][1].output.phonon_dos, PhononDos) assert responses[job.jobs[-1].uuid][1].output.thermal_displacement_data is None assert isinstance(responses[job.jobs[-1].uuid][1].output.structure, Structure) - assert np.allclose( + assert_allclose( responses[job.jobs[-1].uuid][1].output.temperatures, [0, 100, 200, 300, 400] ) - assert np.allclose( - responses[job.jobs[-1].uuid][1].output.has_imaginary_modes, False - ) + assert responses[job.jobs[-1].uuid][1].output.has_imaginary_modes is False assert responses[job.jobs[-1].uuid][1].output.force_constants is None assert isinstance(responses[job.jobs[-1].uuid][1].output.jobdirs, PhononJobDirs) assert isinstance(responses[job.jobs[-1].uuid][1].output.uuids, PhononUUIDs) - assert np.isclose( + assert_allclose( responses[job.jobs[-1].uuid][1].output.total_dft_energy, -5.74525804 ) assert responses[job.jobs[-1].uuid][1].output.born is None assert responses[job.jobs[-1].uuid][1].output.epsilon_static is None - assert np.allclose( + assert_allclose( responses[job.jobs[-1].uuid][1].output.supercell_matrix, ((-1.0, 1.0, 1.0), (1.0, -1.0, 1.0), (1.0, 1.0, -1.0)), ) - assert np.allclose( + assert_allclose( responses[job.jobs[-1].uuid][1].output.primitive_matrix, ( (1.0000000000000002, 0.0, 0.0), @@ -282,7 +268,7 @@ def test_phonon_wf_only_displacements_no_structural_transformation( responses[job.jobs[-1].uuid][1].output.phonopy_settings.kpoint_density_dos == 7000 ) - assert np.allclose( + assert_allclose( responses[job.jobs[-1].uuid][1].output.entropies, [ 0.0, @@ -292,7 +278,7 @@ def test_phonon_wf_only_displacements_no_structural_transformation( 26.398072464162844, ], ) - assert np.allclose( + assert_allclose( responses[job.jobs[-1].uuid][1].output.heat_capacities, [ 0.0, @@ -303,7 +289,7 @@ def test_phonon_wf_only_displacements_no_structural_transformation( ], ) - assert np.allclose( + assert_allclose( responses[job.jobs[-1].uuid][1].output.internal_energies, [ 5774.566996471001, @@ -317,9 +303,9 @@ def test_phonon_wf_only_displacements_no_structural_transformation( # this will test all kpath schemes in combination with primitive cell @pytest.mark.parametrize( - "kpathscheme", ["seekpath", "hinuma", "setyawan_curtarolo", "latimer_munro"] + "kpath_scheme", ["seekpath", "hinuma", "setyawan_curtarolo", "latimer_munro"] ) -def test_phonon_wf_only_displacements_kpath(mock_vasp, clean_dir, kpathscheme): +def test_phonon_wf_only_displacements_kpath(mock_vasp, clean_dir, kpath_scheme): from jobflow import run_locally structure = Structure( @@ -337,33 +323,27 @@ def test_phonon_wf_only_displacements_kpath(mock_vasp, clean_dir, kpathscheme): # automatically use fake VASP and write POTCAR.spec during the test mock_vasp(ref_paths, fake_run_vasp_kwargs) - # !!! Generate job job = PhononMaker( min_length=3.0, bulk_relax_maker=None, static_energy_maker=None, born_maker=None, use_symmetrized_structure="primitive", - kpath_scheme=kpathscheme, + kpath_scheme=kpath_scheme, generate_frequencies_eigenvectors_kwargs={"tstep": 100}, ).make(structure) # run the flow or job and ensure that it finished running successfully responses = run_locally(job, create_folders=True, ensure_success=True) - # !!! validation on the outputs + # validate the outputs # print(type(responses)) assert isinstance(responses[job.jobs[-1].uuid][1].output, PhononBSDOSDoc) - assert np.allclose( + assert_allclose( responses[job.jobs[-1].uuid][1].output.free_energies, - [ - 5776.1499503440455, - 5617.747377776762, - 4725.502693639196, - 3043.818276263367, - 694.4907835517783, - ], + [5776.14995034, 5617.74737777, 4725.50269363, 3043.81827626, 694.490783551], + atol=1e-3, ) assert isinstance( @@ -376,13 +356,11 @@ def test_phonon_wf_only_displacements_kpath(mock_vasp, clean_dir, kpathscheme): ThermalDisplacementData, ) assert isinstance(responses[job.jobs[-1].uuid][1].output.structure, Structure) - assert np.allclose( + assert_allclose( responses[job.jobs[-1].uuid][1].output.temperatures, [0, 100, 200, 300, 400] ) - assert np.allclose( - responses[job.jobs[-1].uuid][1].output.has_imaginary_modes, False - ) - assert np.isclose( + assert responses[job.jobs[-1].uuid][1].output.has_imaginary_modes is False + assert_allclose( responses[job.jobs[-1].uuid][1].output.force_constants.force_constants[0][0][0][ 0 ], @@ -393,13 +371,14 @@ def test_phonon_wf_only_displacements_kpath(mock_vasp, clean_dir, kpathscheme): assert responses[job.jobs[-1].uuid][1].output.total_dft_energy is None assert responses[job.jobs[-1].uuid][1].output.born is None assert responses[job.jobs[-1].uuid][1].output.epsilon_static is None - assert np.allclose( + assert_allclose( responses[job.jobs[-1].uuid][1].output.supercell_matrix, [[-1.0, 1.0, 1.0], [1.0, -1.0, 1.0], [1.0, 1.0, -1.0]], ) - assert np.allclose( + assert_allclose( responses[job.jobs[-1].uuid][1].output.primitive_matrix, [[1, 0, 0], [0, 1, 0], [0, 0, 1]], + atol=1e-8, ) assert responses[job.jobs[-1].uuid][1].output.code == "vasp" assert isinstance( @@ -409,7 +388,7 @@ def test_phonon_wf_only_displacements_kpath(mock_vasp, clean_dir, kpathscheme): assert responses[job.jobs[-1].uuid][1].output.phonopy_settings.npoints_band == 101 assert ( responses[job.jobs[-1].uuid][1].output.phonopy_settings.kpath_scheme - == kpathscheme + == kpath_scheme ) assert ( responses[job.jobs[-1].uuid][1].output.phonopy_settings.kpoint_density_dos @@ -442,14 +421,14 @@ def test_phonon_wf_only_displacements_add_inputs_raises(mock_vasp, clean_dir): [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.1]], ] epsilon_static = [ - [5.25, 0.0, 0.0], - [0.0, 5.25, -0.0], - [0.0, 0.0, 5.25], + [5.25, 0, 0], + [0, 5.25, -0], + [0, 0, 5.25], ] total_dft_energy_per_formula_unit = -5 job = PhononMaker( - min_length=3.0, + min_length=3, bulk_relax_maker=None, static_energy_maker=None, born_maker=None, @@ -485,17 +464,17 @@ def test_phonon_wf_only_displacements_add_inputs(mock_vasp, clean_dir): mock_vasp(ref_paths, fake_run_vasp_kwargs) born = [ - [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]], - [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]], + [[0.0, 0, 0], [0, 0, 0], [0, 0, 0]], + [[0, 0, 0], [0, 0, 0], [0, 0, 0]], ] epsilon_static = [ - [5.25, 0.0, 0.0], - [0.0, 5.25, -0.0], - [0.0, 0.0, 5.25], + [5.25, 0, 0], + [0, 5.25, -0], + [0, 0, 5.25], ] total_dft_energy_per_formula_unit = -5 job = PhononMaker( - min_length=3.0, + min_length=3, bulk_relax_maker=None, static_energy_maker=None, born_maker=None, @@ -511,19 +490,14 @@ def test_phonon_wf_only_displacements_add_inputs(mock_vasp, clean_dir): # run the flow or job and ensure that it finished running successfully responses = run_locally(job, create_folders=True, ensure_success=True) - # !!! validation on the outputs + # validate the outputs # print(type(responses)) assert isinstance(responses[job.jobs[-1].uuid][1].output, PhononBSDOSDoc) - assert np.allclose( + assert_allclose( responses[job.jobs[-1].uuid][1].output.free_energies, - [ - 5776.1499503440455, - 5617.747377776762, - 4725.502693639196, - 3043.818276263367, - 694.4907835517783, - ], + [5776.14995034, 5617.74737777, 4725.50269363, 3043.81827626, 694.490783551], + atol=1e-3, ) assert isinstance( @@ -536,13 +510,11 @@ def test_phonon_wf_only_displacements_add_inputs(mock_vasp, clean_dir): ThermalDisplacementData, ) assert isinstance(responses[job.jobs[-1].uuid][1].output.structure, Structure) - assert np.allclose( + assert_allclose( responses[job.jobs[-1].uuid][1].output.temperatures, [0, 100, 200, 300, 400] ) - assert np.allclose( - responses[job.jobs[-1].uuid][1].output.has_imaginary_modes, False - ) - assert np.isclose( + assert responses[job.jobs[-1].uuid][1].output.has_imaginary_modes is False + assert_allclose( responses[job.jobs[-1].uuid][1].output.force_constants.force_constants[0][0][0][ 0 ], @@ -550,21 +522,22 @@ def test_phonon_wf_only_displacements_add_inputs(mock_vasp, clean_dir): ) assert isinstance(responses[job.jobs[-1].uuid][1].output.jobdirs, PhononJobDirs) assert isinstance(responses[job.jobs[-1].uuid][1].output.uuids, PhononUUIDs) - assert np.allclose(responses[job.jobs[-1].uuid][1].output.born, born) - assert np.isclose( + assert_allclose(responses[job.jobs[-1].uuid][1].output.born, born) + assert_allclose( responses[job.jobs[-1].uuid][1].output.total_dft_energy, total_dft_energy_per_formula_unit, ) - assert np.allclose( - responses[job.jobs[-1].uuid][1].output.epsilon_static, epsilon_static + assert_allclose( + responses[job.jobs[-1].uuid][1].output.epsilon_static, epsilon_static, atol=1e-8 ) - assert np.allclose( + assert_allclose( responses[job.jobs[-1].uuid][1].output.supercell_matrix, - [[-1.0, 1.0, 1.0], [1.0, -1.0, 1.0], [1.0, 1.0, -1.0]], + [[-1, 1, 1], [1, -1, 1], [1, 1, -1]], ) - assert np.allclose( + assert_allclose( responses[job.jobs[-1].uuid][1].output.primitive_matrix, [[1, 0, 0], [0, 1, 0], [0, 0, 1]], + atol=1e-8, ) assert responses[job.jobs[-1].uuid][1].output.code == "vasp" assert isinstance( @@ -601,9 +574,8 @@ def test_phonon_wf_only_displacements_optional_settings(mock_vasp, clean_dir): # automatically use fake VASP and write POTCAR.spec during the test mock_vasp(ref_paths, fake_run_vasp_kwargs) - # !!! Generate job job = PhononMaker( - min_length=3.0, + min_length=3, bulk_relax_maker=None, static_energy_maker=None, born_maker=None, @@ -617,49 +589,29 @@ def test_phonon_wf_only_displacements_optional_settings(mock_vasp, clean_dir): # run the flow or job and ensure that it finished running successfully responses = run_locally(job, create_folders=True, ensure_success=True) - # !!! validation on the outputs + # validate the outputs assert isinstance(responses[job.jobs[-1].uuid][1].output, PhononBSDOSDoc) - assert np.allclose( + assert_allclose( responses[job.jobs[-1].uuid][1].output.free_energies, - [ - 5776.1499503440455, - 5617.747377776762, - 4725.502693639196, - 3043.818276263367, - 694.4907835517783, - ], + [5776.14995034, 5617.74737777, 4725.50269363, 3043.81827626, 694.490783551], + atol=1e-3, ) - assert np.allclose( + assert_allclose( responses[job.jobs[-1].uuid][1].output.entropies, - [ - 0.0, - 4.790668183967239, - 13.034706214127153, - 20.374002842560785, - 26.414254898744545, - ], + [0, 4.79066818396, 13.0347062141, 20.3740028425, 26.4142548987], + atol=1e-8, ) - assert np.allclose( + assert_allclose( responses[job.jobs[-1].uuid][1].output.heat_capacities, - [ - 0.0, - 8.053736263830405, - 15.980056690395037, - 19.980312349314378, - 21.885134767453195, - ], + [0.0, 8.05373626383, 15.9800566903, 19.9803123493, 21.8851347674], + atol=1e-8, ) - assert np.allclose( + assert_allclose( responses[job.jobs[-1].uuid][1].output.internal_energies, - [ - 5776.1499503440455, - 6096.814195199836, - 7332.443935293648, - 9156.019127569401, - 11260.192741251365, - ], + [5776.14995034, 6096.81419519, 7332.44393529, 9156.01912756, 11260.1927412], + atol=1e-8, ) assert isinstance( @@ -669,25 +621,24 @@ def test_phonon_wf_only_displacements_optional_settings(mock_vasp, clean_dir): assert isinstance(responses[job.jobs[-1].uuid][1].output.phonon_dos, PhononDos) assert responses[job.jobs[-1].uuid][1].output.thermal_displacement_data is None assert isinstance(responses[job.jobs[-1].uuid][1].output.structure, Structure) - assert np.allclose( + assert_allclose( responses[job.jobs[-1].uuid][1].output.temperatures, [0, 100, 200, 300, 400] ) - assert np.allclose( - responses[job.jobs[-1].uuid][1].output.has_imaginary_modes, False - ) + assert responses[job.jobs[-1].uuid][1].output.has_imaginary_modes is False assert responses[job.jobs[-1].uuid][1].output.force_constants is None assert isinstance(responses[job.jobs[-1].uuid][1].output.jobdirs, PhononJobDirs) assert isinstance(responses[job.jobs[-1].uuid][1].output.uuids, PhononUUIDs) assert responses[job.jobs[-1].uuid][1].output.total_dft_energy is None assert responses[job.jobs[-1].uuid][1].output.born is None assert responses[job.jobs[-1].uuid][1].output.epsilon_static is None - assert np.allclose( + assert_allclose( responses[job.jobs[-1].uuid][1].output.supercell_matrix, [[-1.0, 1.0, 1.0], [1.0, -1.0, 1.0], [1.0, 1.0, -1.0]], ) - assert np.allclose( + assert_allclose( responses[job.jobs[-1].uuid][1].output.primitive_matrix, [[1, 0, 0], [0, 1, 0], [0, 0, 1]], + atol=1e-8, ) assert responses[job.jobs[-1].uuid][1].output.code == "vasp" assert isinstance( @@ -745,10 +696,10 @@ def test_phonon_wf_all_steps(mock_vasp, clean_dir): # run the flow or job and ensure that it finished running successfully responses = run_locally(job, create_folders=True, ensure_success=True) - # !!! validation on the outputs + # validate the outputs assert isinstance(responses[job.jobs[-1].uuid][1].output, PhononBSDOSDoc) - assert np.allclose( + assert_allclose( responses[job.jobs[-1].uuid][1].output.free_energies, [ 5853.741503991992, @@ -769,13 +720,11 @@ def test_phonon_wf_all_steps(mock_vasp, clean_dir): ThermalDisplacementData, ) assert isinstance(responses[job.jobs[-1].uuid][1].output.structure, Structure) - assert np.allclose( + assert_allclose( responses[job.jobs[-1].uuid][1].output.temperatures, [0, 100, 200, 300, 400] ) - assert np.allclose( - responses[job.jobs[-1].uuid][1].output.has_imaginary_modes, False - ) - assert np.isclose( + assert responses[job.jobs[-1].uuid][1].output.has_imaginary_modes is False + assert_allclose( responses[job.jobs[-1].uuid][1].output.force_constants.force_constants[0][0][0][ 0 ], @@ -783,22 +732,20 @@ def test_phonon_wf_all_steps(mock_vasp, clean_dir): ) assert isinstance(responses[job.jobs[-1].uuid][1].output.jobdirs, PhononJobDirs) assert isinstance(responses[job.jobs[-1].uuid][1].output.uuids, PhononUUIDs) - assert np.allclose( - responses[job.jobs[-1].uuid][1].output.born, - [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]], - ) - assert np.isclose( + assert_allclose(responses[job.jobs[-1].uuid][1].output.born, np.zeros((2, 3, 3))) + assert_allclose( responses[job.jobs[-1].uuid][1].output.total_dft_energy, -5.746290585 ) - assert np.allclose( + assert_allclose( responses[job.jobs[-1].uuid][1].output.epsilon_static, ((13.19242034, -0.0, 0.0), (-0.0, 13.19242034, 0.0), (0.0, 0.0, 13.19242034)), + atol=1e-8, ) - assert np.allclose( + assert_allclose( responses[job.jobs[-1].uuid][1].output.supercell_matrix, [[-1.0, 1.0, 1.0], [1.0, -1.0, 1.0], [1.0, 1.0, -1.0]], ) - assert np.allclose( + assert_allclose( responses[job.jobs[-1].uuid][1].output.primitive_matrix, [[1, 0, 0], [0, 1, 0], [0, 0, 1]], ) @@ -827,10 +774,10 @@ def test_phonon_wf_all_steps(mock_vasp, clean_dir): # combined with non-standard-primitive structures # this will test all kpath schemes in combination with primitive cell @pytest.mark.parametrize( - "kpathscheme", ["hinuma", "setyawan_curtarolo", "latimer_munro"] + "kpath_scheme", ["hinuma", "setyawan_curtarolo", "latimer_munro"] ) def test_phonon_wf_only_displacements_kpath_raises_no_cell_change( - mock_vasp, clean_dir, kpathscheme + mock_vasp, clean_dir, kpath_scheme ): structure = Structure( lattice=[[0, 2.73, 2.73], [2.73, 0, 2.73], [2.73, 2.73, 0]], @@ -854,15 +801,15 @@ def test_phonon_wf_only_displacements_kpath_raises_no_cell_change( static_energy_maker=None, born_maker=None, use_symmetrized_structure=None, - kpath_scheme=kpathscheme, + kpath_scheme=kpath_scheme, generate_frequencies_eigenvectors_kwargs={"tstep": 100}, ).make(structure) @pytest.mark.parametrize( - "kpathscheme", ["hinuma", "setyawan_curtarolo", "latimer_munro"] + "kpath_scheme", ["hinuma", "setyawan_curtarolo", "latimer_munro"] ) -def test_phonon_wf_only_displacements_kpath_raises(mock_vasp, clean_dir, kpathscheme): +def test_phonon_wf_only_displacements_kpath_raises(mock_vasp, clean_dir, kpath_scheme): structure = Structure( lattice=[[0, 2.73, 2.73], [2.73, 0, 2.73], [2.73, 2.73, 0]], species=["Si", "Si"], @@ -878,19 +825,18 @@ def test_phonon_wf_only_displacements_kpath_raises(mock_vasp, clean_dir, kpathsc # automatically use fake VASP and write POTCAR.spec during the test mock_vasp(ref_paths, fake_run_vasp_kwargs) with pytest.raises(ValueError): - # !!! Generate job PhononMaker( min_length=3.0, bulk_relax_maker=None, static_energy_maker=None, born_maker=None, use_symmetrized_structure="conventional", - kpath_scheme=kpathscheme, + kpath_scheme=kpath_scheme, generate_frequencies_eigenvectors_kwargs={"tstep": 100}, ).make(structure) -def test_phonon_wf_all_steps_NaCl(mock_vasp, clean_dir): +def test_phonon_wf_all_steps_na_cl(mock_vasp, clean_dir): from jobflow import run_locally from pymatgen.core.structure import Structure @@ -938,9 +884,9 @@ def test_phonon_wf_all_steps_NaCl(mock_vasp, clean_dir): # run the job responses = run_locally(phonon_flow, create_folders=True, ensure_success=True) - # !!! validation on the outputs + # validate the outputs assert isinstance(responses[phonon_flow.jobs[-1].uuid][1].output, PhononBSDOSDoc) - assert np.allclose( + assert_allclose( responses[phonon_flow.jobs[-1].uuid][1].output.born, [ ((1.1033, 0.0, 0.0), (0.0, 1.1033, -0.0), (0.0, 0.0, 1.1033)), @@ -948,7 +894,7 @@ def test_phonon_wf_all_steps_NaCl(mock_vasp, clean_dir): ], ) - def test_phonon_wf_all_steps_NaCl(mock_vasp, clean_dir): + def test_phonon_wf_all_steps_na_cl(mock_vasp, clean_dir): from jobflow import run_locally from pymatgen.core.structure import Structure @@ -1014,7 +960,7 @@ def test_phonon_wf_all_steps_NaCl(mock_vasp, clean_dir): # run the job responses = run_locally(phonon_flow, create_folders=True, ensure_success=True) - # !!! validation on the outputs + # validate the outputs assert isinstance( responses[phonon_flow.jobs[-1].uuid][1].output, PhononBSDOSDoc ) diff --git a/tests/vasp/jobs/test_core.py b/tests/vasp/jobs/test_core.py index a2012a6677..084f00321c 100644 --- a/tests/vasp/jobs/test_core.py +++ b/tests/vasp/jobs/test_core.py @@ -1,3 +1,4 @@ +from numpy.testing import assert_allclose from pytest import approx @@ -29,7 +30,7 @@ def test_static_maker(mock_vasp, clean_dir, si_structure): # run the flow or job and ensure that it finished running successfully responses = run_locally(job, create_folders=True, ensure_success=True) - # validation the outputs of the job + # validate job outputs output1 = responses[job.uuid][1].output assert isinstance(output1, TaskDoc) assert output1.output.energy == approx(-10.85037078) @@ -61,7 +62,7 @@ def test_relax_maker(mock_vasp, clean_dir, si_structure): # run the flow or job and ensure that it finished running successfully responses = run_locally(job, create_folders=True, ensure_success=True) - # validation the outputs of the job + # validate job outputs output1 = responses[job.uuid][1].output assert isinstance(output1, TaskDoc) assert output1.output.energy == approx(-10.85043620) @@ -70,7 +71,6 @@ def test_relax_maker(mock_vasp, clean_dir, si_structure): def test_dielectric(mock_vasp, clean_dir, si_structure): - import numpy as np from jobflow import run_locally from atomate2.vasp.jobs.core import DielectricMaker @@ -93,12 +93,12 @@ def test_dielectric(mock_vasp, clean_dir, si_structure): # Additional validation on the outputs of the job output1 = responses[job.uuid][1].output - assert np.allclose( + assert_allclose( output1.calcs_reversed[0].output.epsilon_static, [[11.41539467, 0, 0], [0, 11.41539963, 0], [0, 0, 11.41539866]], atol=0.01, ) - assert np.allclose( + assert_allclose( output1.calcs_reversed[0].output.epsilon_ionic, [[0, 0, 0], [0, 0, 0], [0, 0, 0]], atol=0.01, @@ -159,7 +159,7 @@ def test_hse_static_maker(mock_vasp, clean_dir, si_structure): # run the flow or job and ensure that it finished running successfully responses = run_locally(job, create_folders=True, ensure_success=True) - # validation the outputs of the job + # validate job outputs output1 = responses[job.uuid][1].output assert isinstance(output1, TaskDoc) assert output1.output.energy == approx(-12.52887403) @@ -236,8 +236,8 @@ def test_molecular_dynamics(mock_vasp, clean_dir, si_structure): # generate job job = MDMaker().make(si_structure) - NSW = 3 - job.maker.input_set_generator.user_incar_settings["NSW"] = NSW + nsw = 3 + job.maker.input_set_generator.user_incar_settings["NSW"] = nsw # run the flow or job and ensure that it finished running successfully responses = run_locally(job, create_folders=True, ensure_success=True) @@ -251,7 +251,7 @@ def test_molecular_dynamics(mock_vasp, clean_dir, si_structure): # check ionic steps stored as pymatgen Trajectory assert output1.calcs_reversed[0].output.ionic_steps is None traj = output1.vasp_objects[VaspObject.TRAJECTORY] - assert len(traj.frame_properties) == NSW + assert len(traj.frame_properties) == nsw # simply check a frame property can be converted to an IonicStep for frame in traj.frame_properties: IonicStep(**frame) diff --git a/tests/vasp/jobs/test_matpes.py b/tests/vasp/jobs/test_matpes.py new file mode 100644 index 0000000000..8619228a34 --- /dev/null +++ b/tests/vasp/jobs/test_matpes.py @@ -0,0 +1,130 @@ +import pytest +from emmet.core.tasks import TaskDoc +from jobflow import run_locally +from pymatgen.core import Structure + +from atomate2.vasp.jobs.base import BaseVaspMaker +from atomate2.vasp.jobs.matpes import MatPesGGAStaticMaker, MatPesMetaGGAStaticMaker +from atomate2.vasp.sets.matpes import ( + MatPesGGAStaticSetGenerator, + MatPesMetaGGAStaticSetGenerator, +) + +expected_incar = { + "ALGO": "Normal", + "EDIFF": 1e-05, + "ENAUG": 1360, + "ENCUT": 680, + "GGA": "PE", + "ISMEAR": 0, + "ISPIN": 2, + "KSPACING": 0.22, + "LAECHG": True, + "LASPH": True, + "LCHARG": True, + "LMIXTAU": True, + "LORBIT": 11, + "LREAL": False, + "LWAVE": False, + "NELM": 200, + "NSW": 0, + "PREC": "Accurate", + "SIGMA": 0.05, + "LMAXMIX": 6, + "LDAU": False, + "LDAUJ": { + "F": {"Co": 0, "Cr": 0, "Fe": 0, "Mn": 0, "Mo": 0, "Ni": 0, "V": 0, "W": 0}, + "O": {"Co": 0, "Cr": 0, "Fe": 0, "Mn": 0, "Mo": 0, "Ni": 0, "V": 0, "W": 0}, + }, + "LDAUL": { + "F": {"Co": 2, "Cr": 2, "Fe": 2, "Mn": 2, "Mo": 2, "Ni": 2, "V": 2, "W": 2}, + "O": {"Co": 2, "Cr": 2, "Fe": 2, "Mn": 2, "Mo": 2, "Ni": 2, "V": 2, "W": 2}, + }, + "LDAUTYPE": 2, + "LDAUU": { + "F": { + "Co": 3.32, + "Cr": 3.7, + "Fe": 5.3, + "Mn": 3.9, + "Mo": 4.38, + "Ni": 6.2, + "V": 3.25, + "W": 6.2, + }, + "O": { + "Co": 3.32, + "Cr": 3.7, + "Fe": 5.3, + "Mn": 3.9, + "Mo": 4.38, + "Ni": 6.2, + "V": 3.25, + "W": 6.2, + }, + }, +} + + +@pytest.mark.parametrize("maker_cls", [MatPesGGAStaticMaker, MatPesMetaGGAStaticMaker]) +def test_matpes_static_maker_default_values(maker_cls: BaseVaspMaker): + maker = maker_cls() + is_meta = "Meta" in maker_cls.__name__ + assert maker.name == f"MatPES {'meta-' if is_meta else ''}GGA static" + assert isinstance( + maker.input_set_generator, + MatPesMetaGGAStaticSetGenerator if is_meta else MatPesGGAStaticSetGenerator, + ) + config = maker.input_set_generator.config_dict + assert {*config} == {"INCAR", "POTCAR", "PARENT", "POTCAR_FUNCTIONAL"} + assert config["INCAR"] == expected_incar + + +def test_matpes_gga_static_maker(mock_vasp, clean_dir, vasp_test_dir): + # map from job name to directory containing reference input/output files + ref_paths = {"MatPES GGA static": "matpes_pbe_r2scan_flow/pbe_static"} + si_struct = Structure.from_file( + f"{vasp_test_dir}/matpes_pbe_r2scan_flow/pbe_static/inputs/POSCAR" + ) + + # settings passed to fake_run_vasp; adjust these to check for certain INCAR settings + fake_run_vasp_kwargs = {key: {"incar_settings": []} for key in ref_paths} + + mock_vasp(ref_paths, fake_run_vasp_kwargs) + + # generate flow + job = MatPesGGAStaticMaker().make(si_struct) + + # ensure flow runs successfully + responses = run_locally(job, create_folders=True, ensure_success=True) + + # validate output + task_doc = responses[job.uuid][1].output + assert isinstance(task_doc, TaskDoc) + assert task_doc.output.energy == pytest.approx(-10.84940729) + + +def test_matpes_meta_gga_static_maker(mock_vasp, clean_dir, vasp_test_dir): + # map from job name to directory containing reference input/output files + ref_paths = {"MatPES meta-GGA static": "matpes_pbe_r2scan_flow/r2scan_static"} + si_struct = Structure.from_file( + f"{vasp_test_dir}/matpes_pbe_r2scan_flow/r2scan_static/inputs/POSCAR" + ) + + # settings passed to fake_run_vasp; adjust these to check for certain INCAR settings + fake_run_vasp_kwargs = { + key: {"incar_settings": ["GGA", "METAGGA", "ALGO"]} for key in ref_paths + } + + mock_vasp(ref_paths, fake_run_vasp_kwargs) + + # generate flow + job = MatPesMetaGGAStaticMaker().make(si_struct) + + # ensure flow runs successfully + responses = run_locally(job, create_folders=True, ensure_success=True) + + # validate output + task_doc = responses[job.uuid][1].output + assert isinstance(task_doc, TaskDoc) + assert task_doc.output.energy == pytest.approx(-17.53895667) diff --git a/tests/vasp/jobs/test_mp.py b/tests/vasp/jobs/test_mp.py new file mode 100644 index 0000000000..dc21655d61 --- /dev/null +++ b/tests/vasp/jobs/test_mp.py @@ -0,0 +1,136 @@ +import pytest +from pymatgen.core import Structure + +from atomate2.vasp.jobs.mp import ( + MPGGARelaxMaker, + MPMetaGGARelaxMaker, + MPMetaGGAStaticMaker, + MPPreRelaxMaker, +) +from atomate2.vasp.sets.mp import MPMetaGGARelaxSetGenerator + +expected_incar = { + "ISIF": 3, + "IBRION": 2, + "NSW": 99, + "ISMEAR": 0, + "SIGMA": 0.05, + "LREAL": "Auto", + "LWAVE": False, + "LCHARG": True, + "EDIFF": 1e-05, + "EDIFFG": -0.02, +} + + +def test_mp_pre_relax_maker_default_values(): + maker = MPPreRelaxMaker() + assert maker.name == "MP pre-relax" + assert {*maker.input_set_generator.config_dict} >= {"INCAR", "POTCAR"} + for key, expected in expected_incar.items(): + actual = maker.input_set_generator.config_dict["INCAR"][key] + assert actual == expected, f"{key=}, {actual=}, {expected=}" + + +def test_mp_meta_gga_relax_maker_default_values(): + maker = MPMetaGGARelaxMaker() + assert maker.name == "MP meta-GGA relax" + assert {*maker.input_set_generator.config_dict} >= {"INCAR", "POTCAR"} + for key, expected in expected_incar.items(): + actual = maker.input_set_generator.config_dict["INCAR"][key] + assert actual == expected, f"{key=}, {actual=}, {expected=}" + + +def test_mp_meta_gga_static_maker(mock_vasp, clean_dir, vasp_test_dir): + from emmet.core.tasks import TaskDoc + from jobflow import run_locally + + # map from job name to directory containing reference output files + ref_paths = { + "MP meta-GGA static": "Si_mp_meta_gga_relax/r2scan_final_static", + } + si_struct = Structure.from_file( + f"{vasp_test_dir}/Si_mp_meta_gga_relax/r2scan_final_static/inputs/POSCAR" + ) + + # settings passed to fake_run_vasp; adjust these to check for certain INCAR settings + fake_run_vasp_kwargs = {key: {"incar_settings": []} for key in ref_paths} + + mock_vasp(ref_paths, fake_run_vasp_kwargs) + + # generate flow + job = MPMetaGGAStaticMaker( + input_set_generator=MPMetaGGARelaxSetGenerator(auto_kspacing=0.8249) + ).make(si_struct) + + # ensure flow runs successfully + responses = run_locally(job, create_folders=True, ensure_success=True) + + # validate output + task_doc = responses[job.uuid][1].output + assert isinstance(task_doc, TaskDoc) + assert task_doc.output.energy == pytest.approx(-46.8613738) + + +def test_mp_meta_gga_relax_maker(mock_vasp, clean_dir, vasp_test_dir): + from emmet.core.tasks import TaskDoc + from jobflow import run_locally + + # map from job name to directory containing reference output files + ref_paths = { + "MP meta-GGA relax": "Si_mp_meta_gga_relax/r2scan_relax", + } + si_struct = Structure.from_file( + f"{vasp_test_dir}/Si_mp_meta_gga_relax/r2scan_final_static/inputs/POSCAR" + ) + + # settings passed to fake_run_vasp; adjust these to check for certain INCAR settings + fake_run_vasp_kwargs = { + key: {"incar_settings": ["LWAVE", "LCHARG"]} for key in ref_paths + } + + mock_vasp(ref_paths, fake_run_vasp_kwargs) + + # generate flow + job = MPMetaGGARelaxMaker( + input_set_generator=MPMetaGGARelaxSetGenerator(auto_kspacing=0.4786) + ).make(si_struct) + + # ensure flow runs successfully + responses = run_locally(job, create_folders=True, ensure_success=True) + + # validate output + task_doc = responses[job.uuid][1].output + assert isinstance(task_doc, TaskDoc) + assert task_doc.output.energy == pytest.approx(-46.86703814) + + +def test_mp_gga_relax_maker(mock_vasp, clean_dir, vasp_test_dir): + from emmet.core.tasks import TaskDoc + from jobflow import run_locally + + # map from job name to directory containing reference output files + ref_paths = { + "MP GGA relax": "Si_mp_gga_relax/GGA_Relax_1", + } + si_struct = Structure.from_file( + f"{vasp_test_dir}/Si_mp_gga_relax/GGA_Relax_1/inputs/POSCAR" + ) + + # settings passed to fake_run_vasp; adjust these to check for certain INCAR settings + fake_run_vasp_kwargs = { + key: {"incar_settings": ["LWAVE", "LCHARG"]} for key in ref_paths + } + + mock_vasp(ref_paths, fake_run_vasp_kwargs) + + # generate flow + job = MPGGARelaxMaker().make(si_struct) + + # ensure flow runs successfully + responses = run_locally(job, create_folders=True, ensure_success=True) + + # validate output + task_doc = responses[job.uuid][1].output + assert isinstance(task_doc, TaskDoc) + assert task_doc.output.energy == pytest.approx(-10.84140641) diff --git a/tests/vasp/lobster/conftest.py b/tests/vasp/lobster/conftest.py index fbd892dfa2..e6c629db65 100644 --- a/tests/vasp/lobster/conftest.py +++ b/tests/vasp/lobster/conftest.py @@ -1,9 +1,14 @@ +from __future__ import annotations + import logging from pathlib import Path -from typing import Literal, Sequence, Union +from typing import TYPE_CHECKING, Literal import pytest +if TYPE_CHECKING: + from collections.abc import Sequence + logger = logging.getLogger("atomate2") _LFILES = "lobsterin" @@ -81,7 +86,7 @@ def _run(ref_paths, fake_run_lobster_kwargs): def fake_run_lobster( - ref_path: Union[str, Path], + ref_path: str | Path, check_lobster_inputs: Sequence[Literal["lobsterin"]] = _LFILES, check_dft_inputs: Sequence[Literal["WAVECAR", "POSCAR"]] = _DFT_FILES, lobsterin_settings: Sequence[str] = (), @@ -117,7 +122,7 @@ def fake_run_lobster( logger.info("ran fake LOBSTER, generated outputs") -def verify_inputs(ref_path: Union[str, Path], lobsterin_settings: Sequence[str]): +def verify_inputs(ref_path: str | Path, lobsterin_settings: Sequence[str]): from pymatgen.io.lobster import Lobsterin user = Lobsterin.from_file("lobsterin") @@ -130,7 +135,7 @@ def verify_inputs(ref_path: Union[str, Path], lobsterin_settings: Sequence[str]) raise ValueError(f"lobsterin value of {p} is inconsistent!") -def copy_lobster_outputs(ref_path: Union[str, Path]): +def copy_lobster_outputs(ref_path: str | Path): import shutil output_path = ref_path / "outputs" diff --git a/tests/vasp/lobster/flows/test_lobster.py b/tests/vasp/lobster/flows/test_lobster.py index 72e61325ad..6ba1a8136f 100644 --- a/tests/vasp/lobster/flows/test_lobster.py +++ b/tests/vasp/lobster/flows/test_lobster.py @@ -8,7 +8,7 @@ from atomate2.vasp.powerups import update_user_incar_settings -def test_lobsteruniformmaker(mock_vasp, mock_lobster, clean_dir, memory_jobstore): +def test_lobster_uniform_maker(mock_vasp, mock_lobster, clean_dir, memory_jobstore): # mapping from job name to directory containing test files ref_paths = { "relax 1": "Si_lobster_uniform/relax_1", diff --git a/tests/vasp/lobster/schemas/test_lobster.py b/tests/vasp/lobster/schemas/test_lobster.py index b353e0f7c0..38f8bcfb93 100644 --- a/tests/vasp/lobster/schemas/test_lobster.py +++ b/tests/vasp/lobster/schemas/test_lobster.py @@ -1,10 +1,9 @@ -def test_LobsterTaskDocument(lobster_test_dir): +def test_lobster_task_document(lobster_test_dir): """ Test the CCDDocument schema, this test needs to be placed here since we are using the VASP TaskDocuments for testing. """ - - import numpy as np + from numpy.testing import assert_allclose from pymatgen.core.structure import Structure from pymatgen.electronic_structure.cohp import Cohp, CompleteCohp from pymatgen.electronic_structure.dos import LobsterCompleteDos @@ -21,53 +20,53 @@ def test_LobsterTaskDocument(lobster_test_dir): ) assert isinstance(doc.structure, Structure) assert isinstance(doc.lobsterout, LobsteroutModel) - assert np.isclose(doc.lobsterout.charge_spilling[0], 0.009899999999999999) + assert_allclose(doc.lobsterout.charge_spilling[0], 0.009899999999999999) assert isinstance(doc.lobsterin, LobsterinModel) - assert np.isclose(doc.lobsterin.cohpstartenergy, -5) + assert_allclose(doc.lobsterin.cohpstartenergy, -5) assert isinstance(doc.strongest_bonds_icohp, StrongestBonds) - assert np.isclose( + assert_allclose( doc.strongest_bonds_icohp.strongest_bonds["As-Ga"]["ICOHP"], -4.32971 ) - assert np.isclose( + assert_allclose( doc.strongest_bonds_icobi.strongest_bonds["As-Ga"]["ICOBI"], 0.82707 ) - assert np.isclose( + assert_allclose( doc.strongest_bonds_icoop.strongest_bonds["As-Ga"]["ICOOP"], 0.31405 ) - assert np.isclose( + assert_allclose( doc.strongest_bonds_icohp.strongest_bonds["As-Ga"]["length"], 2.4899 ) - assert np.isclose( + assert_allclose( doc.strongest_bonds_icobi.strongest_bonds["As-Ga"]["length"], 2.4899 ) - assert np.isclose( + assert_allclose( doc.strongest_bonds_icoop.strongest_bonds["As-Ga"]["length"], 2.4899 ) assert doc.strongest_bonds_icoop.which_bonds == "all" assert doc.strongest_bonds_icohp.which_bonds == "all" assert doc.strongest_bonds_icobi.which_bonds == "all" - assert np.isclose( + assert_allclose( doc.strongest_bonds_icohp_cation_anion.strongest_bonds["As-Ga"]["ICOHP"], -4.32971, ) - assert np.isclose( + assert_allclose( doc.strongest_bonds_icobi_cation_anion.strongest_bonds["As-Ga"]["ICOBI"], 0.82707, ) - assert np.isclose( + assert_allclose( doc.strongest_bonds_icoop_cation_anion.strongest_bonds["As-Ga"]["ICOOP"], 0.31405, ) - assert np.isclose( + assert_allclose( doc.strongest_bonds_icohp_cation_anion.strongest_bonds["As-Ga"]["length"], 2.4899, ) - assert np.isclose( + assert_allclose( doc.strongest_bonds_icobi_cation_anion.strongest_bonds["As-Ga"]["length"], 2.4899, ) - assert np.isclose( + assert_allclose( doc.strongest_bonds_icoop_cation_anion.strongest_bonds["As-Ga"]["length"], 2.4899, ) @@ -88,25 +87,25 @@ def test_LobsterTaskDocument(lobster_test_dir): assert isinstance(doc.cobi_data, CompleteCohp) assert isinstance(doc.coop_data, CompleteCohp) assert isinstance(doc.dos, LobsterCompleteDos) - assert np.isclose(doc.madelung_energies["Mulliken"], -0.68) - assert np.allclose( + assert_allclose(doc.madelung_energies["Mulliken"], -0.68) + assert_allclose( doc.site_potentials["Mulliken"], [-1.26, -1.27, -1.26, -1.27, 1.27, 1.27, 1.26, 1.26], rtol=1e-2, ) - assert np.isclose(doc.site_potentials["Ewald_splitting"], 3.14) + assert_allclose(doc.site_potentials["Ewald_splitting"], 3.14) assert len(doc.gross_populations) == 8 assert doc.gross_populations[5]["element"] == "As" - expected_gross_popp = { + expected_gross_pop = { "4s": 1.38, "4p_y": 1.18, "4p_z": 1.18, "4p_x": 1.18, "total": 4.93, } - gross_popp_here = doc.gross_populations[5]["Loewdin GP"] - assert expected_gross_popp == gross_popp_here - assert np.allclose( + gross_pop_here = doc.gross_populations[5]["Loewdin GP"] + assert expected_gross_pop == gross_pop_here + assert_allclose( doc.charges["Mulliken"], [0.13, 0.13, 0.13, 0.13, -0.13, -0.13, -0.13, -0.13], rtol=1e-2, @@ -118,50 +117,27 @@ def test_LobsterTaskDocument(lobster_test_dir): doc2 = LobsterTaskDocument.from_directory( dir_name=lobster_test_dir / "lobsteroutputs/mp-754354", save_cohp_plots=False ) - assert np.isclose( + assert_allclose( doc2.strongest_bonds_icohp.strongest_bonds["Ba-O"]["ICOHP"], -0.55689 ) - assert np.isclose( + assert_allclose( doc2.strongest_bonds_icohp.strongest_bonds["Ba-F"]["ICOHP"], -0.44806 ) assert len(doc2.band_overlaps["1"]) + len(doc2.band_overlaps["-1"]) == 2 - assert np.allclose( + assert_allclose( doc2.site_potentials["Loewdin"], - [ - -15.09, - -15.09, - -15.09, - -15.09, - -15.09, - -15.09, - -15.09, - -15.09, - 14.78, - 14.78, - 8.14, - 8.14, - 8.48, - 8.48, - 8.14, - 8.14, - 8.14, - 8.14, - 8.48, - 8.48, - 8.14, - 8.14, - ], + [*[-15.09] * 8, 14.78, 14.78, *[8.14, 8.14, 8.48, 8.48, 8.14, 8.14] * 2], rtol=1e-2, ) - assert np.isclose(doc2.site_potentials["Ewald_splitting"], 3.14) + assert_allclose(doc2.site_potentials["Ewald_splitting"], 3.14) assert len(doc2.gross_populations) == 22 assert doc2.gross_populations[10]["element"] == "F" - expected_gross_popp = { + expected_gross_pop = { "2s": 1.98, "2p_y": 1.97, "2p_z": 1.97, "2p_x": 1.97, "total": 7.88, } - gross_popp_here = doc2.gross_populations[10]["Mulliken GP"] - assert expected_gross_popp == gross_popp_here + gross_pop_here = doc2.gross_populations[10]["Mulliken GP"] + assert expected_gross_pop == gross_pop_here diff --git a/tests/vasp/schemas/test_defect.py b/tests/vasp/schemas/test_defect.py index 0c06e2b415..72897e84ba 100644 --- a/tests/vasp/schemas/test_defect.py +++ b/tests/vasp/schemas/test_defect.py @@ -1,4 +1,4 @@ -def test_CCDDocument(vasp_test_dir): +def test_ccd_document(vasp_test_dir): """ Test the CCDDocument schema, this test needs to be placed here since we are using the VASP TaskDocuments for testing. @@ -34,25 +34,25 @@ def is_strict_minimum(min_index, arr): for task, sdir in zip(static_tasks2, static_dirs2) ] - inputdict = defaultdict(list) + input_dict = defaultdict(list) for s, e, sdir in inputs1: - inputdict["structures1"].append(s) - inputdict["energies1"].append(e) - inputdict["static_dirs1"].append(sdir) - inputdict["static_uuids1"].append(sdir) + input_dict["structures1"].append(s) + input_dict["energies1"].append(e) + input_dict["static_dirs1"].append(sdir) + input_dict["static_uuids1"].append(sdir) for s, e, sdir in inputs2: - inputdict["structures2"].append(s) - inputdict["energies2"].append(e) - inputdict["static_dirs2"].append(sdir) - inputdict["static_uuids2"].append(sdir) + input_dict["structures2"].append(s) + input_dict["energies2"].append(e) + input_dict["static_dirs2"].append(sdir) + input_dict["static_uuids2"].append(sdir) - inputdict["relaxed_uuid1"] = static_dirs1[2] - inputdict["relaxed_uuid2"] = static_dirs2[2] + input_dict["relaxed_uuid1"] = static_dirs1[2] + input_dict["relaxed_uuid2"] = static_dirs2[2] ccd_doc = CCDDocument.from_task_outputs( - **inputdict, + **input_dict, ) # create the CCD document diff --git a/tests/vasp/test_sets.py b/tests/vasp/test_sets.py index b7ead03ace..c9cecb0c63 100644 --- a/tests/vasp/test_sets.py +++ b/tests/vasp/test_sets.py @@ -2,6 +2,7 @@ from pymatgen.core import Lattice, Species, Structure from atomate2.vasp.sets.core import StaticSetGenerator +from atomate2.vasp.sets.mp import MPMetaGGARelaxSetGenerator @pytest.fixture(scope="module") @@ -35,6 +36,16 @@ def struct_with_magmoms(struct_no_magmoms) -> Structure: return struct +@pytest.fixture(scope="module") +def struct_no_u_params() -> Structure: + """Dummy SiO structure with no anticipated +U corrections""" + return Structure( + lattice=Lattice.cubic(3), + species=["Si", "O"], + coords=[[0, 0, 0], [0.5, 0.5, 0.5]], + ) + + def test_user_incar_settings(): structure = Structure([[1, 0, 0], [0, 1, 0], [0, 0, 1]], ["H"], [[0, 0, 0]]) @@ -44,8 +55,8 @@ def test_user_incar_settings(): "ALGO": "VeryFast", "EDIFF": 1e-30, "EDIFFG": -1e-10, - "ENAUG": 20000, - "ENCUT": 15000, + "ENAUG": 20_000, + "ENCUT": 15_000, "GGA": "PE", "IBRION": 1, "ISIF": 1, @@ -59,7 +70,7 @@ def test_user_incar_settings(): "MAGMOM": {"H": 100}, "NELM": 5, "NELMIN": 10, # should not be greater than NELM - "NSW": 5000, + "NSW": 5_000, "PREC": 10, # wrong type, should be string. "SIGMA": 20, } @@ -119,3 +130,61 @@ def test_incar_magmoms_precedence(structure, user_incar_settings, request) -> No input_gen.config_dict["INCAR"]["MAGMOM"].get(str(s), 0.6) for s in structure.species ] + + +@pytest.mark.parametrize("structure", ["struct_no_magmoms", "struct_no_u_params"]) +def test_set_u_params(structure, request) -> None: + structure = request.getfixturevalue(structure) + input_gen = StaticSetGenerator() + incar = input_gen.get_input_set(structure, potcar_spec=True).incar + + has_nonzero_u = ( + any( + input_gen.config_dict["INCAR"]["LDAUU"]["O"].get(str(site.specie), 0) > 0 + for site in structure + ) + and input_gen.config_dict["INCAR"]["LDAU"] + ) + + if has_nonzero_u: + # if at least one site has a nonzero U value in the config_dict, + # ensure that there are LDAU* keys, and that they match expected values + # in config_dict + assert len([key for key in incar if key.startswith("LDAU")]) > 0 + for LDAU_key in ["LDAUU", "LDAUJ", "LDAUL"]: + for isite, site in enumerate(structure): + assert incar[LDAU_key][isite] == input_gen.config_dict["INCAR"][ + LDAU_key + ]["O"].get(str(site.specie), 0) + else: + # if no sites have a nonzero U value in the config_dict, + # ensure that no keys starting with LDAU are in the INCAR + assert len([key for key in incar if key.startswith("LDAU")]) == 0 + + +@pytest.mark.parametrize( + "bandgap, expected_params", + [ + (0, {"KSPACING": 0.22, "ISMEAR": 2, "SIGMA": 0.2}), + (0.1, {"KSPACING": 0.269695615, "ISMEAR": -5, "SIGMA": 0.05}), + (1, {"KSPACING": 0.302352354, "ISMEAR": -5, "SIGMA": 0.05}), + (2, {"KSPACING": 0.349355136, "ISMEAR": -5, "SIGMA": 0.05}), + (5, {"KSPACING": 0.44, "ISMEAR": -5, "SIGMA": 0.05}), + (10, {"KSPACING": 0.44, "ISMEAR": -5, "SIGMA": 0.05}), + ], +) +def test_set_kspacing_and_auto_ismear( + struct_no_magmoms, bandgap, expected_params, monkeypatch +): + static_set = MPMetaGGARelaxSetGenerator(auto_ismear=True, auto_kspacing=True) + + incar = static_set._get_incar( + structure=struct_no_magmoms, + kpoints=None, + previous_incar=None, + incar_updates={}, + bandgap=bandgap, + ) + + actual = {key: incar[key] for key in expected_params} + assert actual == pytest.approx(expected_params)