├── .github ├── dependabot.yml └── workflows │ ├── linting.yml │ ├── python-publish.yml │ └── testing.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── MANIFEST.in ├── README.md ├── examples ├── MP_compliant │ ├── CONTCAR.gz │ ├── DOSCAR.gz │ ├── EIGENVAL.gz │ ├── IBZKPT.gz │ ├── INCAR.gz │ ├── INCAR.orig.gz │ ├── MP_compatible_GaAs_r2SCAN_static.json.gz │ ├── OSZICAR.gz │ ├── OUTCAR.gz │ ├── PCDAT.gz │ ├── POSCAR.gz │ ├── POSCAR.orig.gz │ ├── POTCAR.spec.gz │ ├── PROCAR.gz │ ├── REPORT.gz │ ├── XDATCAR.gz │ ├── custodian.json.gz │ ├── vasp.out.gz │ └── vasprun.xml.gz ├── MP_compliant_job.py ├── MP_non_compliant │ ├── CONTCAR.gz │ ├── DOSCAR.gz │ ├── EIGENVAL.gz │ ├── IBZKPT.gz │ ├── INCAR.gz │ ├── INCAR.orig.gz │ ├── MP_incompatible_GaAs_r2SCAN_static.json.gz │ ├── OSZICAR.gz │ ├── OUTCAR.gz │ ├── PCDAT.gz │ ├── POSCAR.gz │ ├── POSCAR.orig.gz │ ├── POTCAR.spec.gz │ ├── PROCAR.gz │ ├── REPORT.gz │ ├── XDATCAR.gz │ ├── custodian.json.gz │ ├── vasp.out.gz │ └── vasprun.xml.gz └── using_validation_docs.ipynb ├── pymatgen └── io │ └── validation │ ├── __init__.py │ ├── check_common_errors.py │ ├── check_for_excess_empty_space.py │ ├── check_incar.py │ ├── check_kpoints_kspacing.py │ ├── check_package_versions.py │ ├── check_potcar.py │ ├── common.py │ ├── compare_to_MP_ehull.py │ ├── py.typed │ ├── settings.py │ ├── validation.py │ ├── vasp_defaults.py │ └── vasp_defaults.yaml ├── pyproject.toml ├── requirements-dev.txt ├── requirements.txt ├── setup.cfg ├── setup.py └── tests ├── conftest.py ├── test_files └── vasp │ ├── Si_old_double_relax.json.gz │ ├── Si_static.json.gz │ ├── Si_uniform.json.gz │ ├── Si_uniform │ ├── CONTCAR.gz │ ├── INCAR.gz │ ├── INCAR.orig.gz │ ├── KPOINTS.gz │ ├── KPOINTS.orig.gz │ ├── OUTCAR.gz │ ├── POSCAR.gz │ ├── POSCAR.orig.gz │ ├── custodian.json.gz │ └── vasprun.xml.gz │ ├── fake_Si_potcar_spec.json.gz │ ├── fake_potcar │ ├── POT_GGA_PAW_PBE │ │ ├── POTCAR.Al.gz │ │ ├── POTCAR.Eu.gz │ │ ├── POTCAR.Gd.gz │ │ ├── POTCAR.H.gz │ │ ├── POTCAR.O.gz │ │ └── POTCAR.Si.gz │ └── POT_GGA_PAW_PBE_54 │ │ ├── POTCAR.La.gz │ │ └── POTCAR.Si.gz │ ├── mp-1245223_site_props_check.json.gz │ └── scf_incar_check_list.yaml ├── test_validation.py └── test_validation_without_potcar.py /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | open-pull-requests-limit: 10 8 | allow: 9 | - dependency-type: direct 10 | - dependency-type: indirect 11 | ignore: 12 | - dependency-name: numpy 13 | -------------------------------------------------------------------------------- /.github/workflows/linting.yml: -------------------------------------------------------------------------------- 1 | name: Linting 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | max-parallel: 4 10 | matrix: 11 | python-version: ["3.10"] 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | with: 16 | fetch-depth: 0 17 | - name: Set up Python ${{ matrix.python-version }} 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install -r requirements-dev.txt --quiet 25 | python -m pip install types-requests 26 | - name: mypy 27 | run: | 28 | mypy pymatgen 29 | - name: black 30 | run: | 31 | black --version 32 | black --check --diff --color pymatgen 33 | - name: flake8 34 | run: | 35 | flake8 --count --show-source --statistics pymatgen 36 | # exit-zero treats all errors as warnings. 37 | flake8 --count --exit-zero --max-complexity=20 --statistics pymatgen 38 | - name: pydocstyle 39 | run: | 40 | pydocstyle --count pymatgen 41 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | workflow_run: 13 | workflows: [testing] 14 | types: [completed] 15 | 16 | permissions: 17 | contents: read 18 | 19 | jobs: 20 | deploy: 21 | 22 | # only run if commit is a push to master, the testing finished, and tagged as version 23 | if: github.repository_owner == 'materialsproject' && github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' && startsWith(github.event.workflow_run.head_branch, 'v0.') 24 | runs-on: ubuntu-latest 25 | 26 | steps: 27 | - uses: actions/checkout@v4 28 | with: 29 | ref: ${{ github.event.workflow_run.head_branch }} 30 | 31 | - name: Set up Python 32 | uses: actions/setup-python@v5 33 | with: 34 | python-version: '3.11' 35 | 36 | - name: Build 37 | run: | 38 | python -m pip install --upgrade pip 39 | pip install build 40 | python -m build 41 | 42 | - name: Publish package 43 | uses: pypa/gh-action-pypi-publish@release/v1 44 | with: 45 | user: __token__ 46 | password: ${{ secrets.PYPI_API_TOKEN }} 47 | verbose: true -------------------------------------------------------------------------------- /.github/workflows/testing.yml: -------------------------------------------------------------------------------- 1 | name: Testing 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | strategy: 9 | max-parallel: 20 10 | matrix: 11 | os: [ubuntu-latest, windows-latest] 12 | python-version: ["3.10", "3.11", 3.12] 13 | 14 | runs-on: ${{ matrix.os }} 15 | 16 | env: 17 | PMG_MAPI_KEY: ${{ secrets.PMG_MAPI_KEY }} 18 | MPLBACKEND: "Agg" 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v4 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install --quiet -r requirements.txt -r requirements-dev.txt 30 | pip install -e . 31 | - name: pytest 32 | run: | 33 | pytest #--cov=pymatgen tests 34 | - name: Upload coverage reports to Codecov 35 | if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && inputs.task == 'release') 36 | uses: codecov/codecov-action@v3 37 | with: 38 | token: ${{ secrets.CODECOV_TOKEN }} 39 | verbose: true 40 | # - uses: codecov/codecov-action@v1 41 | # if: matrix.python-version == 3.10 42 | # with: 43 | # token: ${{ secrets.CODECOV_TOKEN }} 44 | # file: ./coverage.xml 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # PyCharm settings 132 | .idea* 133 | .DS_store 134 | .log 135 | 136 | docs_build 137 | examples/wip* 138 | 139 | .vscode/ 140 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: ^(docs|.*test_files|cmd_line|dev_scripts|.github) 2 | 3 | default_language_version: 4 | python: python3.11 5 | 6 | ci: 7 | autoupdate_schedule: monthly 8 | skip: [flake8, autoflake, mypy] 9 | 10 | repos: 11 | 12 | - repo: https://github.com/astral-sh/ruff-pre-commit 13 | rev: v0.4.3 14 | hooks: 15 | - id: ruff 16 | args: [--fix, --unsafe-fixes] 17 | 18 | - repo: https://github.com/psf/black 19 | rev: 24.2.0 20 | hooks: 21 | - id: black 22 | 23 | - repo: https://github.com/pre-commit/pre-commit-hooks 24 | rev: v4.5.0 25 | hooks: 26 | - id: check-yaml 27 | - id: end-of-file-fixer 28 | - id: trailing-whitespace 29 | 30 | #- repo: https://github.com/asottile/pyupgrade 31 | # rev: v3.15.1 32 | # hooks: 33 | # - id: pyupgrade 34 | # args: [--py38-plus] 35 | 36 | - repo: https://github.com/PyCQA/autoflake 37 | rev: v2.3.0 38 | hooks: 39 | - id: autoflake 40 | args: 41 | - --in-place 42 | - --remove-unused-variables 43 | - --remove-all-unused-imports 44 | - --expand-star-imports 45 | - --ignore-init-module-imports 46 | 47 | - repo: https://github.com/pre-commit/mirrors-mypy 48 | rev: v1.8.0 49 | hooks: 50 | - id: mypy 51 | files: ^pymatgen/ 52 | args: 53 | - --namespace-packages 54 | - --explicit-package-bases 55 | additional_dependencies: ['types-requests','pydantic>=2.10.0'] 56 | 57 | - repo: https://github.com/kynan/nbstripout 58 | rev: 0.8.1 59 | hooks: 60 | - id: nbstripout 61 | args: 62 | - --drop-empty-cells 63 | - --strip-init-cells 64 | - --extra-keys=metadata.kernelspec 65 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2023-2024 The Regents of the University of California, 3 | through Lawrence Berkeley National Laboratory 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.rst *.in requirements.txt 2 | recursive-include pymatgen *.py 3 | prune */tests 4 | prune */*/tests 5 | prune */*/*/tests 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | pymatgen-io-validation 2 | ===== 3 | 4 | This package is an extension to `pymatgen` for performing I/O validation. Specifically, this package checks for discrepancies between a specific calculation and a provided input set; it also checks for known bugs when certain input parameters are used in combination, alongside several other small checks. The motivation for creating this package was to ensure VASP calculations performed by groups outside of the Materials Project (MP) are compliant with MP data, thus enabling their raw data to be included in the MP Database. 5 | 6 | 7 | Installation 8 | ===== 9 | 10 | You can install this package by simply running 11 | 12 | `pip install pymatgen-io-validation` 13 | 14 | 15 | Usage 16 | ===== 17 | 18 | For validating calculations from the raw files, run: 19 | ``` 20 | from pymatgen.io.validation import VaspValidator 21 | validation_doc = VaspValidator.from_directory(path_to_vasp_calculation_directory) 22 | ``` 23 | 24 | In the above case, whether a calculation passes the validator can be accessed via `validation_doc.valid`. Moreover, reasons for an invalidated calculation can be accessed via `validation_doc.reasons` (this will be empty for valid calculations). Last but not least, warnings for potential issues (sometimes minor, sometimes major) can be accessed via `validation_doc.warnings`. 25 | 26 | Contributors 27 | ===== 28 | 29 | * [Matthew Kuner](https://github.com/matthewkuner) (lead), email: matthewkuner@gmail.com 30 | * [Aaron Kaplan](https://github.com/esoteric-ephemera) 31 | * [Janosh Riebesell](https://github.com/janosh) 32 | * [Jason Munro](https://github.com/munrojm) 33 | 34 | 35 | Rationale 36 | ==== 37 | 38 | | **Parameter** | **Reason** | 39 | | ---- | ---- | 40 | | ADDGRID | ADDGRID must be set to False. MP uses ADDGRID = False, as the VASP manual states "please do not use this tag [ADDGRID] as default in all your calculations!". ADDGRID can affect the outputted forces, hence all calculations are thus required to have ADDGRID = False for compatibility. | 41 | | AEXX / AMGGAX / AMGGAC / AGGAX / ALDAC / ALDAX | These parameters should be the VASP defaults unless otherwise specified in a given MP input set, as changing them is effectively a change to the level of theory. | 42 | | ALGO / IALGO | ALGO must be one of: "Normal", "Conjugate", "All", "Fast", "Exact". (This corresponds to an IALGO of 38, 58, 58, 68, 90, respectively). | 43 | | DEPER / EBREAK / WEIMIN | DEPER, EBREAK, and WEIMIN should not be changed according to the VASP wiki, hence MP requires them to remain as their default values. | 44 | | EDIFF | EDIFF must be equal to or greater than the value in the relevant MP input set. This will ensure compatibility between results with those in the MP Database. | 45 | | EDIFFG | Should be the same or better than in the relevant MP input set. For MP input sets with an energy-based cutoff, the calculation must have an energy change between the last two steps be less in magnitude than the specified EDIFFG (so even if your calculation uses force-based convergence, the energy must converge within the MP input set’s specification). The same logic applies to MP input sets with force-based EDIFFG settings. | 46 | | EFERMI | EFERMI must be one of: "LEGACY", "MIDGAP" | 47 | | EFIELD | Current MP input sets used to construct the main MP database do not set EFIELD, and hence we require this to be unset or set to 0. | 48 | | ENAUG | If ENAUG is present in the relevant MP input set, then a calculation’s ENAUG must be equal to or better than that value. | 49 | | ENCUT | ENCUT must be equal to or greater than the value in the relevant MP input set, as otherwise results will likely be incompatible with MP. | 50 | | ENINI | ENINI must not be adjusted for high-throughput calculations, and hence should be left equal to the value used for ENCUT in the relevant MP input set. Values greater than the ENCUT in the relevant MP input set will also be accepted, though we expect this to be uncommon and do not recommend it. | 51 | | EPSILON | EPSILON must be set to the VASP default of 1. Changing the dielectric constant of the medium will cause results to be incompatible with MP. | 52 | | GGA / METAGGA | The level of theory used must match the relevant MP input set. Moreover, GGA and METAGGA should never be set simultaneously, as this has been shown to result in seriously erroneous results. See https://github.com/materialsproject/atomate2/issues/453#issuecomment-1699605867 for more details. | 53 | | GGA_COMPAT | GGA_COMPAT must be set to the VASP default of True. The VASP manual only recommends setting this to False for noncollinear magnetic calculations, which are not currently included in the MP database. | 54 | | IBRION | IBRION values must be one of: -1, 1, 2. Other IBRION values correspond to non-standard forms of DFT calculations that are not included in the MP database. (Note that, while phonon data is included in the MP database, such values are not calculated using, say, IBRION = 5. Such logic applies to all other IBRION values not allowed). | 55 | | ICHARG | ICHARG must be set to be compatible with the calculation type. For example, if the relevant MP input set is for a SCF calculation, ICHARG $\leq 9$ must be used. For NSCF calculations, the value for ICHARG must exactly match the value contained in the relevant MP input set. | 56 | | ICORELEVEL | ICORELEVEL must be set to 0. MP does not explicitly calculate core energies. | 57 | | IDIPOL | IDIPOL must be set to 0 (the VASP default). | 58 | | IMAGES | IMAGES must be set to 0 to match MP calculations. | 59 | | INIWAV | INIWAV must be set as the VASP default of 1 to be consistent with MP calculations. | 60 | | ISPIN | All values of ISPIN are allowed, though it should be noted that virtually all MP calculations permit spin symmetry breaking, and have ferromagnetic, antiferrogmanetic, or nonmagnetic ordering. | 61 | | ISMEAR | The appropriate ISMEAR depends on the bandgap of the material (which cannot be known a priori). As per the VASP manual: for metals (bandgap = 0), any ISMEAR value in [0, 1, 2] is acceptable. For nonmetals (bandgap > 0), any ISMEAR value in [-5, 0] is acceptable. Hence, for those who are performing normal relaxations/static calculations and want to ensure their calculations are MP-compatible, we recommend setting ISMEAR to 0. | 62 | | ISIF | MP allows any ISIF $\geq 2$. This value is restricted as such simply because all ISIF values $\geq 2$ output the complete stress tensor. | 63 | | ISYM | ISYM must be one of -1, 0, 1, 2, except for when the relevant MP input set uses a hybrid functional, in which case ISYM=3 is also allowed. | 64 | | ISTART | ISTART must be one of: 0, 1, 2. | 65 | | IVDW | IVDW must be set to 0. MP currently does not apply any vdW dispersion corrections. | 66 | | IWAVPR | IWAVPR must be set to 0 (the default). VASP discourages users from setting this tag. | 67 | | KGAMMA | KGAMMA must be set to True (the VASP default). This is only relevant when no KPOINTS file is used. | 68 | | KSPACING / KPOINTS | The KSPACING parameter or KPOINTS file must correspond with at least 0.9 times **the number of KPOINTS in the non-symmetry-reduced Brillouin zone specified by the relevant MP input set** (i.e., not the number of points in the irreducible wedge of the first Brillouin zone). Hence, either method of specifying the KPOINTS can be chosen. This ensures that a calculation uses a comparable number of kpoints to MP. | 69 | | Kpoint mesh type (for KPOINTS) | The type of Kpoint mesh must be valid for the symmetry of the crystal structure in the calculation. For example, for a hexagonal closed packed structure, one must use a $\Gamma$-centered mesh. All Kpoints generated using Pymatgen *should* be valid. | 70 | | LASPH | LASPH must be set to True (this is ***not*** the VASP default). | 71 | | LCALCEPS | LCALCEPS must be set to False (the VASP default). | 72 | | LBERRY | LBERRY must be set to False (the VASP default). | 73 | | LCALCPOL | LCALCPOL must be set to False (the VASP default). | 74 | | LCHIMAG | LCHIMAG must be set to False (the VASP default). | 75 | | LCORR | LCORR must be set to True (the VASP default) for calculations with IALGO = 58. | 76 | | LDAU / LDAUU / LDAUJ / LDAUL / LDAUTYPE | For DFT$`+U`$ calculations, all parameters corresponding to $+U$ or $+J$ corrections must exactly match those specified in the relevant MP input set. Alternatively, LDAU = False (DFT) is always acceptable. | 77 | | LDIPOL | LDIPOL must be set to False (the VASP default). | 78 | | LMONO | LMONO must be set to False (the VASP default). | 79 | | LEFG | LEFG must be set to False (the VASP default), unless explicitly specified to be True by the relevant MP input set. | 80 | | LEPSILON | LEPSILON must be set to False (the VASP default), unless explicitly specified to be True by the relevant MP input set. | 81 | | LHFCALC | The value of LHFCALC should match that of the relevant MP input set, as it will otherwise result in a change in the level of theory applied in the calculation. | 82 | | LHYPERFINE | LHYPERFINE must be set to False (the VASP default). | 83 | | LKPROJ | LKPROJ must be set to False (the VASP default). | 84 | | LKPOINTS_OPT | LKPOINTS_OPT must be set to False. | 85 | | LMAXPAW | LMAXPAW must remain unspecified, as the VASP wiki states that "Energies should be evaluated with the default setting for LMAXPAW". | 86 | | LMAXMIX | LMAXMIX must be set to 6. This is based on tests from Aaron Kaplan (@esoteric-ephemera) — see the "bench_vasp_pars.docx" document in https://github.com/materialsproject/pymatgen/issues/3322. | 87 | | LMAXTAU | LMAXTAU must be set to 6 (the VASP default when using LASPH = True). | 88 | | LMP2LT / LSMP2LT | Both must be set to False (VASP defaults) | 89 | | LNONCOLLINEAR / LSORBIT | Both must be set to False (VASP defaults) | 90 | | LOCPROJ | LOCPROJ must be set to None (the VASP default). | 91 | | LOPTICS | LOPTICS must be set to False (the VASP default), unless explicitly specified by the relevant MP input set. | 92 | | LORBIT | LORBIT must ***not*** be None if the user also sets ISPIN=2, otherwise all values of LORBIT are acceptable. This is due to magnetization values not being output when ISPIN=2 and LORBIT = None are set together. | 93 | | LREAL | If the LREAL in the relevant MP input set is "Auto", then the user must be one of: "Auto", False. Otherwise, if the LREAL in the relevant MP input set is False, then the user must use False. | 94 | | LRPA | LRPA must be set to False (the VASP default). MP does not currently support random phase approximation (RPA) calculations. | 95 | | LSPECTRAL | LSPECTRAL must be set to False (the VASP default for most calculations). | 96 | | LSUBROT | LSUBROT must be set to False (the VASP default). | 97 | | MAGMOM | While any initial magnetic moments are allowed, the final total magnetic moment for any given atom must be less than 5 $\mu_B$ (Bohr magnetons) (except for elements Gd and Eu, which must be less than 10 $\mu_B$). This simply serves as a filter for erroneous data. | 98 | | ML_LMFF | ML_LMFF must be set to False (the VASP default). | 99 | | NGX / NGY / NGZ | The values for NGX/NGY/NGZ must be at least 0.9 times the default value for the respective parameter generated by VASP. If the user simply does not specify these parameters, the calculation should be compatible with MP data. | 100 | | NGFX / NGFY / NGFZ | The values for NGFX/NGFY/NGFZ must be at least 0.9 times the default value for the respective parameter generated by VASP. If the user simply does not specify these parameters, the calculation should be compatible with MP data. | 101 | | NLSPLINE | NLSPLINE should be set to False (the VASP default), unless explicitly specified by the relevant MP input set. | 102 | | NBANDS | NBANDS must be greater than the value $\mathrm{ceil}(\mathrm{NELECT}/2) + 1 $(minimum allowable number of bands to avoid degeneracy) and less than 4 times (minimum allowable number of bands to avoid degeneracy). For high-throughput calculations, it is generally recommended to not set this parameter directly. See https://github.com/materialsproject/custodian/issues/224 for more information. | 103 | | NELECT | NELECT must not be changed from the default value VASP would use for the particular structure and pseudopotentials calculated. The easiest way to ensure that NELECT is compliant with MP data is to simply not specify NELECT in the INCAR file. | 104 | | NWRITE | NWRITE must be set to be $\geq 2$ (the VASP default is 2). | 105 | | POTIM | POTIM $\leq 5$. We suggest not setting POTIM in the INCAR file, and rather allowing VASP to set it to the default value of 0.5. | 106 | | PSTRESS | PSTRESS must be set to exactly 0.0 (the VASP default). | 107 | | PREC | PREC must be one of: "High", "Accurate". | 108 | | ROPT | ROPT should be set to be less than or equal to the default ROPT value (which is set based on the PREC tag). Hence, it is recommended to not set the ROPT tag in the INCAR file. | 109 | | RWIGS | RWIGS should not be set in the INCAR file. | 110 | | SCALEE | SCALEE should not be set in the INCAR file. | 111 | | SYMPREC | SYMPREC must be less than or equal to 1e-3 (as this is the maximum value that the Custodian package will set SYMPREC as of March 2024). For general use, we recommend leaving SYMPREC as the default generated by your desired MP input set. | 112 | | SIGMA | There are several rules for setting SIGMA:
  1. SIGMA must be $\leq 0.05$ for non-metals (bandgap $> 0$).
  2. SIGMA must be $\leq 0.2$ for a metal (bandgap = 0).
  3. For metals, the SIGMA value must be small enough that the entropy term in the energy is $\leq$ 1 meV/atom (as suggested by the VASP manual).
| 113 | | VCA | MP data does not include Virtual Crystal Approximation (VCA) calculations from VASP. As such, this parameter should not be set. | 114 | | VASP version | The following versions of VASP are allowed: 5.4.4 or $>$ 6.0.0. For example, versions $<=$ 5.4.3 are not allowed, whereas version 6.3.1 is allowed. | 115 | -------------------------------------------------------------------------------- /examples/MP_compliant/CONTCAR.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/examples/MP_compliant/CONTCAR.gz -------------------------------------------------------------------------------- /examples/MP_compliant/DOSCAR.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/examples/MP_compliant/DOSCAR.gz -------------------------------------------------------------------------------- /examples/MP_compliant/EIGENVAL.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/examples/MP_compliant/EIGENVAL.gz -------------------------------------------------------------------------------- /examples/MP_compliant/IBZKPT.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/examples/MP_compliant/IBZKPT.gz -------------------------------------------------------------------------------- /examples/MP_compliant/INCAR.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/examples/MP_compliant/INCAR.gz -------------------------------------------------------------------------------- /examples/MP_compliant/INCAR.orig.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/examples/MP_compliant/INCAR.orig.gz -------------------------------------------------------------------------------- /examples/MP_compliant/MP_compatible_GaAs_r2SCAN_static.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/examples/MP_compliant/MP_compatible_GaAs_r2SCAN_static.json.gz -------------------------------------------------------------------------------- /examples/MP_compliant/OSZICAR.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/examples/MP_compliant/OSZICAR.gz -------------------------------------------------------------------------------- /examples/MP_compliant/OUTCAR.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/examples/MP_compliant/OUTCAR.gz -------------------------------------------------------------------------------- /examples/MP_compliant/PCDAT.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/examples/MP_compliant/PCDAT.gz -------------------------------------------------------------------------------- /examples/MP_compliant/POSCAR.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/examples/MP_compliant/POSCAR.gz -------------------------------------------------------------------------------- /examples/MP_compliant/POSCAR.orig.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/examples/MP_compliant/POSCAR.orig.gz -------------------------------------------------------------------------------- /examples/MP_compliant/POTCAR.spec.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/examples/MP_compliant/POTCAR.spec.gz -------------------------------------------------------------------------------- /examples/MP_compliant/PROCAR.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/examples/MP_compliant/PROCAR.gz -------------------------------------------------------------------------------- /examples/MP_compliant/REPORT.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/examples/MP_compliant/REPORT.gz -------------------------------------------------------------------------------- /examples/MP_compliant/XDATCAR.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/examples/MP_compliant/XDATCAR.gz -------------------------------------------------------------------------------- /examples/MP_compliant/custodian.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/examples/MP_compliant/custodian.json.gz -------------------------------------------------------------------------------- /examples/MP_compliant/vasp.out.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/examples/MP_compliant/vasp.out.gz -------------------------------------------------------------------------------- /examples/MP_compliant/vasprun.xml.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/examples/MP_compliant/vasprun.xml.gz -------------------------------------------------------------------------------- /examples/MP_compliant_job.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from emmet.core.utils import jsanitize 4 | from emmet.core.vasp.task_valid import TaskDocument 5 | from jobflow import Flow 6 | from monty.serialization import dumpfn 7 | import numpy as np 8 | from pymatgen.core import Structure, Lattice 9 | 10 | 11 | def get_GaAs_structure(a0: float = 5.6) -> Structure: 12 | lattice_vectors = a0 * np.array([[0.0 if i == j else 0.5 for j in range(3)] for i in range(3)]) 13 | return Structure( 14 | lattice=Lattice(lattice_vectors), 15 | species=["Ga", "As"], 16 | coords=[[0.125, 0.125, 0.125], [0.875, 0.875, 0.875]], 17 | coords_are_cartesian=False, 18 | ) 19 | 20 | 21 | def assign_meta(flow, metadata: dict, name: str | None = None): 22 | if hasattr(flow, "jobs"): 23 | for ijob in range(len(flow.jobs)): 24 | assign_meta(flow.jobs[ijob], metadata, name=name) 25 | if name: 26 | flow.name = name 27 | else: 28 | flow.metadata = metadata.copy() 29 | if name: 30 | flow.name = name 31 | 32 | 33 | def get_MP_compliant_r2SCAN_flow( 34 | structure: Structure, 35 | user_incar_settings: dict | None = None, 36 | metadata: dict | None = None, 37 | name: str | None = None, 38 | ) -> Flow: 39 | from atomate2.vasp.jobs.mp import MPMetaGGAStaticMaker 40 | from atomate2.vasp.powerups import update_user_incar_settings 41 | 42 | maker = MPMetaGGAStaticMaker() 43 | 44 | user_incar_settings = user_incar_settings or {} 45 | if len(user_incar_settings) > 0: 46 | maker = update_user_incar_settings(maker, incar_updates=user_incar_settings) 47 | 48 | flow = maker.make(structure) 49 | 50 | metadata = metadata or {} 51 | assign_meta(flow, metadata, name=name) 52 | 53 | return flow 54 | 55 | 56 | def run_job_fully_locally(flow, job_store=None): 57 | from jobflow import run_locally, JobStore 58 | from maggma.stores import MemoryStore 59 | 60 | if job_store is None: 61 | job_store = JobStore(MemoryStore(), additional_stores={"data": MemoryStore()}) 62 | 63 | response = run_locally(flow, store=job_store, create_folders=True) 64 | uuid = list(response)[0] 65 | return response[uuid][1].output 66 | 67 | 68 | def MP_compliant_calc(): 69 | structure = get_GaAs_structure() 70 | flow = get_MP_compliant_r2SCAN_flow( 71 | structure=structure, 72 | user_incar_settings={ # some forward-looking settings 73 | "LREAL": False, 74 | "LMAXMIX": 6, 75 | "LCHARG": False, # following tags just set for convenience 76 | "LWAVE": False, 77 | "LAECHG": False, 78 | "NCORE": 16, 79 | "KPAR": 2, 80 | }, 81 | ) 82 | return run_job_fully_locally(flow) 83 | 84 | 85 | def MP_non_compliant_calc(): 86 | structure = get_GaAs_structure() 87 | flow = get_MP_compliant_r2SCAN_flow( 88 | structure=structure, 89 | user_incar_settings={ # some backward-looking settings 90 | "ENCUT": 450.0, 91 | "ENAUG": 900.0, 92 | "KSPACING": 0.5, 93 | "LCHARG": False, # following tags just set for convenience 94 | "LWAVE": False, 95 | "LAECHG": False, 96 | "NCORE": 16, 97 | "KPAR": 2, 98 | }, 99 | ) 100 | return run_job_fully_locally(flow) 101 | 102 | 103 | def MP_flows() -> None: 104 | compliant_task_doc = MP_compliant_calc() 105 | dumpfn(jsanitize(compliant_task_doc), "./MP_compatible_GaAs_r2SCAN_static.json.gz") 106 | 107 | non_compliant_task_doc = MP_non_compliant_calc() 108 | dumpfn( 109 | jsanitize(non_compliant_task_doc), 110 | "./MP_incompatible_GaAs_r2SCAN_static.json.gz", 111 | ) 112 | 113 | 114 | def generate_task_documents(cdir, task_id: str | None = None, filename: str | None = None) -> TaskDocument: 115 | from atomate.vasp.drones import VaspDrone 116 | from emmet.core.mpid import MPID 117 | 118 | drone = VaspDrone(store_volumetric_data=[]) 119 | task_doc_dict = drone.assimilate(cdir) 120 | 121 | task_id = task_id or "mp-100000000" 122 | task_doc_dict["task_id"] = MPID(task_id) 123 | task_doc = TaskDocument(**task_doc_dict) 124 | 125 | if filename: 126 | dumpfn(jsanitize(task_doc), filename) 127 | 128 | return task_doc 129 | 130 | 131 | if __name__ == "__main__": 132 | MP_flows() 133 | -------------------------------------------------------------------------------- /examples/MP_non_compliant/CONTCAR.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/examples/MP_non_compliant/CONTCAR.gz -------------------------------------------------------------------------------- /examples/MP_non_compliant/DOSCAR.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/examples/MP_non_compliant/DOSCAR.gz -------------------------------------------------------------------------------- /examples/MP_non_compliant/EIGENVAL.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/examples/MP_non_compliant/EIGENVAL.gz -------------------------------------------------------------------------------- /examples/MP_non_compliant/IBZKPT.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/examples/MP_non_compliant/IBZKPT.gz -------------------------------------------------------------------------------- /examples/MP_non_compliant/INCAR.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/examples/MP_non_compliant/INCAR.gz -------------------------------------------------------------------------------- /examples/MP_non_compliant/INCAR.orig.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/examples/MP_non_compliant/INCAR.orig.gz -------------------------------------------------------------------------------- /examples/MP_non_compliant/MP_incompatible_GaAs_r2SCAN_static.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/examples/MP_non_compliant/MP_incompatible_GaAs_r2SCAN_static.json.gz -------------------------------------------------------------------------------- /examples/MP_non_compliant/OSZICAR.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/examples/MP_non_compliant/OSZICAR.gz -------------------------------------------------------------------------------- /examples/MP_non_compliant/OUTCAR.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/examples/MP_non_compliant/OUTCAR.gz -------------------------------------------------------------------------------- /examples/MP_non_compliant/PCDAT.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/examples/MP_non_compliant/PCDAT.gz -------------------------------------------------------------------------------- /examples/MP_non_compliant/POSCAR.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/examples/MP_non_compliant/POSCAR.gz -------------------------------------------------------------------------------- /examples/MP_non_compliant/POSCAR.orig.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/examples/MP_non_compliant/POSCAR.orig.gz -------------------------------------------------------------------------------- /examples/MP_non_compliant/POTCAR.spec.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/examples/MP_non_compliant/POTCAR.spec.gz -------------------------------------------------------------------------------- /examples/MP_non_compliant/PROCAR.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/examples/MP_non_compliant/PROCAR.gz -------------------------------------------------------------------------------- /examples/MP_non_compliant/REPORT.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/examples/MP_non_compliant/REPORT.gz -------------------------------------------------------------------------------- /examples/MP_non_compliant/XDATCAR.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/examples/MP_non_compliant/XDATCAR.gz -------------------------------------------------------------------------------- /examples/MP_non_compliant/custodian.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/examples/MP_non_compliant/custodian.json.gz -------------------------------------------------------------------------------- /examples/MP_non_compliant/vasp.out.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/examples/MP_non_compliant/vasp.out.gz -------------------------------------------------------------------------------- /examples/MP_non_compliant/vasprun.xml.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/examples/MP_non_compliant/vasprun.xml.gz -------------------------------------------------------------------------------- /examples/using_validation_docs.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "0", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "from __future__ import annotations\n", 11 | "\n", 12 | "from monty.os.path import zpath\n", 13 | "from monty.serialization import loadfn\n", 14 | "import os\n", 15 | "from pathlib import Path\n", 16 | "\n", 17 | "from pymatgen.io.validation.validation import VaspValidator\n", 18 | "\n", 19 | "from pymatgen.io.vasp import PotcarSingle, Potcar" 20 | ] 21 | }, 22 | { 23 | "cell_type": "markdown", 24 | "id": "1", 25 | "metadata": {}, 26 | "source": [ 27 | "For copyright reasons, the POTCAR for these calculations cannot be distributed with this file, but its summary stats can.\n", 28 | "\n", 29 | "If you have the POTCAR resources set up in pymatgen, you can regenerate the POTCARs used here by enabling `regen_potcars`" 30 | ] 31 | }, 32 | { 33 | "cell_type": "code", 34 | "execution_count": null, 35 | "id": "2", 36 | "metadata": {}, 37 | "outputs": [], 38 | "source": [ 39 | "regen_potcars = True\n", 40 | "\n", 41 | "def get_potcar_from_spec(potcar_spec : dict) -> Potcar | None:\n", 42 | " \n", 43 | " for functional in PotcarSingle._potcar_summary_stats:\n", 44 | "\n", 45 | " potcar = Potcar()\n", 46 | " matched = [False for _ in range(len(potcar_spec))]\n", 47 | " for ispec, spec in enumerate(potcar_spec):\n", 48 | " titel = spec.get(\"titel\",\"\")\n", 49 | " titel_no_spc = titel.replace(\" \",\"\")\n", 50 | " symbol = titel.split(\" \")[1].strip()\n", 51 | " \n", 52 | " for stats in PotcarSingle._potcar_summary_stats[functional].get(titel_no_spc,[]):\n", 53 | " \n", 54 | " if PotcarSingle.compare_potcar_stats(spec[\"summary_stats\"], stats):\n", 55 | " potcar.append(PotcarSingle.from_symbol_and_functional(symbol=symbol, functional=functional))\n", 56 | " matched[ispec] = True\n", 57 | " break\n", 58 | " \n", 59 | " if all(matched):\n", 60 | " return potcar\n", 61 | " \n", 62 | "def check_calc(calc_dir : str | Path) -> VaspValidator:\n", 63 | "\n", 64 | " calc_dir = Path(calc_dir)\n", 65 | " potcar_filename = None\n", 66 | " if regen_potcars:\n", 67 | " potcar = get_potcar_from_spec(loadfn(calc_dir / \"POTCAR.spec.gz\"))\n", 68 | " if potcar:\n", 69 | " potcar_filename = calc_dir / \"POTCAR.gz\"\n", 70 | " potcar.write_file(potcar_filename)\n", 71 | " \n", 72 | " vasp_files = {\n", 73 | " k.lower().split(\".\")[0] : zpath(calc_dir / k) for k in (\n", 74 | " \"INCAR\",\"KPOINTS\",\"POSCAR\",\"POTCAR\",\"OUTCAR\", \"vasprun.xml\"\n", 75 | " )\n", 76 | " }\n", 77 | " \n", 78 | " valid_doc = VaspValidator.from_vasp_input(\n", 79 | " vasp_file_paths={\n", 80 | " k : v for k,v in vasp_files.items() if Path(v).exists()\n", 81 | " },\n", 82 | " check_potcar=(regen_potcars and potcar)\n", 83 | " )\n", 84 | "\n", 85 | " if potcar_filename and potcar:\n", 86 | " os.remove(potcar_filename)\n", 87 | " \n", 88 | " return valid_doc\n", 89 | " " 90 | ] 91 | }, 92 | { 93 | "cell_type": "markdown", 94 | "id": "3", 95 | "metadata": {}, 96 | "source": [ 97 | "An example of an MP-compatible r2SCAN static calculation for GaAs is located in the `MP_compliant` directory. We also include `TaskDoc` objects generated with `atomate2`, the workflow software currently used by the Materials Project (MP) for high-throughput calculations. A `TaskDoc` is also the document schema for the MP `task` collection." 98 | ] 99 | }, 100 | { 101 | "cell_type": "code", 102 | "execution_count": null, 103 | "id": "4", 104 | "metadata": {}, 105 | "outputs": [], 106 | "source": [ 107 | "mp_compliant_doc = check_calc(\"MP_compliant\")\n", 108 | "print(mp_compliant_doc.valid)" 109 | ] 110 | }, 111 | { 112 | "cell_type": "markdown", 113 | "id": "5", 114 | "metadata": {}, 115 | "source": [ 116 | "An example of an MP incompatible r2SCAN static calculation for GaAs is located in the `MP_non_compliant` directory.\n", 117 | "\n", 118 | "This calculation uses a lower ENCUT, ENAUG, and k-point density (larger KSPACING) than is permitted by the appropriate input set, `pymatgen.io.vasp.sets.MPScanStaticSet`.\n", 119 | "These reasons are reflected transparently in the output reasons." 120 | ] 121 | }, 122 | { 123 | "cell_type": "code", 124 | "execution_count": null, 125 | "id": "6", 126 | "metadata": {}, 127 | "outputs": [], 128 | "source": [ 129 | "mp_non_compliant_doc = check_calc(\"MP_non_compliant\")\n", 130 | "print(mp_non_compliant_doc.valid)\n", 131 | "for reason in mp_non_compliant_doc.reasons:\n", 132 | " print(reason)" 133 | ] 134 | } 135 | ], 136 | "metadata": { 137 | "language_info": { 138 | "codemirror_mode": { 139 | "name": "ipython", 140 | "version": 3 141 | }, 142 | "file_extension": ".py", 143 | "mimetype": "text/x-python", 144 | "name": "python", 145 | "nbconvert_exporter": "python", 146 | "pygments_lexer": "ipython3", 147 | "version": "3.11.0" 148 | } 149 | }, 150 | "nbformat": 4, 151 | "nbformat_minor": 5 152 | } 153 | -------------------------------------------------------------------------------- /pymatgen/io/validation/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | pymatgen-io-validation provides validation tools for inputs and outputs of computational 3 | simulations and calculations. That is, it checks calculation details against a reference 4 | to ensure that data is compatible with some standard. 5 | """ 6 | 7 | from pymatgen.io.validation.common import SETTINGS 8 | from pymatgen.io.validation.validation import VaspValidator # noqa: F401 9 | 10 | if SETTINGS.CHECK_PYPI_AT_LOAD: 11 | # Only check version at module load time, if specified in module settings. 12 | from pymatgen.io.validation.check_package_versions import package_version_check 13 | 14 | package_version_check() 15 | -------------------------------------------------------------------------------- /pymatgen/io/validation/check_common_errors.py: -------------------------------------------------------------------------------- 1 | """Check common issues with VASP calculations.""" 2 | 3 | from __future__ import annotations 4 | from pydantic import Field 5 | import numpy as np 6 | from typing import TYPE_CHECKING 7 | 8 | from pymatgen.io.validation.common import SETTINGS, BaseValidator 9 | 10 | if TYPE_CHECKING: 11 | from collections.abc import Sequence 12 | from numpy.typing import ArrayLike 13 | 14 | from pymatgen.io.validation.common import VaspFiles 15 | 16 | 17 | class CheckCommonErrors(BaseValidator): 18 | """ 19 | Check for common calculation errors. 20 | """ 21 | 22 | name: str = "Check common errors" 23 | valid_max_magmoms: dict[str, float] = Field( 24 | default_factory=lambda: {"Gd": 10.0, "Eu": 10.0}, 25 | description="Dict of maximum magmoms corresponding to a given element.", 26 | ) 27 | exclude_elements: set[str] = Field( 28 | default_factory=lambda: {"Am", "Po"}, 29 | description="Set of elements that cannot be added to the Materials Project's hull.", 30 | ) 31 | valid_max_allowed_scf_gradient: float | None = Field( 32 | SETTINGS.VASP_MAX_SCF_GRADIENT, description="Largest permitted change in total energies between two SCF cycles." 33 | ) 34 | num_ionic_steps_to_avg_drift_over: int | None = Field( 35 | SETTINGS.VASP_NUM_IONIC_STEPS_FOR_DRIFT, 36 | description="Number of ionic steps to average over to yield the drift in total energy.", 37 | ) 38 | valid_max_energy_per_atom: float | None = Field( 39 | SETTINGS.VASP_MAX_POSITIVE_ENERGY, 40 | description="The maximum permitted, self-consistent positive energy in eV/atom.", 41 | ) 42 | 43 | def _check_vasp_version(self, vasp_files: VaspFiles, reasons: list[str], warnings: list[str]) -> None: 44 | """ 45 | Check for common errors related to the version of VASP used. 46 | 47 | reasons : list[str] 48 | A list of error strings to update if a check fails. These are higher 49 | severity and would deprecate a calculation. 50 | warnings : list[str] 51 | A list of warning strings to update if a check fails. These are lower 52 | severity and would flag a calculation for possible review. 53 | """ 54 | 55 | if not vasp_files.vasp_version: 56 | # Skip if vasprun.xml not specified 57 | return 58 | 59 | if ( 60 | vasp_files.vasp_version[0] == 5 61 | and ( 62 | vasp_files.user_input.incar.get("METAGGA", self.vasp_defaults["METAGGA"].value) 63 | not in [None, "--", "None"] 64 | ) 65 | and vasp_files.user_input.incar.get("ISPIN", self.vasp_defaults["ISPIN"].value) == 2 66 | ): 67 | reasons.append( 68 | "POTENTIAL BUG --> We believe that there may be a bug with spin-polarized calculations for METAGGAs " 69 | "in some versions of VASP 5. Please create a new GitHub issue if you believe this " 70 | "is not the case and we will consider changing this check!" 71 | ) 72 | elif (list(vasp_files.vasp_version) != [5, 4, 4]) and (vasp_files.vasp_version[0] < 6): 73 | vasp_version_str = ".".join([str(x) for x in vasp_files.vasp_version]) 74 | reasons.append( 75 | f"VASP VERSION --> This calculation is using VASP version {vasp_version_str}, " 76 | "but we only allow versions 5.4.4 and >=6.0.0 (as of July 2023)." 77 | ) 78 | 79 | def _check_electronic_convergence(self, vasp_files: VaspFiles, reasons: list[str], warnings: list[str]) -> None: 80 | # check if structure electronically converged 81 | 82 | if ( 83 | vasp_files.user_input.incar.get("ALGO", self.vasp_defaults["ALGO"].value).lower() != "chi" 84 | and vasp_files.vasprun 85 | ): 86 | # Response function calculations are non-self-consistent: only one ionic step, no electronic SCF 87 | if vasp_files.user_input.incar.get("LEPSILON", self.vasp_defaults["LEPSILON"].value): 88 | final_esteps = vasp_files.vasprun.ionic_steps[-1]["electronic_steps"] 89 | to_check = {"e_wo_entrp", "e_fr_energy", "e_0_energy"} 90 | 91 | for i in range(len(final_esteps)): 92 | if set(final_esteps[i]) != to_check: 93 | break 94 | i += 1 95 | 96 | is_converged = i + 1 < vasp_files.user_input.incar.get("NELM", self.vasp_defaults["NELM"].value) 97 | n_non_conv = 1 98 | 99 | else: 100 | conv_steps = [ 101 | len(ionic_step["electronic_steps"]) 102 | < vasp_files.user_input.incar.get("NELM", self.vasp_defaults["NELM"].value) 103 | for ionic_step in vasp_files.vasprun.ionic_steps 104 | ] 105 | is_converged = all(conv_steps) 106 | n_non_conv = len([step for step in conv_steps if not step]) 107 | 108 | if not is_converged: 109 | reasons.append( 110 | f"CONVERGENCE --> Did not achieve electronic convergence in {n_non_conv} ionic step(s). NELM should be increased." 111 | ) 112 | 113 | def _check_drift_forces(self, vasp_files: VaspFiles, reasons: list[str], warnings: list[str]) -> None: 114 | # Check if drift force is too large 115 | 116 | if not self.num_ionic_steps_to_avg_drift_over or not vasp_files.outcar: 117 | return 118 | 119 | if all_drift_forces := vasp_files.outcar.drift: 120 | if len(all_drift_forces) < self.num_ionic_steps_to_avg_drift_over: 121 | drift_forces_to_avg_over = all_drift_forces 122 | else: 123 | drift_forces_to_avg_over = all_drift_forces[::-1][: self.num_ionic_steps_to_avg_drift_over] 124 | 125 | drift_mags_to_avg_over = [np.linalg.norm(drift_forces) for drift_forces in drift_forces_to_avg_over] 126 | cur_avg_drift_mag = np.average(drift_mags_to_avg_over) 127 | 128 | valid_max_drift = 0.05 129 | if cur_avg_drift_mag > valid_max_drift: 130 | warnings.append( 131 | f"CONVERGENCE --> Excessive drift of {round(cur_avg_drift_mag,4)} eV/A is greater than allowed " 132 | f"value of {valid_max_drift} eV/A." 133 | ) 134 | else: 135 | warnings.append( 136 | "Could not determine drift forces from OUTCAR, and thus could not check for excessive drift." 137 | ) 138 | 139 | def _check_positive_energy(self, vasp_files: VaspFiles, reasons: list[str], warnings: list[str]) -> None: 140 | # Check for excessively positive final energies (which usually indicates a bad structure) 141 | if ( 142 | vasp_files.vasprun 143 | and self.valid_max_energy_per_atom 144 | and (cur_final_energy_per_atom := vasp_files.vasprun.final_energy / len(vasp_files.user_input.structure)) 145 | > self.valid_max_energy_per_atom 146 | ): 147 | reasons.append( 148 | f"LARGE POSITIVE FINAL ENERGY --> Final energy is {round(cur_final_energy_per_atom,4)} eV/atom, which is " 149 | f"greater than the maximum allowed value of {self.valid_max_energy_per_atom} eV/atom." 150 | ) 151 | 152 | def _check_large_magmoms(self, vasp_files: VaspFiles, reasons: list[str], warnings: list[str]) -> None: 153 | # Check for excessively large final magnetic moments 154 | 155 | if ( 156 | not vasp_files.outcar 157 | or not vasp_files.outcar.magnetization 158 | or any(mag.get("tot") is None for mag in vasp_files.outcar.magnetization) 159 | ): 160 | warnings.append("MAGNETISM --> No OUTCAR file specified or data missing.") 161 | return 162 | 163 | cur_magmoms = [abs(mag["tot"]) for mag in vasp_files.outcar.magnetization] 164 | bad_site_magmom_msgs = [] 165 | if len(cur_magmoms) > 0: 166 | for site_num in range(0, len(vasp_files.user_input.structure)): 167 | cur_site_ele = vasp_files.user_input.structure.sites[site_num].species_string 168 | cur_site_magmom = cur_magmoms[site_num] 169 | cur_site_max_allowed_magmom = self.valid_max_magmoms.get(cur_site_ele, 5.0) 170 | 171 | if cur_site_magmom > cur_site_max_allowed_magmom: 172 | bad_site_magmom_msgs.append( 173 | f"at least one {cur_site_ele} site with magmom greater than {cur_site_max_allowed_magmom}" 174 | ) 175 | 176 | if len(bad_site_magmom_msgs) > 0: 177 | reasons.append( 178 | "MAGNETISM --> Final structure contains sites with magnetic moments " 179 | "that are very likely erroneous. This includes: " 180 | f"{'; '.join(set(bad_site_magmom_msgs))}." 181 | ) 182 | 183 | def _check_scf_grad(self, vasp_files: VaspFiles, reasons: list[str], warnings: list[str]) -> None: 184 | # Check for a SCF gradient that is too large (usually indicates unstable calculations) 185 | # NOTE: do NOT use `e_0_energy`, as there is a bug in the vasprun.xml when printing that variable 186 | # (see https://www.vasp.at/forum/viewtopic.php?t=16942 for more details). 187 | 188 | if not vasp_files.vasprun or not self.valid_max_allowed_scf_gradient: 189 | return 190 | 191 | skip = abs(vasp_files.user_input.incar.get("NELMDL", self.vasp_defaults["NELMDL"].value)) - 1 192 | 193 | energies = [d["e_fr_energy"] for d in vasp_files.vasprun.ionic_steps[-1]["electronic_steps"]] 194 | if len(energies) > skip: 195 | cur_max_gradient = np.max(np.gradient(energies)[skip:]) 196 | cur_max_gradient_per_atom = cur_max_gradient / vasp_files.user_input.structure.num_sites 197 | if self.valid_max_allowed_scf_gradient and cur_max_gradient_per_atom > self.valid_max_allowed_scf_gradient: 198 | warnings.append( 199 | f"STABILITY --> The max SCF gradient is {round(cur_max_gradient_per_atom,4)} eV/atom, " 200 | "which is larger than the typical max expected value of " 201 | f"{self.valid_max_allowed_scf_gradient} eV/atom. " 202 | f"This sometimes indicates an unstable calculation." 203 | ) 204 | else: 205 | warnings.append( 206 | "Not enough electronic steps to compute valid gradient and compare with max SCF gradient tolerance." 207 | ) 208 | 209 | def _check_unused_elements(self, vasp_files: VaspFiles, reasons: list[str], warnings: list[str]) -> None: 210 | # Check for Am and Po elements. These currently do not have proper elemental entries 211 | # and will not get treated properly by the thermo builder. 212 | elements = set(vasp_files.user_input.structure.composition.chemical_system.split("-")) 213 | if excluded_elements := self.exclude_elements.intersection(elements): 214 | reasons.append( 215 | f"COMPOSITION --> Your structure contains the elements {' '.join(excluded_elements)}, " 216 | "which are not currently being accepted." 217 | ) 218 | 219 | 220 | class CheckStructureProperties(BaseValidator): 221 | """Check structure for options that are not suitable for thermodynamic calculations.""" 222 | 223 | name: str = "VASP POSCAR properties validator" 224 | site_properties_to_check: tuple[str, ...] = Field( 225 | ("selective_dynamics", "velocities"), description="Which site properties to check on a structure." 226 | ) 227 | 228 | @staticmethod 229 | def _has_frozen_degrees_of_freedom(selective_dynamics_array: Sequence[bool] | None) -> bool: 230 | """Check selective dynamics array for False values.""" 231 | if selective_dynamics_array is None: 232 | return False 233 | return not np.all(selective_dynamics_array) 234 | 235 | def _check_selective_dynamics(self, vasp_files: VaspFiles, reasons: list[str], warnings: list[str]) -> None: 236 | """Check structure for inappropriate site properties.""" 237 | if ( 238 | selec_dyn := vasp_files.user_input.structure.site_properties.get("selective_dynamics") 239 | ) is not None and vasp_files.run_type == "relax": 240 | if any(self._has_frozen_degrees_of_freedom(sd_array) for sd_array in selec_dyn): 241 | reasons.append( 242 | "Selective dynamics: certain degrees of freedom in the structure " 243 | "were not permitted to relax. To correctly place entries on the convex " 244 | "hull, all degrees of freedom should be allowed to relax." 245 | ) 246 | 247 | @staticmethod 248 | def _has_nonzero_velocities(velocities: ArrayLike | None, tol: float = 1.0e-8) -> bool: 249 | if velocities is None: 250 | return False 251 | return np.any(np.abs(velocities) > tol) # type: ignore [return-value] 252 | 253 | def _check_velocities(self, vasp_files: VaspFiles, reasons: list[str], warnings: list[str]) -> None: 254 | """Check structure for non-zero velocities.""" 255 | if ( 256 | velos := vasp_files.user_input.structure.site_properties.get("velocities") 257 | ) is not None and vasp_files.run_type != "md": 258 | if any(self._has_nonzero_velocities(velo) for velo in velos): 259 | warnings.append( 260 | "At least one of the structures had non-zero velocities. " 261 | f"While these are ignored by VASP for {vasp_files.run_type} " 262 | "calculations, please ensure that you intended to run a " 263 | "non-molecular dynamics calculation." 264 | ) 265 | -------------------------------------------------------------------------------- /pymatgen/io/validation/check_for_excess_empty_space.py: -------------------------------------------------------------------------------- 1 | """Module for checking if a structure is not a bulk crystal""" 2 | 3 | from pymatgen.analysis.local_env import VoronoiNN 4 | import numpy as np 5 | 6 | 7 | def check_for_excess_empty_space(structure): 8 | """Relatively robust method for checking if a structure is a surface slab/1d structure/anything that is not a bulk crystal""" 9 | # Check 1: find large gaps along one of the defined lattice vectors 10 | lattice_vec_lengths = structure.lattice.lengths 11 | fcoords = np.array(structure.frac_coords) 12 | 13 | lattice_vec_1_frac_coords = list(np.sort(fcoords[:, 0])) 14 | lattice_vec_1_frac_coords.append(lattice_vec_1_frac_coords[0] + 1) 15 | lattice_vec_1_diffs = np.diff(lattice_vec_1_frac_coords) 16 | max_lattice_vec_1_empty_dist = max(lattice_vec_1_diffs) * lattice_vec_lengths[0] 17 | 18 | lattice_vec_2_frac_coords = list(np.sort(fcoords[:, 1])) 19 | lattice_vec_2_frac_coords.append(lattice_vec_2_frac_coords[0] + 1) 20 | lattice_vec_2_diffs = np.diff(lattice_vec_2_frac_coords) 21 | max_lattice_vec_2_empty_dist = max(lattice_vec_2_diffs) * lattice_vec_lengths[1] 22 | 23 | lattice_vec_3_frac_coords = list(np.sort(fcoords[:, 2])) 24 | lattice_vec_3_frac_coords.append(lattice_vec_3_frac_coords[0] + 1) 25 | lattice_vec_3_diffs = np.diff(lattice_vec_3_frac_coords) 26 | max_lattice_vec_3_empty_dist = max(lattice_vec_3_diffs) * lattice_vec_lengths[2] 27 | 28 | max_empty_distance = max( 29 | max_lattice_vec_1_empty_dist, 30 | max_lattice_vec_2_empty_dist, 31 | max_lattice_vec_3_empty_dist, 32 | ) 33 | 34 | # Check 2: get max voronoi polyhedra volume in structure 35 | def get_max_voronoi_polyhedra_volume(structure): 36 | max_voronoi_polyhedra_vol = 0 37 | vnn = VoronoiNN().get_all_voronoi_polyhedra(structure) 38 | for polyhedra in vnn: 39 | for key in polyhedra.keys(): 40 | cur_vol = polyhedra[key]["volume"] 41 | if cur_vol > max_voronoi_polyhedra_vol: 42 | max_voronoi_polyhedra_vol = cur_vol 43 | return max_voronoi_polyhedra_vol 44 | 45 | max_voronoi_polyhedra_vol = 0 46 | try: 47 | max_voronoi_polyhedra_vol = get_max_voronoi_polyhedra_volume(structure) 48 | except Exception as e: 49 | if "No Voronoi neighbors found for site - try increasing cutoff".lower() in str(e).lower(): 50 | try: 51 | structure.make_supercell( 52 | 2 53 | ) # to circumvent weird issue with voronoi class, though this decreases performance significantly. 54 | max_voronoi_polyhedra_vol = get_max_voronoi_polyhedra_volume(structure) 55 | except Exception: 56 | pass 57 | 58 | if "infinite vertex in the Voronoi construction".lower() in str(e).lower(): 59 | print(f"{str(e)} As a result, this structure is marked as having excess empty space.") 60 | max_voronoi_polyhedra_vol = np.inf 61 | 62 | if (max_voronoi_polyhedra_vol > 25) or (max_voronoi_polyhedra_vol > 5 and max_empty_distance > 7.5): 63 | return True 64 | else: 65 | return False 66 | -------------------------------------------------------------------------------- /pymatgen/io/validation/check_incar.py: -------------------------------------------------------------------------------- 1 | """Validate VASP INCAR files.""" 2 | 3 | from __future__ import annotations 4 | import numpy as np 5 | from pydantic import Field 6 | 7 | from pymatgen.io.validation.common import SETTINGS, BaseValidator 8 | from pymatgen.io.validation.vasp_defaults import InputCategory, VaspParam 9 | 10 | from typing import TYPE_CHECKING 11 | 12 | if TYPE_CHECKING: 13 | from typing import Any 14 | from pymatgen.io.validation.common import VaspFiles 15 | 16 | # TODO: fix ISIF getting overwritten by MP input set. 17 | 18 | 19 | class CheckIncar(BaseValidator): 20 | """ 21 | Check calculation parameters related to INCAR input tags. 22 | 23 | Because this class checks many INCAR tags in sequence, while it 24 | inherits from the `pymatgen.io.validation.common.BaseValidator` 25 | class, it also defines a custom `check` method. 26 | 27 | Note about `fft_grid_tolerance`: 28 | Directly calculating the FFT grid defaults from VASP is actually impossible 29 | without information on how VASP was compiled. This is because the FFT 30 | params generated depend on whatever fft library used. So instead, we do our 31 | best to calculate the FFT grid defaults and then lower it artificially by 32 | `fft_grid_tolerance`. So if the user's FFT grid parameters are greater than 33 | (fft_grid_tolerance x slightly-off defaults), the FFT params are marked 34 | as valid. 35 | """ 36 | 37 | name: str = "Check INCAR tags" 38 | fft_grid_tolerance: float | None = Field( 39 | SETTINGS.VASP_FFT_GRID_TOLERANCE, description="Tolerance for determining sufficient density of FFT grid." 40 | ) 41 | bandgap_tol: float = Field(1.0e-4, description="Tolerance for assuming a material has no gap.") 42 | 43 | def check(self, vasp_files: VaspFiles, reasons: list[str], warnings: list[str]) -> None: 44 | """ 45 | Check calculation parameters related to INCAR input tags. 46 | 47 | This first updates any parameter with a specified update method. 48 | In practice, each INCAR tag in `VASP` has a "tag" attribute. 49 | If there is an update method 50 | `UpdateParameterValues.update_{tag}_params`, 51 | all parameters with that tag will be updated. 52 | 53 | Then after all missing values in the supplied parameters (padding 54 | implicit values with their defaults), this checks whether the user- 55 | supplied/-parsed parameters satisfy a set of operations against the 56 | reference valid input set. 57 | """ 58 | 59 | # Instantiate class that updates "dynamic" INCAR tags 60 | # (like NBANDS, or hybrid-related parameters) 61 | 62 | user_incar_params, valid_incar_params = self.update_parameters_and_defaults(vasp_files) 63 | msgs = { 64 | "reason": reasons, 65 | "warning": warnings, 66 | } 67 | # Validate each parameter in the set of working parameters 68 | for vasp_param in self.vasp_defaults.values(): 69 | if self.fast and len(reasons) > 0: 70 | # fast check: stop checking whenever a single check fails 71 | break 72 | resp = vasp_param.check(user_incar_params[vasp_param.name], valid_incar_params[vasp_param.name]) 73 | msgs[vasp_param.severity].extend(resp.get(vasp_param.severity, [])) 74 | 75 | def update_parameters_and_defaults(self, vasp_files: VaspFiles) -> tuple[dict[str, Any], dict[str, Any]]: 76 | """Update a set of parameters according to supplied rules and defaults. 77 | 78 | While many of the parameters in VASP need only a simple check to determine 79 | validity with respect to Materials Project parameters, a few are updated 80 | by VASP when other conditions are met. 81 | 82 | For example, if LDAU is set to False, none of the various LDAU* (LDAUU, LDAUJ, 83 | LDAUL) tags need validation. But if LDAU is set to true, these all need validation. 84 | 85 | Another example is NBANDS, which VASP computes from a set of input tags. 86 | This class allows one to mimic the VASP NBANDS functionality for computing 87 | NBANDS dynamically, and update both the current and reference values for NBANDs. 88 | 89 | To do this in a simple, automatic fashion, each parameter in `VASP_DEFAULTS` has 90 | a "tag" field. To update a set of parameters with a given tag, one then adds a function 91 | to `GetParams` called `update_{tag}_params`. For example, the "dft plus u" 92 | tag has an update function called `update_dft_plus_u_params`. If no such update method 93 | exists, that tag is skipped. 94 | """ 95 | 96 | # Note: we cannot make these INCAR objects because INCAR checks certain keys 97 | # Like LREAL and forces them to bool when the validator expects them to be str 98 | user_incar = {k: v for k, v in vasp_files.user_input.incar.as_dict().items() if not k.startswith("@")} 99 | ref_incar = {k: v for k, v in vasp_files.valid_input_set.incar.as_dict().items() if not k.startswith("@")} 100 | 101 | self.add_defaults_to_parameters(user_incar, ref_incar) 102 | # collect list of tags in parameter defaults 103 | for tag in InputCategory.__members__: 104 | # check to see if update method for that tag exists, and if so, run it 105 | update_method_str = f"_update_{tag}_params" 106 | if hasattr(self, update_method_str): 107 | getattr(self, update_method_str)(user_incar, ref_incar, vasp_files) 108 | 109 | # add defaults to parameters from the defaults as needed 110 | self.add_defaults_to_parameters(user_incar, ref_incar) 111 | 112 | return user_incar, ref_incar 113 | 114 | def add_defaults_to_parameters(self, *incars) -> None: 115 | """ 116 | Update parameters with initial defaults. 117 | """ 118 | for key in self.vasp_defaults: 119 | for incar in incars: 120 | if (incar.get(key)) is None: 121 | incar[key] = self.vasp_defaults[key].value 122 | 123 | def _update_dft_plus_u_params(self, user_incar: dict, ref_incar: dict, vasp_files: VaspFiles) -> None: 124 | """Update DFT+U params.""" 125 | if not user_incar["LDAU"]: 126 | return 127 | 128 | for key in [v.name for v in self.vasp_defaults.values() if v.tag == "dft_plus_u"]: 129 | 130 | # TODO: ADK: is LDAUTYPE usually specified as a list?? 131 | if key == "LDAUTYPE": 132 | user_incar[key] = user_incar[key][0] if isinstance(user_incar[key], list) else user_incar[key] 133 | if isinstance(ref_incar[key], list): 134 | ref_incar[key] = ref_incar[key][0] 135 | 136 | self.vasp_defaults[key].operation = "==" 137 | 138 | def _update_symmetry_params(self, user_incar: dict, ref_incar: dict, vasp_files: VaspFiles) -> None: 139 | """Update symmetry-related parameters.""" 140 | # ISYM. 141 | ref_incar["ISYM"] = [-1, 0, 1, 2] 142 | if user_incar["LHFCALC"]: 143 | self.vasp_defaults["ISYM"].value = 3 144 | ref_incar["ISYM"].append(3) 145 | self.vasp_defaults["ISYM"].operation = "in" 146 | 147 | # SYMPREC. 148 | # custodian will set SYMPREC to a maximum of 1e-3 (as of August 2023) 149 | ref_incar["SYMPREC"] = 1e-3 150 | self.vasp_defaults["SYMPREC"].operation = "<=" 151 | self.vasp_defaults["SYMPREC"].comment = ( 152 | "If you believe that this SYMPREC value is necessary " 153 | "(perhaps this calculation has a very large cell), please create " 154 | "a GitHub issue and we will consider to admit your calculations." 155 | ) 156 | 157 | def _update_startup_params(self, user_incar: dict, ref_incar: dict, vasp_files: VaspFiles) -> None: 158 | """Update VASP initialization parameters.""" 159 | ref_incar["ISTART"] = [0, 1, 2] 160 | 161 | # ICHARG. 162 | if ref_incar.get("ICHARG", self.vasp_defaults["ICHARG"].value) < 10: 163 | ref_incar["ICHARG"] = 9 # should be <10 (SCF calcs) 164 | self.vasp_defaults["ICHARG"].operation = "<=" 165 | else: 166 | ref_incar["ICHARG"] = ref_incar.get("ICHARG") 167 | self.vasp_defaults["ICHARG"].operation = "==" 168 | 169 | def _update_precision_params(self, user_incar: dict, ref_incar: dict, vasp_files: VaspFiles) -> None: 170 | """Update VASP parameters related to precision.""" 171 | # LREAL. 172 | # Do NOT use the value for LREAL from the `Vasprun.parameters` object, as VASP changes these values 173 | # relative to the INCAR. Rather, check the LREAL value in the `Vasprun.incar` object. 174 | if str(ref_incar.get("LREAL")).upper() in ["AUTO", "A"]: 175 | ref_incar["LREAL"] = ["FALSE", "AUTO", "A"] 176 | elif str(ref_incar.get("LREAL")).upper() in ["FALSE"]: 177 | ref_incar["LREAL"] = ["FALSE"] 178 | 179 | user_incar["LREAL"] = str(user_incar["LREAL"]).upper() 180 | # PREC. 181 | user_incar["PREC"] = user_incar["PREC"].upper() 182 | if ref_incar["PREC"].upper() in {"ACCURATE", "HIGH"}: 183 | ref_incar["PREC"] = ["ACCURATE", "ACCURA", "HIGH"] 184 | else: 185 | raise ValueError("Validation code check for PREC tag needs to be updated to account for a new input set!") 186 | self.vasp_defaults["PREC"].operation = "in" 187 | 188 | # ROPT. Should be better than or equal to default for the PREC level. 189 | # This only matters if projectors are done in real-space. 190 | # Note that if the user sets LREAL = Auto in their Incar, it will show 191 | # up as "True" in the `parameters` object (hence we use the `parameters` object) 192 | # According to VASP wiki (https://www.vasp.at/wiki/index.php/ROPT), only 193 | # the magnitude of ROPT is relevant for precision. 194 | if user_incar["LREAL"] == "TRUE": 195 | # this only matters if projectors are done in real-space. 196 | cur_prec = user_incar["PREC"].upper() 197 | ropt_default = { 198 | "NORMAL": -5e-4, 199 | "ACCURATE": -2.5e-4, 200 | "ACCURA": -2.5e-4, 201 | "LOW": -0.01, 202 | "MED": -0.002, 203 | "HIGH": -4e-4, 204 | } 205 | user_incar["ROPT"] = [abs(value) for value in user_incar.get("ROPT", [ropt_default[cur_prec]])] 206 | self.vasp_defaults["ROPT"] = VaspParam( 207 | name="ROPT", 208 | value=[abs(ropt_default[cur_prec]) for _ in user_incar["ROPT"]], 209 | tag="startup", 210 | operation=["<=" for _ in user_incar["ROPT"]], 211 | ) 212 | 213 | def _update_misc_special_params(self, user_incar: dict, ref_incar: dict, vasp_files: VaspFiles) -> None: 214 | """Update miscellaneous parameters that do not fall into another category.""" 215 | # EFERMI. Only available for VASP >= 6.4. Should not be set to a numerical 216 | # value, as this may change the number of electrons. 217 | # self.vasp_version = (major, minor, patch) 218 | if vasp_files.vasp_version and (vasp_files.vasp_version[0] >= 6) and (vasp_files.vasp_version[1] >= 4): 219 | # Must check EFERMI in the *incar*, as it is saved as a numerical 220 | # value after VASP guesses it in the vasprun.xml `parameters` 221 | # (which would always cause this check to fail, even if the user 222 | # set EFERMI properly in the INCAR). 223 | ref_incar["EFERMI"] = ["LEGACY", "MIDGAP"] 224 | self.vasp_defaults["EFERMI"].operation = "in" 225 | 226 | # IWAVPR. 227 | if user_incar.get("IWAVPR"): 228 | self.vasp_defaults["IWAVPR"].operation = "==" 229 | self.vasp_defaults["IWAVPR"].comment = ( 230 | "VASP discourages users from setting the IWAVPR tag (as of July 2023)." 231 | ) 232 | 233 | # LCORR. 234 | if user_incar["IALGO"] != 58: 235 | self.vasp_defaults["LCORR"].operation = "==" 236 | 237 | if ( 238 | user_incar["ISPIN"] == 2 239 | and vasp_files.outcar 240 | and len(getattr(vasp_files.outcar, "magnetization", [])) != vasp_files.user_input.structure.num_sites 241 | ): 242 | self.vasp_defaults["LORBIT"].update( 243 | { 244 | "operation": "auto fail", 245 | "comment": ( 246 | "Magnetization values were not written " 247 | "to the OUTCAR. This is usually due to LORBIT being set to None or " 248 | "False for calculations with ISPIN=2." 249 | ), 250 | } 251 | ) 252 | 253 | if ( 254 | vasp_files.vasp_version 255 | and (vasp_files.vasp_version[0] < 6) 256 | and user_incar["LORBIT"] >= 11 257 | and user_incar["ISYM"] 258 | ): 259 | self.vasp_defaults["LORBIT"]["warning"] = ( 260 | "For LORBIT >= 11 and ISYM = 2 the partial charge densities are not correctly symmetrized and can result " 261 | "in different charges for symmetrically equivalent partial charge densities. This issue is fixed as of version " 262 | ">=6. See the vasp wiki page for LORBIT for more details." 263 | ) 264 | 265 | # RWIGS and VCA - do not set 266 | for key in ["RWIGS", "VCA"]: 267 | aux_str = "" 268 | if key == "RWIGS": 269 | aux_str = " This is because it will change some outputs like the magmom on each site." 270 | self.vasp_defaults[key] = VaspParam( 271 | name=key, 272 | value=[self.vasp_defaults[key].value[0] for _ in user_incar[key]], 273 | tag="misc_special", 274 | operation=["==" for _ in user_incar[key]], 275 | comment=f"{key} should not be set. {aux_str}", 276 | ) 277 | ref_incar[key] = self.vasp_defaults[key].value 278 | 279 | def _update_hybrid_params(self, user_incar: dict, ref_incar: dict, vasp_files: VaspFiles) -> None: 280 | """Update params related to hybrid functionals.""" 281 | ref_incar["LHFCALC"] = ref_incar.get("LHFCALC", self.vasp_defaults["LHFCALC"].value) 282 | 283 | if ref_incar["LHFCALC"]: 284 | self.vasp_defaults["AEXX"].value = 0.25 285 | user_incar["AEXX"] = user_incar.get("AEXX", self.vasp_defaults["AEXX"].value) 286 | self.vasp_defaults["AGGAC"].value = 0.0 287 | for key in ("AGGAX", "ALDAX", "AMGGAX"): 288 | self.vasp_defaults[key].value = 1.0 - user_incar["AEXX"] 289 | 290 | if user_incar.get("AEXX", self.vasp_defaults["AEXX"].value) == 1.0: 291 | self.vasp_defaults["ALDAC"].value = 0.0 292 | self.vasp_defaults["AMGGAC"].value = 0.0 293 | 294 | for key in [v.name for v in self.vasp_defaults.values() if v.tag == "hybrid"]: 295 | self.vasp_defaults[key]["operation"] = "==" if isinstance(self.vasp_defaults[key].value, bool) else "approx" 296 | 297 | def _update_fft_params(self, user_incar: dict, ref_incar: dict, vasp_files: VaspFiles) -> None: 298 | """Update ENCUT and parameters related to the FFT grid.""" 299 | 300 | # ensure that ENCUT is appropriately updated 301 | user_incar["ENMAX"] = user_incar.get("ENCUT", getattr(vasp_files.vasprun, "parameters", {}).get("ENMAX")) 302 | 303 | ref_incar["ENMAX"] = vasp_files.valid_input_set.incar.get("ENCUT", self.vasp_defaults["ENMAX"]) 304 | 305 | grid_keys = {"NGX", "NGXF", "NGY", "NGYF", "NGZ", "NGZF"} 306 | # NGX/Y/Z and NGXF/YF/ZF. Not checked if not in INCAR file (as this means the VASP default was used). 307 | if any(i for i in grid_keys if i in user_incar.keys()): 308 | enmaxs = [user_incar["ENMAX"], ref_incar["ENMAX"]] 309 | ref_incar["ENMAX"] = max([v for v in enmaxs if v < float("inf")]) 310 | 311 | if fft_grid := vasp_files.valid_input_set._calculate_ng(custom_encut=ref_incar["ENMAX"]): 312 | ( 313 | [ 314 | ref_incar["NGX"], 315 | ref_incar["NGY"], 316 | ref_incar["NGZ"], 317 | ], 318 | [ 319 | ref_incar["NGXF"], 320 | ref_incar["NGYF"], 321 | ref_incar["NGZF"], 322 | ], 323 | ) = fft_grid 324 | 325 | for key in grid_keys: 326 | ref_incar[key] = int(ref_incar[key] * self.fft_grid_tolerance) 327 | 328 | self.vasp_defaults[key] = VaspParam( 329 | name=key, 330 | value=ref_incar[key], 331 | tag="fft", 332 | operation=">=", 333 | comment=( 334 | "This likely means the number FFT grid points was modified by the user. " 335 | "If not, please create a GitHub issue." 336 | ), 337 | ) 338 | 339 | def _update_density_mixing_params(self, user_incar: dict, ref_incar: dict, vasp_files: VaspFiles) -> None: 340 | """ 341 | Check that LMAXMIX and LMAXTAU are above the required value. 342 | 343 | Also ensure that they are not greater than 6, as that is inadvisable 344 | according to the VASP development team (as of August 2023). 345 | """ 346 | 347 | ref_incar["LMAXTAU"] = min(ref_incar["LMAXMIX"] + 2, 6) 348 | 349 | for key in ["LMAXMIX", "LMAXTAU"]: 350 | if key == "LMAXTAU" and user_incar["METAGGA"] in ["--", None, "None"]: 351 | continue 352 | 353 | if user_incar[key] > 6: 354 | self.vasp_defaults[key].comment = ( 355 | f"From empirical testing, using {key} > 6 appears " 356 | "to introduce computational instabilities, and is currently inadvisable " 357 | "according to the VASP development team." 358 | ) 359 | 360 | # Either add to reasons or warnings depending on task type (as this affects NSCF calcs the most) 361 | # @ Andrew Rosen, is this an adequate check? Or should we somehow also be checking for cases where 362 | # a previous SCF calc used the wrong LMAXMIX too? 363 | if ( 364 | not any( 365 | [ 366 | vasp_files.run_type == "nonscf", 367 | user_incar["ICHARG"] >= 10, 368 | ] 369 | ) 370 | and key == "LMAXMIX" 371 | ): 372 | self.vasp_defaults[key].severity = "warning" 373 | 374 | if ref_incar[key] < 6: 375 | ref_incar[key] = [ref_incar[key], 6] 376 | self.vasp_defaults[key].operation = [">=", "<="] 377 | user_incar[key] = [user_incar[key], user_incar[key]] 378 | else: 379 | self.vasp_defaults[key].operation = "==" 380 | 381 | def _update_smearing_params(self, user_incar: dict, ref_incar: dict, vasp_files: VaspFiles) -> None: 382 | """ 383 | Update parameters related to Fermi-level smearing. 384 | 385 | This is based on the final bandgap obtained in the calc. 386 | """ 387 | if vasp_files.bandgap is not None: 388 | 389 | smearing_comment = ( 390 | f"This is flagged as incorrect because this calculation had a bandgap of {round(vasp_files.bandgap,3)}" 391 | ) 392 | 393 | # bandgap_tol taken from 394 | # https://github.com/materialsproject/pymatgen/blob/1f98fa21258837ac174105e00e7ac8563e119ef0/pymatgen/io/vasp/sets.py#L969 395 | if vasp_files.bandgap > self.bandgap_tol: 396 | ref_incar["ISMEAR"] = [-5, 0] 397 | ref_incar["SIGMA"] = 0.05 398 | else: 399 | ref_incar["ISMEAR"] = [-1, 0, 1, 2] 400 | if user_incar["NSW"] == 0: 401 | # ISMEAR = -5 is valid for metals *only* when doing static calc 402 | ref_incar["ISMEAR"].append(-5) 403 | smearing_comment += " and is a static calculation" 404 | else: 405 | smearing_comment += " and is a non-static calculation" 406 | ref_incar["SIGMA"] = 0.2 407 | 408 | smearing_comment += "." 409 | 410 | for key in ["ISMEAR", "SIGMA"]: 411 | self.vasp_defaults[key].comment = smearing_comment 412 | 413 | if user_incar["ISMEAR"] not in [-5, -4, -2]: 414 | self.vasp_defaults["SIGMA"].operation = "<=" 415 | 416 | else: 417 | # These are generally applicable in all cases. Loosen check to warning. 418 | ref_incar["ISMEAR"] = [-1, 0] 419 | if vasp_files.run_type == "static": 420 | ref_incar["ISMEAR"] += [-5] 421 | elif vasp_files.run_type == "relax": 422 | self.vasp_defaults["ISMEAR"].comment = ( 423 | "Performing relaxations in metals with the tetrahedron method " 424 | "may lead to significant errors in forces. To enable this check, " 425 | "supply a vasprun.xml file." 426 | ) 427 | self.vasp_defaults["ISMEAR"].severity = "warning" 428 | 429 | # Also check if SIGMA is too large according to the VASP wiki, 430 | # which occurs when the entropy term in the energy is greater than 1 meV/atom. 431 | user_incar["ELECTRONIC ENTROPY"] = -1e20 432 | if vasp_files.vasprun: 433 | for ionic_step in vasp_files.vasprun.ionic_steps: 434 | if eentropy := ionic_step["electronic_steps"][-1].get("eentropy"): 435 | user_incar["ELECTRONIC ENTROPY"] = max( 436 | user_incar["ELECTRONIC ENTROPY"], 437 | abs(eentropy / vasp_files.user_input.structure.num_sites), 438 | ) 439 | 440 | convert_eV_to_meV = 1000 441 | user_incar["ELECTRONIC ENTROPY"] = round(user_incar["ELECTRONIC ENTROPY"] * convert_eV_to_meV, 3) 442 | ref_incar["ELECTRONIC ENTROPY"] = 0.001 * convert_eV_to_meV 443 | 444 | self.vasp_defaults["ELECTRONIC ENTROPY"] = VaspParam( 445 | name="ELECTRONIC ENTROPY", 446 | value=0.0, 447 | tag="smearing", 448 | comment=( 449 | "The entropy term (T*S) in the energy is suggested to be less than " 450 | f"{round(ref_incar['ELECTRONIC ENTROPY'], 1)} meV/atom " 451 | f"in the VASP wiki. Thus, SIGMA should be decreased." 452 | ), 453 | operation="<=", 454 | ) 455 | 456 | def _get_default_nbands(self, nelect: float, user_incar: dict, vasp_files: VaspFiles): 457 | """ 458 | Estimate number of bands used in calculation. 459 | 460 | This method is copied from the `estimate_nbands` function in pymatgen.io.vasp.sets.py. 461 | The only noteworthy changes (should) be that there is no reliance on the user setting 462 | up the psp_resources for pymatgen. 463 | """ 464 | nions = len(vasp_files.user_input.structure.sites) 465 | 466 | if user_incar["ISPIN"] == 1: 467 | nmag = 0 468 | else: 469 | nmag = sum(user_incar.get("MAGMOM", [0])) 470 | nmag = np.floor((nmag + 1) / 2) 471 | 472 | possible_val_1 = np.floor((nelect + 2) / 2) + max(np.floor(nions / 2), 3) 473 | possible_val_2 = np.floor(nelect * 0.6) 474 | 475 | default_nbands = max(possible_val_1, possible_val_2) + nmag 476 | 477 | if user_incar.get("LNONCOLLINEAR"): 478 | default_nbands = default_nbands * 2 479 | 480 | if vasp_files.vasprun and (npar := vasp_files.vasprun.parameters.get("NPAR")): 481 | default_nbands = (np.floor((default_nbands + npar - 1) / npar)) * npar 482 | 483 | return int(default_nbands) 484 | 485 | def _update_electronic_params(self, user_incar: dict, ref_incar: dict, vasp_files: VaspFiles): 486 | """Update electronic self-consistency parameters.""" 487 | # ENINI. Only check for IALGO = 48 / ALGO = VeryFast, as this is the only algo that uses this tag. 488 | if user_incar["IALGO"] == 48: 489 | ref_incar["ENINI"] = ref_incar["ENMAX"] 490 | self.vasp_defaults["ENINI"].operation = ">=" 491 | 492 | # ENAUG. Should only be checked for calculations where the relevant MP input set specifies ENAUG. 493 | # In that case, ENAUG should be the same or greater than in valid_input_set. 494 | if ref_incar.get("ENAUG") and not np.isinf(ref_incar["ENAUG"]): 495 | self.vasp_defaults["ENAUG"].operation = ">=" 496 | 497 | # IALGO. 498 | ref_incar["IALGO"] = [38, 58, 68, 90] 499 | # TODO: figure out if 'normal' algos every really affect results other than convergence 500 | 501 | # NELECT. 502 | # Do not check for non-neutral NELECT if NELECT is not in the INCAR 503 | if vasp_files.vasprun and (nelect := vasp_files.vasprun.parameters.get("NELECT")): 504 | ref_incar["NELECT"] = 0.0 505 | try: 506 | user_incar["NELECT"] = float(vasp_files.vasprun.final_structure._charge or 0.0) 507 | self.vasp_defaults["NELECT"].operation = "approx" 508 | self.vasp_defaults["NELECT"].comment = ( 509 | f"This causes the structure to have a charge of {user_incar['NELECT']}. " 510 | f"NELECT should be set to {nelect + user_incar['NELECT']} instead." 511 | ) 512 | except Exception: 513 | self.vasp_defaults["NELECT"] = VaspParam( 514 | name="NELECT", 515 | value=None, 516 | tag="electronic", 517 | operation="auto fail", 518 | severity="warning", 519 | alias="NELECT / POTCAR", 520 | comment=( 521 | "Issue checking whether NELECT was changed to make " 522 | "the structure have a non-zero charge. This is likely due to the " 523 | "directory not having a POTCAR file." 524 | ), 525 | ) 526 | 527 | # NBANDS. 528 | min_nbands = int(np.ceil(nelect / 2) + 1) 529 | self.vasp_defaults["NBANDS"] = VaspParam( 530 | name="NBANDS", 531 | value=self._get_default_nbands(nelect, user_incar, vasp_files), 532 | tag="electronic", 533 | operation=[">=", "<="], 534 | comment=( 535 | "Too many or too few bands can lead to unphysical electronic structure " 536 | "(see https://github.com/materialsproject/custodian/issues/224 " 537 | "for more context.)" 538 | ), 539 | ) 540 | ref_incar["NBANDS"] = [min_nbands, 4 * self.vasp_defaults["NBANDS"].value] 541 | user_incar["NBANDS"] = [vasp_files.vasprun.parameters.get("NBANDS") for _ in range(2)] 542 | 543 | def _update_ionic_params(self, user_incar: dict, ref_incar: dict, vasp_files: VaspFiles): 544 | """Update parameters related to ionic relaxation.""" 545 | 546 | ref_incar["ISIF"] = 2 547 | 548 | # IBRION. 549 | ref_incar["IBRION"] = [-1, 1, 2] 550 | if (inp_set_ibrion := vasp_files.valid_input_set.incar.get("IBRION")) and inp_set_ibrion not in ref_incar[ 551 | "IBRION" 552 | ]: 553 | ref_incar["IBRION"].append(inp_set_ibrion) 554 | 555 | ionic_steps = [] 556 | if vasp_files.vasprun is not None: 557 | ionic_steps = vasp_files.vasprun.ionic_steps 558 | 559 | # POTIM. 560 | if user_incar["IBRION"] in [1, 2, 3, 5, 6]: 561 | # POTIM is only used for some IBRION values 562 | ref_incar["POTIM"] = 5 563 | self.vasp_defaults["POTIM"].operation = "<=" 564 | self.vasp_defaults["POTIM"].comment = "POTIM being so high will likely lead to erroneous results." 565 | 566 | # Check for large changes in energy between ionic steps (usually indicates too high POTIM) 567 | if len(ionic_steps) > 1: 568 | # Do not use `e_0_energy`, as there is a bug in the vasprun.xml when printing that variable 569 | # (see https://www.vasp.at/forum/viewtopic.php?t=16942 for more details). 570 | cur_ionic_step_energies = [ionic_step["e_fr_energy"] for ionic_step in ionic_steps] 571 | cur_ionic_step_energy_gradient = np.diff(cur_ionic_step_energies) 572 | user_incar["MAX ENERGY GRADIENT"] = round( 573 | max(np.abs(cur_ionic_step_energy_gradient)) / vasp_files.user_input.structure.num_sites, 574 | 3, 575 | ) 576 | ref_incar["MAX ENERGY GRADIENT"] = 1 577 | self.vasp_defaults["MAX ENERGY GRADIENT"] = VaspParam( 578 | name="MAX ENERGY GRADIENT", 579 | value=None, 580 | tag="ionic", 581 | operation="<=", 582 | comment=( 583 | f"The energy changed by a maximum of {user_incar['MAX ENERGY GRADIENT']} eV/atom " 584 | "between ionic steps; this indicates that POTIM is too high." 585 | ), 586 | ) 587 | 588 | if not ionic_steps: 589 | return 590 | 591 | # EDIFFG. 592 | # Should be the same or smaller than in valid_input_set. Force-based cutoffs (not in every 593 | # every MP-compliant input set, but often have comparable or even better results) will also be accepted 594 | # I am **NOT** confident that this should be the final check. Perhaps I need convincing (or perhaps it does indeed need to be changed...) 595 | # TODO: -somehow identify if a material is a vdW structure, in which case force-convergence should maybe be more strict? 596 | self.vasp_defaults["EDIFFG"] = VaspParam( 597 | name="EDIFFG", 598 | value=10 * ref_incar["EDIFF"], 599 | tag="ionic", 600 | operation=None, 601 | ) 602 | 603 | ref_incar["EDIFFG"] = ref_incar.get("EDIFFG", self.vasp_defaults["EDIFFG"].value) 604 | self.vasp_defaults["EDIFFG"].comment = ( 605 | "The structure is not force-converged according " 606 | f"to |EDIFFG|={abs(ref_incar['EDIFFG'])} (or smaller in magnitude)." 607 | ) 608 | 609 | if ionic_steps[-1].get("forces") is None: 610 | self.vasp_defaults["EDIFFG"].comment = ( 611 | "vasprun.xml does not contain forces, cannot check force convergence." 612 | ) 613 | self.vasp_defaults["EDIFFG"].severity = "warning" 614 | self.vasp_defaults["EDIFFG"].operation = "auto fail" 615 | 616 | elif ref_incar["EDIFFG"] < 0.0 and (vrun_forces := ionic_steps[-1].get("forces")) is not None: 617 | user_incar["EDIFFG"] = round( 618 | max([np.linalg.norm(force_on_atom) for force_on_atom in vrun_forces]), 619 | 3, 620 | ) 621 | 622 | ref_incar["EDIFFG"] = abs(ref_incar["EDIFFG"]) 623 | self.vasp_defaults["EDIFFG"] = VaspParam( 624 | name="EDIFFG", 625 | value=self.vasp_defaults["EDIFFG"].value, 626 | tag="ionic", 627 | operation="<=", 628 | alias="MAX FINAL FORCE MAGNITUDE", 629 | ) 630 | 631 | # the latter two checks just ensure the code does not error by indexing out of range 632 | elif ref_incar["EDIFFG"] > 0.0 and vasp_files.vasprun and len(ionic_steps) > 1: 633 | energy_of_last_step = ionic_steps[-1]["e_0_energy"] 634 | energy_of_second_to_last_step = ionic_steps[-2]["e_0_energy"] 635 | user_incar["EDIFFG"] = abs(energy_of_last_step - energy_of_second_to_last_step) 636 | self.vasp_defaults["EDIFFG"].operation = "<=" 637 | self.vasp_defaults["EDIFFG"].alias = "ENERGY CHANGE BETWEEN LAST TWO IONIC STEPS" 638 | 639 | def _update_post_init_params(self, user_incar: dict, ref_incar: dict, vasp_files: VaspFiles): 640 | """Update any params that depend on other params being set/updated.""" 641 | 642 | # EBREAK 643 | # vasprun includes default EBREAK value, so we check ionic steps 644 | # to see if the user set a value for EBREAK. 645 | # Note that the NBANDS estimation differs from VASP's documentation, 646 | # so we can't check the vasprun value directly 647 | if user_incar.get("EBREAK"): 648 | self.vasp_defaults["EBREAK"].value = self.vasp_defaults["EDIFF"].value / ( 649 | 4.0 * self.vasp_defaults["NBANDS"].value 650 | ) 651 | self.vasp_defaults["EBREAK"].operation = "auto fail" 652 | -------------------------------------------------------------------------------- /pymatgen/io/validation/check_kpoints_kspacing.py: -------------------------------------------------------------------------------- 1 | """Validate VASP KPOINTS files or the KSPACING/KGAMMA INCAR settings.""" 2 | 3 | from __future__ import annotations 4 | from pydantic import Field 5 | from typing import TYPE_CHECKING 6 | import numpy as np 7 | 8 | from pymatgen.io.validation.common import SETTINGS, BaseValidator 9 | 10 | if TYPE_CHECKING: 11 | from pymatgen.core import Structure 12 | from pymatgen.io.validation.common import VaspFiles 13 | 14 | 15 | def get_kpoint_divisions_from_kspacing(structure: Structure, kspacing: float) -> tuple[int, int, int]: 16 | """ 17 | Determine the number of k-points generated by VASP when KSPACING is set. 18 | 19 | See https://www.vasp.at/wiki/index.php/KSPACING for a discussion. 20 | The 2 pi factor on that page appears to be irrelevant. 21 | 22 | Parameters 23 | ----------- 24 | structure : Structure 25 | kspacing : float 26 | 27 | Returns 28 | ----------- 29 | tuple of int, int, int 30 | The number of k-point divisions along each axis. 31 | """ 32 | return tuple([max(1, int(np.ceil(structure.lattice.reciprocal_lattice.abc[ik] / kspacing))) for ik in range(3)]) # type: ignore[return-value] 33 | 34 | 35 | class CheckKpointsKspacing(BaseValidator): 36 | """Check that k-point density is sufficiently high and is compatible with lattice symmetry.""" 37 | 38 | name: str = "Check k-point density" 39 | kpts_tolerance: float = Field( 40 | SETTINGS.VASP_KPTS_TOLERANCE, 41 | description="Tolerance for evaluating k-point density, to accommodate different the k-point generation schemes across VASP versions.", 42 | ) 43 | allow_explicit_kpoint_mesh: bool | str | None = Field( 44 | SETTINGS.VASP_ALLOW_EXPLICIT_KPT_MESH, 45 | description="Whether to permit explicit generation of k-points (as for a bandstructure calculation).", 46 | ) 47 | allow_kpoint_shifts: bool = Field( 48 | SETTINGS.VASP_ALLOW_KPT_SHIFT, 49 | description="Whether to permit shifting the origin of the k-point mesh from Gamma.", 50 | ) 51 | 52 | def _get_valid_num_kpts( 53 | self, 54 | vasp_files: VaspFiles, 55 | ) -> int: 56 | """ 57 | Get the minimum permitted number of k-points for a structure according to an input set. 58 | 59 | Returns 60 | ----------- 61 | int, the minimum permitted number of k-points, consistent with self.kpts_tolerance 62 | """ 63 | # If MP input set specifies KSPACING in the INCAR 64 | if (kspacing := vasp_files.valid_input_set.incar.get("KSPACING")) and ( 65 | vasp_files.valid_input_set.kpoints is None 66 | ): 67 | valid_kspacing = kspacing 68 | # number of kpoints along each of the three lattice vectors 69 | valid_num_kpts = np.prod( 70 | get_kpoint_divisions_from_kspacing(vasp_files.user_input.structure, valid_kspacing), dtype=int 71 | ) 72 | # If MP input set specifies a KPOINTS file 73 | elif vasp_files.valid_input_set.kpoints: 74 | valid_num_kpts = vasp_files.valid_input_set.kpoints.num_kpts or np.prod( 75 | vasp_files.valid_input_set.kpoints.kpts[0], dtype=int 76 | ) 77 | 78 | return int(np.floor(int(valid_num_kpts) * self.kpts_tolerance)) 79 | 80 | def _check_user_shifted_mesh(self, vasp_files: VaspFiles, reasons: list[str], warnings: list[str]) -> None: 81 | # Check for user shifts 82 | if ( 83 | (not self.allow_kpoint_shifts) 84 | and vasp_files.actual_kpoints 85 | and any(shift_val != 0 for shift_val in vasp_files.actual_kpoints.kpts_shift) 86 | ): # type: ignore[union-attr] 87 | reasons.append("INPUT SETTINGS --> KPOINTS: shifting the kpoint mesh is not currently allowed.") 88 | 89 | def _check_explicit_mesh_permitted(self, vasp_files: VaspFiles, reasons: list[str], warnings: list[str]) -> None: 90 | # Check for explicit kpoint meshes 91 | 92 | if not vasp_files.actual_kpoints: 93 | return 94 | 95 | if isinstance(self.allow_explicit_kpoint_mesh, bool): 96 | allow_explicit = self.allow_explicit_kpoint_mesh 97 | elif self.allow_explicit_kpoint_mesh == "auto": 98 | allow_explicit = vasp_files.run_type == "nonscf" 99 | else: 100 | allow_explicit = False 101 | 102 | if (not allow_explicit) and len(vasp_files.actual_kpoints.kpts) > 1: # type: ignore[union-attr] 103 | reasons.append( 104 | "INPUT SETTINGS --> KPOINTS: explicitly defining " 105 | "the k-point mesh is not currently allowed. " 106 | "Automatic k-point generation is required." 107 | ) 108 | 109 | def _check_kpoint_density(self, vasp_files: VaspFiles, reasons: list[str], warnings: list[str]) -> None: 110 | """ 111 | Check that k-point density is sufficiently high and is compatible with lattice symmetry. 112 | """ 113 | 114 | # Check number of kpoints used 115 | # Checks should work regardless of whether vasprun was supplied. 116 | valid_num_kpts = self._get_valid_num_kpts(vasp_files) 117 | if vasp_files.actual_kpoints: 118 | if vasp_files.actual_kpoints.num_kpts <= 0: 119 | cur_num_kpts = np.prod(vasp_files.actual_kpoints.kpts, dtype=int) 120 | else: 121 | cur_num_kpts = vasp_files.actual_kpoints.num_kpts 122 | else: 123 | cur_num_kpts = np.prod( 124 | get_kpoint_divisions_from_kspacing( 125 | vasp_files.user_input.structure, 126 | vasp_files.user_input.incar.get("KSPACING", self.vasp_defaults["KSPACING"].value), 127 | ), 128 | dtype=int, 129 | ) 130 | 131 | if cur_num_kpts < valid_num_kpts: 132 | reasons.append( 133 | f"INPUT SETTINGS --> KPOINTS or KSPACING: {cur_num_kpts} kpoints were " 134 | f"used, but it should have been at least {valid_num_kpts}." 135 | ) 136 | 137 | def _check_kpoint_mesh_symmetry(self, vasp_files: VaspFiles, reasons: list[str], warnings: list[str]) -> None: 138 | # check for valid kpoint mesh (which depends on symmetry of the structure) 139 | 140 | if vasp_files.actual_kpoints: 141 | cur_kpoint_style = vasp_files.actual_kpoints.style.name.lower() # type: ignore[union-attr] 142 | else: 143 | cur_kpoint_style = ( 144 | "gamma" 145 | if vasp_files.user_input.incar.get("KGAMMA", self.vasp_defaults["KGAMMA"].value) 146 | else "monkhorst" 147 | ) 148 | 149 | is_hexagonal = vasp_files.user_input.structure.lattice.is_hexagonal() 150 | is_face_centered = vasp_files.user_input.structure.get_space_group_info()[0][0] == "F" 151 | monkhorst_mesh_is_invalid = is_hexagonal or is_face_centered 152 | if ( 153 | cur_kpoint_style == "monkhorst" 154 | and monkhorst_mesh_is_invalid 155 | and any(x % 2 == 0 for x in vasp_files.actual_kpoints.kpts[0]) # type: ignore[union-attr] 156 | ): 157 | # only allow Monkhorst with all odd number of subdivisions per axis. 158 | kv = vasp_files.actual_kpoints.kpts[0] # type: ignore[union-attr] 159 | reasons.append( 160 | f"INPUT SETTINGS --> KPOINTS or KGAMMA: ({'×'.join([f'{_k}' for _k in kv])}) " 161 | "Monkhorst-Pack kpoint mesh was used." 162 | "To be compatible with the symmetry of the lattice, " 163 | "a Monkhorst-Pack mesh should have only odd number of " 164 | "subdivisions per axis." 165 | ) 166 | -------------------------------------------------------------------------------- /pymatgen/io/validation/check_package_versions.py: -------------------------------------------------------------------------------- 1 | """Optionally check whether package versions are up to date.""" 2 | 3 | from __future__ import annotations 4 | from importlib.metadata import version 5 | import requests # type: ignore[import-untyped] 6 | import warnings 7 | 8 | 9 | def package_version_check() -> None: 10 | """Warn the user if pymatgen / pymatgen-io-validation is not up-to-date.""" 11 | 12 | packages = { 13 | "pymatgen": "Hence, if any pymatgen input sets have been updated, this validator will be outdated.", 14 | "pymatgen-io-validation": "Hence, if any checks in this package have been updated, the validator you use will be outdated.", 15 | } 16 | 17 | for package, context_msg in packages.items(): 18 | if not is_package_is_up_to_date(package): 19 | warnings.warn( 20 | "We *STRONGLY* recommend you to update your " 21 | f"`{package}` package, which is behind the most " 22 | f"recent version. {context_msg}" 23 | ) 24 | 25 | 26 | def is_package_is_up_to_date(package_name: str) -> bool: 27 | """Check if a package is up-to-date with the PyPI version.""" 28 | 29 | try: 30 | cur_version = version(package_name) 31 | except Exception: 32 | raise ImportError(f"Package `{package_name}` is not installed!") 33 | 34 | try: 35 | response = requests.get(f"https://pypi.org/pypi/{package_name}/json") 36 | latest_version = response.json()["info"]["version"] 37 | except Exception: 38 | raise ImportError(f"Package `{package_name}` does not exist in PyPI!") 39 | 40 | return cur_version == latest_version 41 | -------------------------------------------------------------------------------- /pymatgen/io/validation/check_potcar.py: -------------------------------------------------------------------------------- 1 | """Check POTCAR against known POTCARs in pymatgen, without setting up psp_resources.""" 2 | 3 | from __future__ import annotations 4 | from copy import deepcopy 5 | from functools import cached_property 6 | from pathlib import Path 7 | from pydantic import Field 8 | from importlib.resources import files as import_resource_files 9 | from monty.serialization import loadfn 10 | from typing import TYPE_CHECKING 11 | 12 | from pymatgen.io.vasp import PotcarSingle 13 | 14 | from pymatgen.io.validation.common import BaseValidator, ValidationError 15 | 16 | if TYPE_CHECKING: 17 | from typing import Any 18 | from pymatgen.io.validation.common import VaspFiles 19 | 20 | 21 | class CheckPotcar(BaseValidator): 22 | """ 23 | Check POTCAR against library of known valid POTCARs. 24 | """ 25 | 26 | name: str = "Check POTCAR" 27 | potcar_summary_stats_path: str | Path | None = Field( 28 | str(import_resource_files("pymatgen.io.vasp") / "potcar-summary-stats.json.bz2"), 29 | description="Path to potcar summary data. Mapping is calculation type -> potcar symbol -> summary data.", 30 | ) 31 | data_match_tol: float = Field(1.0e-6, description="Tolerance for matching POTCARs to summary statistics data.") 32 | ignore_header_keys: set[str] | None = Field( 33 | {"copyr", "sha256"}, description="POTCAR summary statistics keywords.header fields to ignore during validation" 34 | ) 35 | 36 | @cached_property 37 | def potcar_summary_stats(self) -> dict: 38 | """Load POTCAR summary statistics file.""" 39 | if self.potcar_summary_stats_path: 40 | return loadfn(self.potcar_summary_stats_path, cls=None) 41 | return {} 42 | 43 | def auto_fail(self, vasp_files: VaspFiles, reasons: list[str], warnings: list[str]) -> bool: 44 | """Skip if no POTCAR was provided, or if summary stats file was unset.""" 45 | 46 | if self.potcar_summary_stats_path is None: 47 | # If no reference summary stats specified, or we're only doing a quick check, 48 | # and there are already failure reasons, return 49 | return True 50 | elif vasp_files.user_input.potcar is None or any( 51 | ps.keywords is None or ps.stats is None for ps in vasp_files.user_input.potcar 52 | ): 53 | reasons.append( 54 | "PSEUDOPOTENTIALS --> Missing POTCAR files. " 55 | "Alternatively, our potcar checker may have an issue--please create a GitHub issue if you " 56 | "know your POTCAR exists and can be read by Pymatgen." 57 | ) 58 | return True 59 | return False 60 | 61 | def _check_potcar_spec(self, vasp_files: VaspFiles, reasons: list[str], warnings: list[str]): 62 | """ 63 | Checks to make sure the POTCAR is equivalent to the correct POTCAR from the pymatgen input set.""" 64 | 65 | if vasp_files.valid_input_set.potcar: 66 | # If the user has pymatgen set up, use the pregenerated POTCAR summary stats. 67 | valid_potcar_summary_stats: dict[str, list[dict[str, Any]]] = { 68 | p.titel.replace(" ", ""): [p.model_dump()] for p in vasp_files.valid_input_set.potcar 69 | } 70 | elif vasp_files.valid_input_set._pmg_vis: 71 | # Fallback, use the stats from pymatgen - only load and cache summary stats here. 72 | psp_subset = self.potcar_summary_stats.get(vasp_files.valid_input_set.potcar_functional, {}) 73 | 74 | valid_potcar_summary_stats = {} 75 | for element in vasp_files.user_input.structure.composition.remove_charges().as_dict(): 76 | potcar_symbol = vasp_files.valid_input_set._pmg_vis._config_dict["POTCAR"][element] 77 | for titel_no_spc in psp_subset: 78 | for psp in psp_subset[titel_no_spc]: 79 | if psp["symbol"] == potcar_symbol: 80 | if titel_no_spc not in valid_potcar_summary_stats: 81 | valid_potcar_summary_stats[titel_no_spc] = [] 82 | valid_potcar_summary_stats[titel_no_spc].append(psp) 83 | else: 84 | raise ValidationError("Could not determine reference POTCARs.") 85 | 86 | try: 87 | incorrect_potcars: list[str] = [] 88 | for potcar in vasp_files.user_input.potcar: # type: ignore[union-attr] 89 | reference_summary_stats = valid_potcar_summary_stats.get(potcar.titel.replace(" ", ""), []) 90 | potcar_symbol = potcar.titel.split(" ")[1] 91 | 92 | if len(reference_summary_stats) == 0: 93 | incorrect_potcars.append(potcar_symbol) 94 | continue 95 | 96 | for _ref_psp in reference_summary_stats: 97 | user_summary_stats = potcar.model_dump() 98 | ref_psp = deepcopy(_ref_psp) 99 | for _set in (user_summary_stats, ref_psp): 100 | _set["keywords"]["header"] = set(_set["keywords"]["header"]).difference(self.ignore_header_keys) # type: ignore[arg-type] 101 | if found_match := PotcarSingle.compare_potcar_stats( 102 | ref_psp, user_summary_stats, tolerance=self.data_match_tol 103 | ): 104 | break 105 | 106 | if not found_match: 107 | incorrect_potcars.append(potcar_symbol) 108 | if self.fast: 109 | # quick return, only matters that one POTCAR didn't match 110 | break 111 | 112 | if len(incorrect_potcars) > 0: 113 | # format error string 114 | incorrect_potcars = [potcar.split("_")[0] for potcar in incorrect_potcars] 115 | if len(incorrect_potcars) == 1: 116 | incorrect_potcar_str = incorrect_potcars[0] 117 | else: 118 | incorrect_potcar_str = ( 119 | ", ".join(incorrect_potcars[:-1]) + f", and {incorrect_potcars[-1]}" 120 | ) # type: ignore 121 | 122 | reasons.append( 123 | f"PSEUDOPOTENTIALS --> Incorrect POTCAR files were used for {incorrect_potcar_str}. " 124 | "Alternatively, our potcar checker may have an issue--please create a GitHub issue if you " 125 | "believe the POTCARs used are correct." 126 | ) 127 | 128 | except KeyError: 129 | reasons.append( 130 | "Issue validating POTCARS --> Likely due to an old version of Emmet " 131 | "(wherein potcar summary_stats is not saved in TaskDoc), though " 132 | "other errors have been seen. Hence, it is marked as invalid." 133 | ) 134 | -------------------------------------------------------------------------------- /pymatgen/io/validation/common.py: -------------------------------------------------------------------------------- 1 | """Common class constructor for validation checks.""" 2 | 3 | from __future__ import annotations 4 | 5 | from functools import cached_property 6 | import hashlib 7 | from importlib import import_module 8 | from monty.serialization import loadfn 9 | import os 10 | import numpy as np 11 | from pathlib import Path 12 | from pydantic import BaseModel, Field, model_validator, model_serializer, PrivateAttr 13 | from typing import TYPE_CHECKING, Any, Optional 14 | 15 | from pymatgen.core import Structure 16 | from pymatgen.io.vasp.inputs import POTCAR_STATS_PATH, Incar, Kpoints, Poscar, Potcar 17 | from pymatgen.io.vasp.outputs import Outcar, Vasprun 18 | from pymatgen.io.vasp.sets import VaspInputSet 19 | 20 | from pymatgen.io.validation.vasp_defaults import VaspParam, VASP_DEFAULTS_DICT 21 | from pymatgen.io.validation.settings import IOValidationSettings 22 | 23 | if TYPE_CHECKING: 24 | from typing_extensions import Self 25 | 26 | SETTINGS = IOValidationSettings() 27 | 28 | 29 | class ValidationError(Exception): 30 | """Define custom exception during validation.""" 31 | 32 | 33 | class PotcarSummaryKeywords(BaseModel): 34 | """Schematize `PotcarSingle._summary_stats["keywords"]` field.""" 35 | 36 | header: set[str] = Field(description="The keywords in the POTCAR header.") 37 | data: set[str] = Field(description="The keywords in the POTCAR body.") 38 | 39 | @model_serializer 40 | def set_to_list(self) -> dict[str, list[str]]: 41 | """Ensure JSON compliance of set fields.""" 42 | return {k: list(getattr(self, k)) for k in ("header", "data")} 43 | 44 | 45 | class PotcarSummaryStatisticsFields(BaseModel): 46 | """Define statistics used in `PotcarSingle._summary_stats`.""" 47 | 48 | MEAN: float = Field(description="Data mean.") 49 | ABSMEAN: float = Field(description="Data magnitude mean.") 50 | VAR: float = Field(description="Mean of squares of data.") 51 | MIN: float = Field(description="Data minimum.") 52 | MAX: float = Field(description="Data maximum.") 53 | 54 | 55 | class PotcarSummaryStatistics(BaseModel): 56 | """Schematize `PotcarSingle._summary_stats["stats"]` field.""" 57 | 58 | header: PotcarSummaryStatisticsFields = Field(description="The keywords in the POTCAR header.") 59 | data: PotcarSummaryStatisticsFields = Field(description="The keywords in the POTCAR body.") 60 | 61 | 62 | class PotcarSummaryStats(BaseModel): 63 | """Schematize `PotcarSingle._summary_stats`.""" 64 | 65 | keywords: Optional[PotcarSummaryKeywords] = None 66 | stats: Optional[PotcarSummaryStatistics] = None 67 | titel: str 68 | lexch: str 69 | 70 | @classmethod 71 | def from_file(cls, potcar_path: os.PathLike | Potcar) -> list[Self]: 72 | """Create a list of PotcarSummaryStats from a POTCAR.""" 73 | if isinstance(potcar_path, Potcar): 74 | potcar: Potcar = potcar_path 75 | else: 76 | potcar = Potcar.from_file(str(potcar_path)) 77 | return [cls(**p._summary_stats, titel=p.TITEL, lexch=p.LEXCH) for p in potcar] 78 | 79 | 80 | class LightOutcar(BaseModel): 81 | """Schematic of pymatgen's Outcar.""" 82 | 83 | drift: Optional[list[list[float]]] = Field(None, description="The drift forces.") 84 | magnetization: Optional[list[dict[str, float]]] = Field( 85 | None, description="The on-site magnetic moments, possibly with orbital resolution." 86 | ) 87 | 88 | 89 | class LightVasprun(BaseModel): 90 | """Lightweight version of pymatgen Vasprun.""" 91 | 92 | vasp_version: str = Field(description="The dot-separated version of VASP used.") 93 | ionic_steps: list[dict[str, Any]] = Field(description="The ionic steps in the calculation.") 94 | final_energy: float = Field(description="The final total energy in eV.") 95 | final_structure: Structure = Field(description="The final structure.") 96 | kpoints: Kpoints = Field(description="The actual k-points used in the calculation.") 97 | parameters: dict[str, Any] = Field(description="The default-padded input parameters interpreted by VASP.") 98 | bandgap: float = Field(description="The bandgap - note that this field is derived from the Vasprun object.") 99 | potcar_symbols: Optional[list[str]] = Field( 100 | None, 101 | description="Optional: if a POTCAR is unavailable, this is used to determine the functional used in the calculation.", 102 | ) 103 | 104 | @classmethod 105 | def from_vasprun(cls, vasprun: Vasprun) -> Self: 106 | """ 107 | Create a LightVasprun from a pymatgen Vasprun. 108 | 109 | Parameters 110 | ----------- 111 | vasprun : pymatgen Vasprun 112 | 113 | Returns 114 | ----------- 115 | LightVasprun 116 | """ 117 | return cls( 118 | **{k: getattr(vasprun, k) for k in cls.model_fields if k != "bandgap"}, 119 | bandgap=vasprun.get_band_structure(efermi="smart").get_band_gap()["energy"], 120 | ) 121 | 122 | @model_serializer 123 | def deserialize_objects(self) -> dict[str, Any]: 124 | """Ensure all pymatgen objects are deserialized.""" 125 | model_dumped = {k: getattr(self, k) for k in self.__class__.model_fields} 126 | for k in ("final_structure", "kpoints"): 127 | model_dumped[k] = model_dumped[k].as_dict() 128 | for iion, istep in enumerate(model_dumped["ionic_steps"]): 129 | if (istruct := istep.get("structure")) and isinstance(istruct, Structure): 130 | model_dumped["ionic_steps"][iion]["structure"] = istruct.as_dict() 131 | for k in ("forces", "stress"): 132 | if (val := istep.get(k)) is not None and isinstance(val, np.ndarray): 133 | model_dumped["ionic_steps"][iion][k] = val.tolist() 134 | return model_dumped 135 | 136 | 137 | class VaspInputSafe(BaseModel): 138 | """Stricter VaspInputSet with no POTCAR info.""" 139 | 140 | incar: Incar = Field(description="The INCAR used in the calculation.") 141 | structure: Structure = Field(description="The structure associated with the calculation.") 142 | kpoints: Optional[Kpoints] = Field(None, description="The optional KPOINTS or IBZKPT file used in the calculation.") 143 | potcar: Optional[list[PotcarSummaryStats]] = Field(None, description="The optional POTCAR used in the calculation.") 144 | potcar_functional: Optional[str] = Field(None, description="The pymatgen-labelled POTCAR library release.") 145 | _pmg_vis: Optional[VaspInputSet] = PrivateAttr(None) 146 | 147 | @model_serializer 148 | def deserialize_objects(self) -> dict[str, Any]: 149 | """Ensure all pymatgen objects are deserialized.""" 150 | model_dumped: dict[str, Any] = {} 151 | if self.potcar: 152 | model_dumped["potcar"] = [p.model_dump() for p in self.potcar] 153 | for k in ( 154 | "incar", 155 | "structure", 156 | "kpoints", 157 | ): 158 | if pmg_obj := getattr(self, k): 159 | model_dumped[k] = pmg_obj.as_dict() 160 | return model_dumped 161 | 162 | @classmethod 163 | def from_vasp_input_set(cls, vis: VaspInputSet) -> Self: 164 | """ 165 | Create a VaspInputSafe from a pymatgen VaspInputSet. 166 | 167 | Parameters 168 | ----------- 169 | vasprun : pymatgen VaspInputSet 170 | 171 | Returns 172 | ----------- 173 | VaspInputSafe 174 | """ 175 | 176 | cls_config: dict[str, Any] = { 177 | k: getattr(vis, k) 178 | for k in ( 179 | "incar", 180 | "kpoints", 181 | "structure", 182 | ) 183 | } 184 | try: 185 | # Cleaner solution (because these map one POTCAR symbol to one POTCAR) 186 | # Requires POTCAR library to be available 187 | potcar: list[PotcarSummaryStats] = PotcarSummaryStats.from_file(vis.potcar) 188 | potcar_functional = vis.potcar_functional 189 | 190 | except FileNotFoundError: 191 | # Fall back to pregenerated POTCAR meta 192 | # Note that multiple POTCARs may use the same symbol / TITEL 193 | # within a given release of VASP. 194 | 195 | potcar_stats = loadfn(POTCAR_STATS_PATH) 196 | potcar_functional = vis._config_dict["POTCAR_FUNCTIONAL"] 197 | potcar = [] 198 | for ele in vis.structure.elements: 199 | if potcar_symb := vis._config_dict["POTCAR"].get(ele.name): 200 | for titel_no_spc, potcars in potcar_stats[potcar_functional].items(): 201 | for entry in potcars: 202 | if entry["symbol"] == potcar_symb: 203 | titel_comp = titel_no_spc.split(potcar_symb) 204 | 205 | potcar += [ 206 | PotcarSummaryStats( 207 | titel=" ".join([titel_comp[0], potcar_symb, titel_comp[1]]), 208 | lexch=entry.get("LEXCH"), 209 | **entry, 210 | ) 211 | ] 212 | 213 | cls_config.update( 214 | potcar=potcar, 215 | potcar_functional=potcar_functional, 216 | ) 217 | new_vis = cls(**cls_config) 218 | new_vis._pmg_vis = vis 219 | return new_vis 220 | 221 | def _calculate_ng(self, **kwargs) -> tuple[list[int], list[int]] | None: 222 | """Interface to pymatgen vasp input set as needed.""" 223 | if self._pmg_vis: 224 | return self._pmg_vis.calculate_ng(**kwargs) 225 | return None 226 | 227 | 228 | class VaspFiles(BaseModel): 229 | """Define required and optional files for validation.""" 230 | 231 | user_input: VaspInputSafe = Field(description="The VASP input set used in the calculation.") 232 | outcar: Optional[LightOutcar] = None 233 | vasprun: Optional[LightVasprun] = None 234 | 235 | @model_validator(mode="before") 236 | @classmethod 237 | def coerce_to_lightweight(cls, config: Any) -> Any: 238 | """Ensure that pymatgen objects are converted to minimal representations.""" 239 | if isinstance(config.get("outcar"), Outcar): 240 | config["outcar"] = LightOutcar( 241 | drift=config["outcar"].drift, 242 | magnetization=config["outcar"].magnetization, 243 | ) 244 | 245 | if isinstance(config.get("vasprun"), Vasprun): 246 | config["vasprun"] = LightVasprun.from_vasprun(config["vasprun"]) 247 | return config 248 | 249 | @property 250 | def md5(self) -> str: 251 | """Get MD5 of VaspFiles for use in validation checks.""" 252 | return hashlib.md5(self.model_dump_json().encode()).hexdigest() 253 | 254 | @property 255 | def actual_kpoints(self) -> Kpoints | None: 256 | """The actual KPOINTS / IBZKPT used in the calculation, if applicable.""" 257 | if self.user_input.kpoints: 258 | return self.user_input.kpoints 259 | elif self.vasprun: 260 | return self.vasprun.kpoints 261 | return None 262 | 263 | @property 264 | def vasp_version(self) -> tuple[int, int, int] | None: 265 | """Return the VASP version as a tuple of int, if available.""" 266 | if self.vasprun: 267 | vvn = [int(x) for x in self.vasprun.vasp_version.split(".")] 268 | return (vvn[0], vvn[1], vvn[2]) 269 | return None 270 | 271 | @classmethod 272 | def from_paths( 273 | cls, 274 | incar: str | Path | os.PathLike[str], 275 | poscar: str | Path | os.PathLike[str], 276 | kpoints: str | Path | os.PathLike[str] | None = None, 277 | potcar: str | Path | os.PathLike[str] | None = None, 278 | outcar: str | Path | os.PathLike[str] | None = None, 279 | vasprun: str | Path | os.PathLike[str] | None = None, 280 | ): 281 | """Construct a set of VASP I/O from file paths.""" 282 | config: dict[str, Any] = {"user_input": {}} 283 | _vars = locals() 284 | 285 | to_obj = { 286 | "incar": Incar, 287 | "kpoints": Kpoints, 288 | "poscar": Poscar, 289 | "potcar": PotcarSummaryStats, 290 | "outcar": Outcar, 291 | "vasprun": Vasprun, 292 | } 293 | potcar_enmax = None 294 | for file_name, file_cls in to_obj.items(): 295 | if (path := _vars.get(file_name)) and Path(path).exists(): 296 | if file_name == "poscar": 297 | config["user_input"]["structure"] = Poscar.from_file(path).structure 298 | elif hasattr(file_cls, "from_file"): 299 | config["user_input"][file_name] = file_cls.from_file(path) 300 | else: 301 | config[file_name] = file_cls(path) 302 | 303 | if file_name == "potcar": 304 | potcar_enmax = max(ps.ENMAX for ps in Potcar.from_file(path)) 305 | 306 | if not config.get("vasprun") and not config["user_input"]["incar"].get("ENCUT") and potcar_enmax: 307 | config["user_input"]["incar"]["ENCUT"] = potcar_enmax 308 | 309 | return cls(**config) 310 | 311 | @cached_property 312 | def run_type(self) -> str: 313 | """Get the run type of a calculation.""" 314 | 315 | ibrion = self.user_input.incar.get("IBRION", VASP_DEFAULTS_DICT["IBRION"].value) 316 | if self.user_input.incar.get("NSW", VASP_DEFAULTS_DICT["NSW"].value) > 0 and ibrion == -1: 317 | ibrion = 0 318 | 319 | run_type = { 320 | -1: "static", 321 | 0: "md", 322 | **{k: "relax" for k in range(1, 4)}, 323 | **{k: "phonon" for k in range(5, 9)}, 324 | **{k: "ts" for k in (40, 44)}, 325 | }.get(ibrion) 326 | 327 | if self.user_input.incar.get("ICHARG", VASP_DEFAULTS_DICT["ICHARG"].value) >= 10: 328 | run_type = "nonscf" 329 | if self.user_input.incar.get("LCHIMAG", VASP_DEFAULTS_DICT["LCHIMAG"].value): 330 | run_type == "nmr" 331 | 332 | if run_type is None: 333 | raise ValidationError( 334 | "Could not determine a valid run type. We currently only validate " 335 | "Geometry optimizations (relaxations), single-points (statics), " 336 | "and non-self-consistent fixed charged density calculations. ", 337 | ) 338 | 339 | return run_type 340 | 341 | @cached_property 342 | def functional(self) -> str: 343 | """Determine the functional used in the calculation. 344 | 345 | Note that this is not a complete determination. 346 | Only the functionals used by MP are detected here. 347 | """ 348 | 349 | func = None 350 | func_from_potcar = None 351 | if self.user_input.potcar: 352 | func_from_potcar = {"pe": "pbe", "ca": "lda"}.get(self.user_input.potcar[0].lexch.lower()) 353 | elif self.vasprun and self.vasprun.potcar_symbols: 354 | pot_func = self.vasprun.potcar_symbols[0].split()[0].split("_")[-1] 355 | func_from_potcar = "pbe" if pot_func == "PBE" else "lda" 356 | 357 | if gga := self.user_input.incar.get("GGA"): 358 | if gga.lower() == "pe": 359 | func = "pbe" 360 | elif gga.lower() == "ps": 361 | func = "pbesol" 362 | else: 363 | func = gga.lower() 364 | 365 | if (metagga := self.user_input.incar.get("METAGGA")) and metagga.lower() != "none": 366 | if gga: 367 | raise ValidationError( 368 | "Both the GGA and METAGGA tags were set, which can lead to large errors. " 369 | "For context, see:\n" 370 | "https://github.com/materialsproject/atomate2/issues/453#issuecomment-1699605867" 371 | ) 372 | if metagga.lower() == "scan": 373 | func = "scan" 374 | elif metagga.lower().startswith("r2sca"): 375 | func = "r2scan" 376 | else: 377 | func = metagga.lower() 378 | 379 | if self.user_input.incar.get("LHFCALC", False): 380 | if (func == "pbe" or func_from_potcar == "pbe") and (self.user_input.incar.get("HFSCREEN", 0.0) > 0.0): 381 | func = "hse06" 382 | else: 383 | func = None 384 | 385 | func = func or func_from_potcar 386 | if func is None: 387 | raise ValidationError( 388 | "Currently, we only validate calculations using the following functionals:\n" 389 | "GGA : PBE, PBEsol\n" 390 | "meta-GGA : SCAN, r2SCAN\n" 391 | "Hybrids: HSE06" 392 | ) 393 | return func 394 | 395 | @property 396 | def bandgap(self) -> float | None: 397 | """Determine the bandgap from vasprun.xml.""" 398 | if self.vasprun: 399 | return self.vasprun.bandgap 400 | return None 401 | 402 | @cached_property 403 | def valid_input_set(self) -> VaspInputSafe: 404 | """ 405 | Determine the MP-compliant input set for a calculation. 406 | 407 | We need only determine a rough input set here. 408 | The precise details of the input set do not matter. 409 | """ 410 | 411 | incar_updates: dict[str, Any] = {} 412 | set_name: str | None = None 413 | if self.functional == "pbe": 414 | if self.run_type == "nonscf": 415 | set_name = "MPNonSCFSet" 416 | elif self.run_type == "nmr": 417 | set_name = "MPNMRSet" 418 | elif self.run_type == "md": 419 | set_name = None 420 | else: 421 | set_name = f"MP{self.run_type.capitalize()}Set" 422 | elif self.functional in ("pbesol", "scan", "r2scan", "hse06"): 423 | if self.functional == "pbesol": 424 | incar_updates["GGA"] = "PS" 425 | elif self.functional == "scan": 426 | incar_updates["METAGGA"] = "SCAN" 427 | elif self.functional == "hse06": 428 | incar_updates.update( 429 | LHFCALC=True, 430 | HFSCREEN=0.2, 431 | GGA="PE", 432 | ) 433 | set_name = f"MPScan{self.run_type.capitalize()}Set" 434 | 435 | if set_name is None: 436 | raise ValidationError( 437 | "Could not determine a valid input set from the specified " 438 | f"functional = {self.functional} and calculation type {self.run_type}." 439 | ) 440 | 441 | # Note that only the *previous* bandgap informs the k-point density 442 | vis = getattr(import_module("pymatgen.io.vasp.sets"), set_name)( 443 | structure=self.user_input.structure, 444 | bandgap=None, 445 | user_incar_settings=incar_updates, 446 | ) 447 | 448 | return VaspInputSafe.from_vasp_input_set(vis) 449 | 450 | 451 | class BaseValidator(BaseModel): 452 | """ 453 | Template for validation classes. 454 | 455 | This class will check any function with the name prefix `_check_`. 456 | `_check_*` functions should take VaspFiles, and two lists of strings 457 | (`reasons` and `warnings`) as args: 458 | 459 | def _check_example(self, vasp_files : VaspFiles, reasons : list[str], warnings : list[str]) -> None: 460 | if self.name == "whole mango": 461 | reasons.append("We only accept sliced or diced mango at this time.") 462 | elif self.name == "diced mango": 463 | warnings.append("We prefer sliced mango, but will accept diced mango.") 464 | """ 465 | 466 | name: str = Field("Base validator class", description="Name of the validator class.") 467 | vasp_defaults: dict[str, VaspParam] = Field(VASP_DEFAULTS_DICT, description="Default VASP settings.") 468 | fast: bool = Field(False, description="Whether to perform a quick check (True) or to perform all checks (False).") 469 | 470 | def auto_fail(self, vasp_files: VaspFiles, reasons: list[str], warnings: list[str]) -> bool: 471 | """Quick stop in case none of the checks can be performed.""" 472 | return False 473 | 474 | def check(self, vasp_files: VaspFiles, reasons: list[str], warnings: list[str]) -> None: 475 | """ 476 | Execute all methods on the class with a name prefix `_check_`. 477 | 478 | Parameters 479 | ----------- 480 | reasons : VaspFiles 481 | A set of required and optional VASP input and output objects. 482 | reasons : list of str 483 | A list of errors to update if a check fails. These are higher 484 | severity and would deprecate a calculation. 485 | warnings : list of str 486 | A list of warnings to update if a check fails. These are lower 487 | severity and would flag a calculation for possible review. 488 | """ 489 | 490 | if self.auto_fail(vasp_files, reasons, warnings): 491 | return 492 | 493 | checklist = {attr for attr in dir(self) if attr.startswith("_check_")} 494 | for attr in checklist: 495 | if self.fast and len(reasons) > 0: 496 | # fast check: stop checking whenever a single check fails 497 | break 498 | 499 | getattr(self, attr)(vasp_files, reasons, warnings) 500 | -------------------------------------------------------------------------------- /pymatgen/io/validation/compare_to_MP_ehull.py: -------------------------------------------------------------------------------- 1 | """Module for checking if a structure's energy is within a certain distance of the MPDB hull""" 2 | 3 | from mp_api.client import MPRester # type: ignore[import-untyped] 4 | from pymatgen.analysis.phase_diagram import PhaseDiagram 5 | from pymatgen.entries.mixing_scheme import MaterialsProjectDFTMixingScheme 6 | from pymatgen.entries.computed_entries import ComputedStructureEntry 7 | 8 | 9 | def compare_to_MP_ehull(mp_api_key=None, task_doc=None): 10 | """Method for comparing energy of a structure to the MP Database""" 11 | if mp_api_key is None: 12 | raise ValueError("Please input your mp API key") 13 | 14 | # get ComputedStructureEntry from taskdoc. 15 | # The `taskdoc.structure_entry()` does not work directly, as the `entry` field in the taskdoc is None 16 | entry = task_doc.get_entry(task_doc.calcs_reversed, task_id="-1") 17 | entry_dict = entry.as_dict() 18 | entry_dict["structure"] = task_doc.output.structure 19 | cur_structure_entry = ComputedStructureEntry.from_dict(entry_dict) 20 | elements = task_doc.output.structure.composition.to_reduced_dict.keys() 21 | 22 | with MPRester(mp_api_key) as mpr: 23 | # Obtain GGA, GGA+U, and r2SCAN ComputedStructureEntry objects 24 | entries = mpr.get_entries_in_chemsys( 25 | elements=elements, 26 | compatible_only=True, 27 | additional_criteria={ 28 | "thermo_types": ["GGA_GGA+U", "R2SCAN"], 29 | "is_stable": True, 30 | }, 31 | ) 32 | 33 | entries.append(cur_structure_entry) 34 | 35 | # Apply corrections locally with the mixing scheme 36 | scheme = MaterialsProjectDFTMixingScheme() 37 | corrected_entries = scheme.process_entries(entries) 38 | 39 | # Construct phase diagram 40 | pd = PhaseDiagram(corrected_entries) 41 | cur_corrected_structure_entry = [entry for entry in corrected_entries if entry.entry_id == "-1"][0] 42 | e_above_hull = pd.get_e_above_hull(cur_corrected_structure_entry) 43 | 44 | return e_above_hull 45 | -------------------------------------------------------------------------------- /pymatgen/io/validation/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/pymatgen/io/validation/py.typed -------------------------------------------------------------------------------- /pymatgen/io/validation/settings.py: -------------------------------------------------------------------------------- 1 | # mypy: ignore-errors 2 | 3 | """ 4 | Settings for pymatgen-io-validation. Used to be part of EmmetSettings. 5 | """ 6 | 7 | import json 8 | from pathlib import Path 9 | from typing import Dict, Type, TypeVar, Union 10 | 11 | import requests 12 | from monty.json import MontyDecoder 13 | from pydantic import field_validator, model_validator, Field, ImportString 14 | from pydantic_settings import BaseSettings, SettingsConfigDict 15 | 16 | DEFAULT_CONFIG_FILE_PATH = str(Path.home().joinpath(".emmet.json")) 17 | 18 | 19 | S = TypeVar("S", bound="IOValidationSettings") 20 | 21 | 22 | class IOValidationSettings(BaseSettings): 23 | """ 24 | Settings for pymatgen-io-validation 25 | """ 26 | 27 | config_file: str = Field(DEFAULT_CONFIG_FILE_PATH, description="File to load alternative defaults from") 28 | 29 | CHECK_PYPI_AT_LOAD: bool = Field( 30 | False, 31 | description=( 32 | "Whether to do a version check when this module is loaded. " 33 | "Helps user ensure most recent parameter checks are used." 34 | ), 35 | ) 36 | 37 | VASP_KPTS_TOLERANCE: float = Field( 38 | 0.9, 39 | description="Relative tolerance for kpt density to still be a valid task document", 40 | ) 41 | 42 | VASP_ALLOW_KPT_SHIFT: bool = Field( 43 | False, 44 | description="Whether to consider a task valid if kpoints are shifted by the user", 45 | ) 46 | 47 | VASP_ALLOW_EXPLICIT_KPT_MESH: Union[str, bool] = Field( 48 | "auto", 49 | description="Whether to consider a task valid if the user defines an explicit kpoint mesh", 50 | ) 51 | 52 | VASP_FFT_GRID_TOLERANCE: float = Field( 53 | 0.9, 54 | description="Relative tolerance for FFT grid parameters to still be a valid", 55 | ) 56 | 57 | VASP_DEFAULT_INPUT_SETS: Dict[str, ImportString] = Field( 58 | { 59 | "GGA Structure Optimization": "pymatgen.io.vasp.sets.MPRelaxSet", 60 | "GGA+U Structure Optimization": "pymatgen.io.vasp.sets.MPRelaxSet", 61 | "r2SCAN Structure Optimization": "pymatgen.io.vasp.sets.MPScanRelaxSet", 62 | "SCAN Structure Optimization": "pymatgen.io.vasp.sets.MPScanRelaxSet", 63 | "PBESol Structure Optimization": "pymatgen.io.vasp.sets.MPScanRelaxSet", 64 | "GGA Static": "pymatgen.io.vasp.sets.MPStaticSet", 65 | "GGA+U Static": "pymatgen.io.vasp.sets.MPStaticSet", 66 | "PBE Static": "pymatgen.io.vasp.sets.MPStaticSet", 67 | "PBE+U Static": "pymatgen.io.vasp.sets.MPStaticSet", 68 | "r2SCAN Static": "pymatgen.io.vasp.sets.MPScanStaticSet", 69 | "SCAN Static": "pymatgen.io.vasp.sets.MPScanStaticSet", 70 | "PBESol Static": "pymatgen.io.vasp.sets.MPScanStaticSet", 71 | "HSE06 Static": "pymatgen.io.vasp.sets.MPScanStaticSet", 72 | "GGA NSCF Uniform": "pymatgen.io.vasp.sets.MPNonSCFSet", 73 | "GGA+U NSCF Uniform": "pymatgen.io.vasp.sets.MPNonSCFSet", 74 | "GGA NSCF Line": "pymatgen.io.vasp.sets.MPNonSCFSet", 75 | "GGA+U NSCF Line": "pymatgen.io.vasp.sets.MPNonSCFSet", 76 | "GGA NMR Electric Field Gradient": "pymatgen.io.vasp.sets.MPNMRSet", 77 | "GGA NMR Nuclear Shielding": "pymatgen.io.vasp.sets.MPNMRSet", 78 | "GGA+U NMR Electric Field Gradient": "pymatgen.io.vasp.sets.MPNMRSet", 79 | "GGA+U NMR Nuclear Shielding": "pymatgen.io.vasp.sets.MPNMRSet", 80 | "GGA Deformation": "pymatgen.io.vasp.sets.MPStaticSet", 81 | "GGA+U Deformation": "pymatgen.io.vasp.sets.MPStaticSet", 82 | "GGA DFPT Dielectric": "pymatgen.io.vasp.sets.MPStaticSet", 83 | "GGA+U DFPT Dielectric": "pymatgen.io.vasp.sets.MPStaticSet", 84 | }, 85 | description="Default input sets for task validation", 86 | ) 87 | 88 | VASP_MAX_SCF_GRADIENT: float = Field( 89 | 1000, 90 | description="Maximum upward gradient in the last SCF for any VASP calculation", 91 | ) 92 | 93 | VASP_NUM_IONIC_STEPS_FOR_DRIFT: int = Field( 94 | 3, 95 | description="Number of ionic steps to average over when validating drift forces", 96 | ) 97 | 98 | VASP_MAX_POSITIVE_ENERGY: float = Field( 99 | 50.0, description="Maximum allowable positive energy at the end of a calculation." 100 | ) 101 | 102 | model_config = SettingsConfigDict(env_prefix="pymatgen_io_validation_", extra="ignore") 103 | 104 | FAST_VALIDATION: bool = Field( 105 | default=False, 106 | description=( 107 | "Whether to attempt to find all reasons a calculation fails (False), " 108 | "or stop validation if any single check fails." 109 | ), 110 | ) 111 | 112 | @model_validator(mode="before") 113 | @classmethod 114 | def load_default_settings(cls, values): 115 | """ 116 | Loads settings from a root file if available and uses that as defaults in 117 | place of built in defaults 118 | """ 119 | config_file_path: str = values.get("config_file", DEFAULT_CONFIG_FILE_PATH) 120 | 121 | new_values = {} 122 | 123 | if config_file_path.startswith("http"): 124 | new_values = requests.get(config_file_path).json() 125 | elif Path(config_file_path).exists(): 126 | with open(config_file_path, encoding="utf8") as f: 127 | new_values = json.load(f) 128 | 129 | new_values.update(values) 130 | 131 | return new_values 132 | 133 | @classmethod 134 | def autoload(cls: Type[S], settings: Union[None, dict, S]) -> S: # noqa 135 | if settings is None: 136 | return cls() 137 | elif isinstance(settings, dict): 138 | return cls(**settings) 139 | return settings 140 | 141 | @field_validator("VASP_DEFAULT_INPUT_SETS", mode="before") 142 | @classmethod 143 | def convert_input_sets(cls, value): # noqa 144 | if isinstance(value, dict): 145 | return {k: MontyDecoder().process_decoded(v) for k, v in value.items()} 146 | return value 147 | 148 | def as_dict(self): 149 | """ 150 | HotPatch to enable serializing IOValidationSettings via Monty 151 | """ 152 | return self.model_dump(exclude_unset=True, exclude_defaults=True) 153 | -------------------------------------------------------------------------------- /pymatgen/io/validation/validation.py: -------------------------------------------------------------------------------- 1 | """Define core validation schema.""" 2 | 3 | from __future__ import annotations 4 | from pathlib import Path 5 | from pydantic import BaseModel, Field, PrivateAttr 6 | from typing import TYPE_CHECKING 7 | 8 | from monty.os.path import zpath 9 | 10 | from pymatgen.io.validation.common import VaspFiles 11 | from pymatgen.io.validation.check_common_errors import CheckStructureProperties, CheckCommonErrors 12 | from pymatgen.io.validation.check_kpoints_kspacing import CheckKpointsKspacing 13 | from pymatgen.io.validation.check_potcar import CheckPotcar 14 | from pymatgen.io.validation.check_incar import CheckIncar 15 | 16 | if TYPE_CHECKING: 17 | from collections.abc import Mapping 18 | import os 19 | from typing_extensions import Self 20 | 21 | 22 | DEFAULT_CHECKS = [CheckStructureProperties, CheckPotcar, CheckCommonErrors, CheckKpointsKspacing, CheckIncar] 23 | 24 | # TODO: check for surface/slab calculations. Especially necessary for external calcs. 25 | # TODO: implement check to make sure calcs are within some amount (e.g. 250 meV) of the convex hull in the MPDB 26 | 27 | 28 | class VaspValidator(BaseModel): 29 | """Validate a VASP calculation.""" 30 | 31 | vasp_files: VaspFiles = Field(description="The VASP I/O.") 32 | reasons: list[str] = Field([], description="List of deprecation tags detailing why this task isn't valid") 33 | warnings: list[str] = Field([], description="List of warnings about this calculation") 34 | 35 | _validated_md5: str | None = PrivateAttr(None) 36 | 37 | @property 38 | def valid(self) -> bool: 39 | """Determine if the calculation is valid after ensuring inputs have not changed.""" 40 | self.recheck() 41 | return len(self.reasons) == 0 42 | 43 | @property 44 | def has_warnings(self) -> bool: 45 | """Determine if any warnings were incurred.""" 46 | return len(self.warnings) > 0 47 | 48 | def recheck(self) -> None: 49 | """Rerun validation, prioritizing speed.""" 50 | new_md5 = None 51 | if self._validated_md5 is None or (new_md5 := self.vasp_files.md5) != self._validated_md5: 52 | 53 | if self.vasp_files.user_input.potcar: 54 | check_list = DEFAULT_CHECKS 55 | else: 56 | check_list = [c for c in DEFAULT_CHECKS if c.__name__ != "CheckPotcar"] 57 | self.reasons, self.warnings = self.run_checks(self.vasp_files, check_list=check_list, fast=True) 58 | self._validated_md5 = new_md5 or self.vasp_files.md5 59 | 60 | @staticmethod 61 | def run_checks( 62 | vasp_files: VaspFiles, 63 | check_list: list | tuple = DEFAULT_CHECKS, 64 | fast: bool = False, 65 | ) -> tuple[list[str], list[str]]: 66 | """Perform validation. 67 | 68 | Parameters 69 | ----------- 70 | vasp_files : VaspFiles 71 | The VASP I/O to validate. 72 | check_list : list or tuple of BaseValidator. 73 | The list of checks to perform. Defaults to `DEFAULT_CHECKS`. 74 | fast : bool (default = False) 75 | Whether to stop validation at the first validation failure (True) 76 | or compile a list of all failure reasons. 77 | 78 | Returns 79 | ----------- 80 | tuple of list of str 81 | The first list are all reasons for validation failure, 82 | the second list contains all warnings. 83 | """ 84 | reasons: list[str] = [] 85 | warnings: list[str] = [] 86 | for check in check_list: 87 | check(fast=fast).check(vasp_files, reasons, warnings) # type: ignore[arg-type] 88 | if fast and len(reasons) > 0: 89 | break 90 | return reasons, warnings 91 | 92 | @classmethod 93 | def from_vasp_input( 94 | cls, 95 | vasp_file_paths: Mapping[str, str | Path | os.PathLike[str]] | None = None, 96 | vasp_files: VaspFiles | None = None, 97 | fast: bool = False, 98 | check_potcar: bool = True, 99 | **kwargs, 100 | ) -> Self: 101 | """ 102 | Validate a VASP calculation from VASP files or their object representation. 103 | 104 | Parameters 105 | ----------- 106 | vasp_file_paths : dict of str to os.PathLike, optional 107 | If specified, a dict of the form: 108 | { 109 | "incar": < path to INCAR>, 110 | "poscar": < path to POSCAR>, 111 | ... 112 | } 113 | where keys are taken by `VaspFiles.from_paths`. 114 | vasp_files : VaspFiles, optional 115 | This takes higher precendence than `vasp_file_paths`, and 116 | allows the user to specify VASP input/output from a VaspFiles 117 | object. 118 | fast : bool (default = False) 119 | Whether to stop validation at the first failure (True) 120 | or to list all reasons why a calculation failed (False) 121 | check_potcar : bool (default = True) 122 | Whether to check the POTCAR for validity. 123 | **kwargs 124 | kwargs to pass to `VaspValidator` 125 | """ 126 | 127 | if vasp_files: 128 | vf: VaspFiles = vasp_files 129 | elif vasp_file_paths: 130 | vf = VaspFiles.from_paths(**vasp_file_paths) 131 | 132 | config: dict[str, list[str]] = { 133 | "reasons": [], 134 | "warnings": [], 135 | } 136 | 137 | if check_potcar: 138 | check_list = DEFAULT_CHECKS 139 | else: 140 | check_list = [c for c in DEFAULT_CHECKS if c.__name__ != "CheckPotcar"] 141 | 142 | config["reasons"], config["warnings"] = cls.run_checks(vf, check_list=check_list, fast=fast) 143 | validated = cls(**config, vasp_files=vf, **kwargs) 144 | validated._validated_md5 = vf.md5 145 | return validated 146 | 147 | @classmethod 148 | def from_directory(cls, dir_name: str | Path, **kwargs) -> Self: 149 | """Convenience method to validate a calculation from a directory. 150 | 151 | This method is intended solely for use cases where VASP input/output 152 | files are not renamed, beyond the compression methods supported by 153 | monty.os.zpath. 154 | 155 | Thus, INCAR, INCAR.gz, INCAR.bz2, INCAR.lzma are all acceptable, but 156 | INCAR.relax1.gz is not. 157 | 158 | For finer-grained control of which files are validated, explicitly 159 | pass file names to `VaspValidator.from_vasp_input`. 160 | 161 | Parameters 162 | ----------- 163 | dir_name : str or Path 164 | The path to the calculation directory. 165 | **kwargs 166 | kwargs to pass to `VaspValidator` 167 | """ 168 | dir_name = Path(dir_name) 169 | vasp_file_paths = {} 170 | for file_name in ("INCAR", "KPOINTS", "POSCAR", "POTCAR", "OUTCAR", "vasprun.xml"): 171 | if (file_path := Path(zpath(str(dir_name / file_name)))).exists(): 172 | vasp_file_paths[file_name.lower().split(".")[0]] = file_path 173 | return cls.from_vasp_input(vasp_file_paths=vasp_file_paths, **kwargs) 174 | -------------------------------------------------------------------------------- /pymatgen/io/validation/vasp_defaults.yaml: -------------------------------------------------------------------------------- 1 | - name: ADDGRID 2 | value: false 3 | operation: == 4 | alias: ADDGRID 5 | tag: fft 6 | tolerance: 0.0001 7 | comment: 8 | warning: 9 | severity: reason 10 | - name: AEXX 11 | value: 0.0 12 | operation: 13 | alias: AEXX 14 | tag: hybrid 15 | tolerance: 0.0001 16 | comment: 17 | warning: 18 | severity: reason 19 | - name: AGGAC 20 | value: 1.0 21 | operation: 22 | alias: AGGAC 23 | tag: hybrid 24 | tolerance: 0.0001 25 | comment: 26 | warning: 27 | severity: reason 28 | - name: AGGAX 29 | value: 1.0 30 | operation: 31 | alias: AGGAX 32 | tag: hybrid 33 | tolerance: 0.0001 34 | comment: 35 | warning: 36 | severity: reason 37 | - name: ALDAC 38 | value: 1.0 39 | operation: 40 | alias: ALDAC 41 | tag: hybrid 42 | tolerance: 0.0001 43 | comment: 44 | warning: 45 | severity: reason 46 | - name: ALDAX 47 | value: 1.0 48 | operation: 49 | alias: ALDAX 50 | tag: hybrid 51 | tolerance: 0.0001 52 | comment: 53 | warning: 54 | severity: reason 55 | - name: ALGO 56 | value: normal 57 | operation: 58 | alias: ALGO 59 | tag: electronic_self_consistency 60 | tolerance: 0.0001 61 | comment: 62 | warning: 63 | severity: reason 64 | - name: AMGGAC 65 | value: 1.0 66 | operation: 67 | alias: AMGGAC 68 | tag: hybrid 69 | tolerance: 0.0001 70 | comment: 71 | warning: 72 | severity: reason 73 | - name: AMGGAX 74 | value: 1.0 75 | operation: 76 | alias: AMGGAX 77 | tag: hybrid 78 | tolerance: 0.0001 79 | comment: 80 | warning: 81 | severity: reason 82 | - name: DEPER 83 | value: 0.3 84 | operation: == 85 | alias: DEPER 86 | tag: misc 87 | tolerance: 0.0001 88 | comment: According to the VASP manual, DEPER should not be set by the user. 89 | warning: 90 | severity: reason 91 | - name: EBREAK 92 | value: 93 | operation: 94 | alias: EBREAK 95 | tag: post_init 96 | tolerance: 0.0001 97 | comment: According to the VASP manual, EBREAK should not be set by the user. 98 | warning: 99 | severity: reason 100 | - name: EDIFF 101 | value: 0.0001 102 | operation: <= 103 | alias: EDIFF 104 | tag: electronic 105 | tolerance: 0.0001 106 | comment: 107 | warning: 108 | severity: reason 109 | - name: EFERMI 110 | value: LEGACY 111 | operation: 112 | alias: EFERMI 113 | tag: misc_special 114 | tolerance: 0.0001 115 | comment: 116 | warning: 117 | severity: reason 118 | - name: EFIELD 119 | value: 0.0 120 | operation: == 121 | alias: EFIELD 122 | tag: dipole 123 | tolerance: 0.0001 124 | comment: 125 | warning: 126 | severity: reason 127 | - name: ENAUG 128 | value: .inf 129 | operation: 130 | alias: ENAUG 131 | tag: electronic 132 | tolerance: 0.0001 133 | comment: 134 | warning: 135 | severity: reason 136 | - name: ENINI 137 | value: 0 138 | operation: 139 | alias: ENINI 140 | tag: electronic 141 | tolerance: 0.0001 142 | comment: 143 | warning: 144 | severity: reason 145 | - name: ENMAX 146 | value: .inf 147 | operation: '>=' 148 | alias: ENCUT 149 | tag: fft 150 | tolerance: 0.0001 151 | comment: 152 | warning: 153 | severity: reason 154 | - name: EPSILON 155 | value: 1.0 156 | operation: == 157 | alias: EPSILON 158 | tag: dipole 159 | tolerance: 0.0001 160 | comment: 161 | warning: 162 | severity: reason 163 | - name: GGA_COMPAT 164 | value: true 165 | operation: == 166 | alias: GGA_COMPAT 167 | tag: misc 168 | tolerance: 0.0001 169 | comment: 170 | warning: 171 | severity: reason 172 | - name: IALGO 173 | value: 38 174 | operation: in 175 | alias: IALGO 176 | tag: misc_special 177 | tolerance: 0.0001 178 | comment: 179 | warning: 180 | severity: reason 181 | - name: IBRION 182 | value: 0 183 | operation: in 184 | alias: IBRION 185 | tag: ionic 186 | tolerance: 0.0001 187 | comment: 188 | warning: 189 | severity: reason 190 | - name: ICHARG 191 | value: 2 192 | operation: 193 | alias: ICHARG 194 | tag: startup 195 | tolerance: 0.0001 196 | comment: 197 | warning: 198 | severity: reason 199 | - name: ICORELEVEL 200 | value: 0 201 | operation: == 202 | alias: ICORELEVEL 203 | tag: misc 204 | tolerance: 0.0001 205 | comment: 206 | warning: 207 | severity: reason 208 | - name: IDIPOL 209 | value: 0 210 | operation: == 211 | alias: IDIPOL 212 | tag: dipole 213 | tolerance: 0.0001 214 | comment: 215 | warning: 216 | severity: reason 217 | - name: IMAGES 218 | value: 0 219 | operation: == 220 | alias: IMAGES 221 | tag: misc 222 | tolerance: 0.0001 223 | comment: 224 | warning: 225 | severity: reason 226 | - name: INIWAV 227 | value: 1 228 | operation: == 229 | alias: INIWAV 230 | tag: startup 231 | tolerance: 0.0001 232 | comment: 233 | warning: 234 | severity: reason 235 | - name: ISIF 236 | value: 2 237 | operation: '>=' 238 | alias: ISIF 239 | tag: ionic 240 | tolerance: 0.0001 241 | comment: ISIF values < 2 do not output the complete stress tensor. 242 | warning: 243 | severity: reason 244 | - name: ISMEAR 245 | value: 1 246 | operation: in 247 | alias: ISMEAR 248 | tag: smearing 249 | tolerance: 0.0001 250 | comment: 251 | warning: 252 | severity: reason 253 | - name: ISPIN 254 | value: 1 255 | operation: 256 | alias: ISPIN 257 | tag: misc_special 258 | tolerance: 0.0001 259 | comment: 260 | warning: 261 | severity: reason 262 | - name: ISTART 263 | value: 0 264 | operation: in 265 | alias: ISTART 266 | tag: startup 267 | tolerance: 0.0001 268 | comment: 269 | warning: 270 | severity: reason 271 | - name: ISYM 272 | value: 2 273 | operation: in 274 | alias: ISYM 275 | tag: symmetry 276 | tolerance: 0.0001 277 | comment: 278 | warning: 279 | severity: reason 280 | - name: IVDW 281 | value: 0 282 | operation: == 283 | alias: IVDW 284 | tag: misc 285 | tolerance: 0.0001 286 | comment: 287 | warning: 288 | severity: reason 289 | - name: IWAVPR 290 | value: 291 | operation: 292 | alias: IWAVPR 293 | tag: misc_special 294 | tolerance: 0.0001 295 | comment: 296 | warning: 297 | severity: reason 298 | - name: KGAMMA 299 | value: true 300 | operation: 301 | alias: KGAMMA 302 | tag: k_mesh 303 | tolerance: 0.0001 304 | comment: 305 | warning: 306 | severity: reason 307 | - name: KSPACING 308 | value: 0.5 309 | operation: 310 | alias: KSPACING 311 | tag: k_mesh 312 | tolerance: 0.0001 313 | comment: 314 | warning: 315 | severity: reason 316 | - name: LASPH 317 | value: true 318 | operation: == 319 | alias: LASPH 320 | tag: misc 321 | tolerance: 0.0001 322 | comment: 323 | warning: 324 | severity: reason 325 | - name: LBERRY 326 | value: false 327 | operation: == 328 | alias: LBERRY 329 | tag: misc 330 | tolerance: 0.0001 331 | comment: 332 | warning: 333 | severity: reason 334 | - name: LCALCEPS 335 | value: false 336 | operation: == 337 | alias: LCALCEPS 338 | tag: misc 339 | tolerance: 0.0001 340 | comment: 341 | warning: 342 | severity: reason 343 | - name: LCALCPOL 344 | value: false 345 | operation: == 346 | alias: LCALCPOL 347 | tag: misc 348 | tolerance: 0.0001 349 | comment: 350 | warning: 351 | severity: reason 352 | - name: LCHIMAG 353 | value: false 354 | operation: == 355 | alias: LCHIMAG 356 | tag: chemical_shift 357 | tolerance: 0.0001 358 | comment: 359 | warning: 360 | severity: reason 361 | - name: LCORR 362 | value: true 363 | operation: 364 | alias: LCORR 365 | tag: misc_special 366 | tolerance: 0.0001 367 | comment: 368 | warning: 369 | severity: reason 370 | - name: LDAU 371 | value: false 372 | operation: 373 | alias: LDAU 374 | tag: dft_plus_u 375 | tolerance: 0.0001 376 | comment: 377 | warning: 378 | severity: reason 379 | - name: LDAUJ 380 | value: [] 381 | operation: 382 | alias: LDAUJ 383 | tag: dft_plus_u 384 | tolerance: 0.0001 385 | comment: 386 | warning: 387 | severity: reason 388 | - name: LDAUL 389 | value: [] 390 | operation: 391 | alias: LDAUL 392 | tag: dft_plus_u 393 | tolerance: 0.0001 394 | comment: 395 | warning: 396 | severity: reason 397 | - name: LDAUTYPE 398 | value: 2 399 | operation: 400 | alias: LDAUTYPE 401 | tag: dft_plus_u 402 | tolerance: 0.0001 403 | comment: 404 | warning: 405 | severity: reason 406 | - name: LDAUU 407 | value: [] 408 | operation: 409 | alias: LDAUU 410 | tag: dft_plus_u 411 | tolerance: 0.0001 412 | comment: 413 | warning: 414 | severity: reason 415 | - name: LDIPOL 416 | value: false 417 | operation: == 418 | alias: LDIPOL 419 | tag: dipole 420 | tolerance: 0.0001 421 | comment: 422 | warning: 423 | severity: reason 424 | - name: LEFG 425 | value: false 426 | operation: == 427 | alias: LEFG 428 | tag: write 429 | tolerance: 0.0001 430 | comment: 431 | warning: 432 | severity: reason 433 | - name: LEPSILON 434 | value: false 435 | operation: == 436 | alias: LEPSILON 437 | tag: misc 438 | tolerance: 0.0001 439 | comment: 440 | warning: 441 | severity: reason 442 | - name: LHFCALC 443 | value: false 444 | operation: 445 | alias: LHFCALC 446 | tag: hybrid 447 | tolerance: 0.0001 448 | comment: 449 | warning: 450 | severity: reason 451 | - name: LHYPERFINE 452 | value: false 453 | operation: == 454 | alias: LHYPERFINE 455 | tag: misc 456 | tolerance: 0.0001 457 | comment: 458 | warning: 459 | severity: reason 460 | - name: LKPOINTS_OPT 461 | value: false 462 | operation: == 463 | alias: LKPOINTS_OPT 464 | tag: misc 465 | tolerance: 0.0001 466 | comment: 467 | warning: 468 | severity: reason 469 | - name: LKPROJ 470 | value: false 471 | operation: == 472 | alias: LKPROJ 473 | tag: misc 474 | tolerance: 0.0001 475 | comment: 476 | warning: 477 | severity: reason 478 | - name: LMAXMIX 479 | value: 2 480 | operation: 481 | alias: LMAXMIX 482 | tag: density_mixing 483 | tolerance: 0.0001 484 | comment: 485 | warning: 486 | severity: reason 487 | - name: LMAXPAW 488 | value: -100 489 | operation: == 490 | alias: LMAXPAW 491 | tag: electronic_projector 492 | tolerance: 0.0001 493 | comment: 494 | warning: 495 | severity: reason 496 | - name: LMAXTAU 497 | value: 6 498 | operation: 499 | alias: LMAXTAU 500 | tag: density_mixing 501 | tolerance: 0.0001 502 | comment: 503 | warning: 504 | severity: reason 505 | - name: LMONO 506 | value: false 507 | operation: == 508 | alias: LMONO 509 | tag: dipole 510 | tolerance: 0.0001 511 | comment: 512 | warning: 513 | severity: reason 514 | - name: LMP2LT 515 | value: false 516 | operation: == 517 | alias: LMP2LT 518 | tag: misc 519 | tolerance: 0.0001 520 | comment: 521 | warning: 522 | severity: reason 523 | - name: LNMR_SYM_RED 524 | value: false 525 | operation: == 526 | alias: LNMR_SYM_RED 527 | tag: chemical_shift 528 | tolerance: 0.0001 529 | comment: 530 | warning: 531 | severity: reason 532 | - name: LNONCOLLINEAR 533 | value: false 534 | operation: == 535 | alias: LNONCOLLINEAR 536 | tag: ncl 537 | tolerance: 0.0001 538 | comment: 539 | warning: 540 | severity: reason 541 | - name: LOCPROJ 542 | value: NONE 543 | operation: == 544 | alias: LOCPROJ 545 | tag: misc 546 | tolerance: 0.0001 547 | comment: 548 | warning: 549 | severity: reason 550 | - name: LOPTICS 551 | value: false 552 | operation: == 553 | alias: LOPTICS 554 | tag: tddft 555 | tolerance: 0.0001 556 | comment: 557 | warning: 558 | severity: reason 559 | - name: LORBIT 560 | value: 561 | operation: 562 | alias: LORBIT 563 | tag: misc_special 564 | tolerance: 0.0001 565 | comment: 566 | warning: 567 | severity: reason 568 | - name: LREAL 569 | value: 'false' 570 | operation: in 571 | alias: LREAL 572 | tag: precision 573 | tolerance: 0.0001 574 | comment: 575 | warning: 576 | severity: reason 577 | - name: LRPA 578 | value: false 579 | operation: == 580 | alias: LRPA 581 | tag: misc 582 | tolerance: 0.0001 583 | comment: 584 | warning: 585 | severity: reason 586 | - name: LSMP2LT 587 | value: false 588 | operation: == 589 | alias: LSMP2LT 590 | tag: misc 591 | tolerance: 0.0001 592 | comment: 593 | warning: 594 | severity: reason 595 | - name: LSORBIT 596 | value: false 597 | operation: == 598 | alias: LSORBIT 599 | tag: ncl 600 | tolerance: 0.0001 601 | comment: 602 | warning: 603 | severity: reason 604 | - name: LSPECTRAL 605 | value: false 606 | operation: == 607 | alias: LSPECTRAL 608 | tag: misc 609 | tolerance: 0.0001 610 | comment: 611 | warning: 612 | severity: reason 613 | - name: LSUBROT 614 | value: false 615 | operation: == 616 | alias: LSUBROT 617 | tag: misc 618 | tolerance: 0.0001 619 | comment: 620 | warning: 621 | severity: reason 622 | - name: METAGGA 623 | value: 624 | operation: 625 | alias: METAGGA 626 | tag: dft 627 | tolerance: 0.0001 628 | comment: 629 | warning: 630 | severity: reason 631 | - name: ML_LMLFF 632 | value: false 633 | operation: == 634 | alias: ML_LMLFF 635 | tag: misc 636 | tolerance: 0.0001 637 | comment: 638 | warning: 639 | severity: reason 640 | - name: NELECT 641 | value: 642 | operation: 643 | alias: NELECT 644 | tag: electronic 645 | tolerance: 0.0001 646 | comment: 647 | warning: 648 | severity: reason 649 | - name: NELM 650 | value: 60 651 | operation: 652 | alias: NELM 653 | tag: electronic_self_consistency 654 | tolerance: 0.0001 655 | comment: 656 | warning: 657 | severity: reason 658 | - name: NELMDL 659 | value: -5 660 | operation: 661 | alias: NELMDL 662 | tag: electronic_self_consistency 663 | tolerance: 0.0001 664 | comment: 665 | warning: 666 | severity: reason 667 | - name: NLSPLINE 668 | value: false 669 | operation: == 670 | alias: NLSPLINE 671 | tag: electronic_projector 672 | tolerance: 0.0001 673 | comment: 674 | warning: 675 | severity: reason 676 | - name: NSW 677 | value: 0 678 | operation: 679 | alias: NSW 680 | tag: startup 681 | tolerance: 0.0001 682 | comment: 683 | warning: 684 | severity: reason 685 | - name: NWRITE 686 | value: 2 687 | operation: '>=' 688 | alias: NWRITE 689 | tag: write 690 | tolerance: 0.0001 691 | comment: The specified value of NWRITE does not output all needed information. 692 | warning: 693 | severity: reason 694 | - name: POTIM 695 | value: 0.5 696 | operation: 697 | alias: POTIM 698 | tag: ionic 699 | tolerance: 0.0001 700 | comment: 701 | warning: 702 | severity: reason 703 | - name: PREC 704 | value: NORMAL 705 | operation: 706 | alias: PREC 707 | tag: precision 708 | tolerance: 0.0001 709 | comment: 710 | warning: 711 | severity: reason 712 | - name: PSTRESS 713 | value: 0.0 714 | operation: approx 715 | alias: PSTRESS 716 | tag: ionic 717 | tolerance: 0.0001 718 | comment: 719 | warning: 720 | severity: reason 721 | - name: RWIGS 722 | value: 723 | - -1.0 724 | operation: 725 | alias: RWIGS 726 | tag: misc_special 727 | tolerance: 0.0001 728 | comment: 729 | warning: 730 | severity: reason 731 | - name: SCALEE 732 | value: 1.0 733 | operation: approx 734 | alias: SCALEE 735 | tag: ionic 736 | tolerance: 0.0001 737 | comment: 738 | warning: 739 | severity: reason 740 | - name: SIGMA 741 | value: 0.2 742 | operation: 743 | alias: SIGMA 744 | tag: smearing 745 | tolerance: 0.0001 746 | comment: 747 | warning: 748 | severity: reason 749 | - name: SYMPREC 750 | value: 1e-05 751 | operation: 752 | alias: SYMPREC 753 | tag: symmetry 754 | tolerance: 0.0001 755 | comment: 756 | warning: 757 | severity: reason 758 | - name: VCA 759 | value: 760 | - 1.0 761 | operation: 762 | alias: VCA 763 | tag: misc_special 764 | tolerance: 0.0001 765 | comment: 766 | warning: 767 | severity: reason 768 | - name: WEIMIN 769 | value: 0.001 770 | operation: <= 771 | alias: WEIMIN 772 | tag: misc 773 | tolerance: 0.0001 774 | comment: 775 | warning: 776 | severity: reason 777 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "setuptools.build_meta" 3 | requires = ["setuptools >= 65.0.0", "versioningit >= 1,< 4", "wheel"] 4 | 5 | [project] 6 | authors = [ 7 | { name = "Matthew Kuner", email = "matthewkuner@gmail.com" }, 8 | { name = "Janosh Riebesell"}, 9 | { name = "Jason Munro"}, 10 | { name = "Aaron Kaplan"}, 11 | ] 12 | classifiers = [ 13 | "Programming Language :: Python :: 3", 14 | "Programming Language :: Python :: 3.8", 15 | "Programming Language :: Python :: 3.9", 16 | "Programming Language :: Python :: 3.10", 17 | "Programming Language :: Python :: 3.11", 18 | "Development Status :: 4 - Beta", 19 | "Intended Audience :: Science/Research", 20 | "Operating System :: OS Independent", 21 | "Topic :: Scientific/Engineering", 22 | "Topic :: Scientific/Engineering :: Information Analysis", 23 | "Topic :: Scientific/Engineering :: Physics", 24 | "Topic :: Scientific/Engineering :: Chemistry", 25 | "Topic :: Software Development :: Libraries :: Python Modules", 26 | ] 27 | dependencies = [ 28 | "pymatgen", 29 | "numpy", 30 | "requests", 31 | "pydantic>=2.0.1", 32 | "pydantic-settings>=2.0.0", 33 | ] 34 | description = "A comprehensive I/O validator for electronic structure calculations" 35 | dynamic = ["version"] 36 | keywords = ["io", "validation", "dft", "vasp"] 37 | license = { text = "modified BSD" } 38 | name = "pymatgen-io-validation" 39 | readme = "README.md" 40 | requires-python = '>=3.8' 41 | 42 | [project.optional-dependencies] 43 | dev = ["pre-commit>=2.12.1"] 44 | #docs = ["jupyter-book>=0.13.1",] 45 | 46 | tests = ["pytest==8.3.5", "pytest-cov==6.1.1", "types-requests"] 47 | 48 | 49 | [tool.setuptools.dynamic] 50 | readme = { file = ["README.md"] } 51 | 52 | #[project.urls] 53 | #repository = "https://github.com/materialsproject/pymatgen-io-validation" 54 | 55 | [tool.setuptools.packages.find] 56 | exclude = ["tests"] 57 | where = ["./"] 58 | 59 | [tool.versioningit.vcs] 60 | default-tag = "0.0.1" 61 | method = "git" 62 | 63 | [tool.isort] 64 | profile = "black" 65 | 66 | [tool.black] 67 | line-length = 120 68 | 69 | [tool.blacken-docs] 70 | line-length = 120 71 | 72 | [tool.flake8] 73 | extend-ignore = "E203, Wv503, E501, F401, RST21" 74 | max-line-length = 120 75 | max-doc-length = 120 76 | min-python-version = "3.8.0" 77 | rst-roles = "class, func, ref, obj" 78 | select = "C, E, F, W, B, B950" 79 | 80 | [tool.mypy] 81 | explicit_package_bases = true 82 | namespace_packages = true 83 | ignore_missing_imports = true 84 | no_strict_optional = true 85 | plugins = ["pydantic.mypy"] 86 | 87 | [tool.coverage.run] 88 | branch = true 89 | include = ["pymatgen/*"] 90 | parallel = true 91 | 92 | [tool.coverage.paths] 93 | source = ["pymatgen/"] 94 | 95 | [tool.coverage.report] 96 | show_missing = true 97 | skip_covered = true 98 | 99 | [tool.pydocstyle] 100 | convention = 'google' 101 | match = '^pymatgen/(?!_).*\.py' 102 | inherit = false 103 | add-ignore = "D107, " 104 | 105 | [tool.autoflake] 106 | in-place = true 107 | remove-unused-variables = true 108 | remove-all-unused-imports = true 109 | expand-star-imports = true 110 | ignore-init-module-imports = true 111 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pytest==8.3.5 2 | pytest-cov==6.1.1 3 | coverage==7.8.2 4 | mypy==1.15.0 5 | pydocstyle==6.1.1 6 | flake8==7.2.0 7 | pylint==3.3.7 8 | black==25.1.0 9 | pydantic==2.11.5 10 | pydantic-settings==2.9.1 11 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pymatgen==2025.4.17 2 | pydantic==2.11.5 3 | pydantic-settings==2.9.1 4 | typing-extensions==4.13.2 5 | monty==2025.3.3 6 | numpy==1.26.1 7 | requests==2.32.3 8 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = .git,__pycache__,docs_rst/conf.py,tests 3 | # max-complexity = 10 4 | extend-ignore = E741,W291,W293,E501,E231,E203,W605 5 | max-line-length = 120 6 | 7 | [tool:pytest] 8 | addopts = --durations=30 --quiet 9 | 10 | [pydocstyle] 11 | ignore = D105,D2,D4 12 | match-dir=(?!(tests)).* 13 | 14 | [coverage:run] 15 | omit = *tests* 16 | relative_files = True 17 | 18 | [coverage.report] 19 | exclude_lines = 20 | pragma: no cover 21 | def __repr__ 22 | if self.debug: 23 | if settings.DEBUG 24 | raise AssertionError 25 | raise NotImplementedError 26 | if 0: 27 | if __name__ == .__main__.: 28 | @deprecated 29 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Materials Virtual Lab 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | from setuptools import setup, find_namespace_packages 5 | 6 | import os 7 | 8 | SETUP_PTH = os.path.dirname(os.path.abspath(__file__)) 9 | 10 | with open(os.path.join(SETUP_PTH, "README.md"), encoding="utf8") as f: 11 | desc = f.read() 12 | 13 | 14 | setup( 15 | name="pymatgen-io-validation", 16 | packages=find_namespace_packages(include=["pymatgen.io.*"]), 17 | version="0.1.0rc", 18 | install_requires=[ 19 | "pymatgen>=2024.5.1", 20 | "pydantic>=2.4.2", 21 | "pydantic-settings>=2.0.0", 22 | "requests>=2.28.1", 23 | ], 24 | extras_require={}, 25 | package_data={"pymatgen.io.validation": ["*.yaml"]}, 26 | author="Matthew Kuner, Janosh Riebesell, Jason Munro, Aaron Kaplan", 27 | author_email="matthewkuner@berkeley.edu", 28 | maintainer="Matthew Kuner", 29 | url="https://github.com/materialsproject/pymatgen-io-validation", 30 | license="BSD", 31 | description="A comprehensive I/O validator for electronic structure calculations", 32 | long_description=open("README.md", encoding="utf8").read(), 33 | long_description_content_type="text/markdown", 34 | keywords=["pymatgen"], 35 | classifiers=[ 36 | "Programming Language :: Python :: 3", 37 | "Programming Language :: Python :: 3.7", 38 | "Programming Language :: Python :: 3.8", 39 | "Development Status :: 4 - Beta", 40 | "Intended Audience :: Science/Research", 41 | "License :: OSI Approved :: BSD License", 42 | "Operating System :: OS Independent", 43 | "Topic :: Scientific/Engineering :: Information Analysis", 44 | "Topic :: Scientific/Engineering :: Physics", 45 | "Topic :: Scientific/Engineering :: Chemistry", 46 | "Topic :: Software Development :: Libraries :: Python Modules", 47 | ], 48 | ) 49 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import pytest 3 | 4 | from monty.serialization import loadfn 5 | from pymatgen.core import SETTINGS as PMG_SETTINGS 6 | 7 | from pymatgen.io.validation.common import VaspFiles 8 | 9 | _test_dir = Path(__file__).parent.joinpath("test_files").resolve() 10 | 11 | 12 | def set_fake_potcar_dir() -> None: 13 | FAKE_POTCAR_DIR = _test_dir / "vasp" / "fake_potcar" 14 | pytest.MonkeyPatch().setitem(PMG_SETTINGS, "PMG_VASP_PSP_DIR", str(FAKE_POTCAR_DIR)) 15 | 16 | 17 | @pytest.fixture(scope="session") 18 | def test_dir(): 19 | return _test_dir 20 | 21 | 22 | vasp_calc_data: dict[str, VaspFiles] = { 23 | k: VaspFiles(**loadfn(_test_dir / "vasp" / f"{k}.json.gz")) 24 | for k in ("Si_uniform", "Si_static", "Si_old_double_relax") 25 | } 26 | 27 | 28 | def incar_check_list(): 29 | """Pre-defined list of pass/fail tests.""" 30 | return loadfn(_test_dir / "vasp" / "scf_incar_check_list.yaml") 31 | -------------------------------------------------------------------------------- /tests/test_files/vasp/Si_old_double_relax.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/tests/test_files/vasp/Si_old_double_relax.json.gz -------------------------------------------------------------------------------- /tests/test_files/vasp/Si_static.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/tests/test_files/vasp/Si_static.json.gz -------------------------------------------------------------------------------- /tests/test_files/vasp/Si_uniform.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/tests/test_files/vasp/Si_uniform.json.gz -------------------------------------------------------------------------------- /tests/test_files/vasp/Si_uniform/CONTCAR.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/tests/test_files/vasp/Si_uniform/CONTCAR.gz -------------------------------------------------------------------------------- /tests/test_files/vasp/Si_uniform/INCAR.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/tests/test_files/vasp/Si_uniform/INCAR.gz -------------------------------------------------------------------------------- /tests/test_files/vasp/Si_uniform/INCAR.orig.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/tests/test_files/vasp/Si_uniform/INCAR.orig.gz -------------------------------------------------------------------------------- /tests/test_files/vasp/Si_uniform/KPOINTS.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/tests/test_files/vasp/Si_uniform/KPOINTS.gz -------------------------------------------------------------------------------- /tests/test_files/vasp/Si_uniform/KPOINTS.orig.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/tests/test_files/vasp/Si_uniform/KPOINTS.orig.gz -------------------------------------------------------------------------------- /tests/test_files/vasp/Si_uniform/OUTCAR.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/tests/test_files/vasp/Si_uniform/OUTCAR.gz -------------------------------------------------------------------------------- /tests/test_files/vasp/Si_uniform/POSCAR.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/tests/test_files/vasp/Si_uniform/POSCAR.gz -------------------------------------------------------------------------------- /tests/test_files/vasp/Si_uniform/POSCAR.orig.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/tests/test_files/vasp/Si_uniform/POSCAR.orig.gz -------------------------------------------------------------------------------- /tests/test_files/vasp/Si_uniform/custodian.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/tests/test_files/vasp/Si_uniform/custodian.json.gz -------------------------------------------------------------------------------- /tests/test_files/vasp/Si_uniform/vasprun.xml.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/tests/test_files/vasp/Si_uniform/vasprun.xml.gz -------------------------------------------------------------------------------- /tests/test_files/vasp/fake_Si_potcar_spec.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/tests/test_files/vasp/fake_Si_potcar_spec.json.gz -------------------------------------------------------------------------------- /tests/test_files/vasp/fake_potcar/POT_GGA_PAW_PBE/POTCAR.Al.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/tests/test_files/vasp/fake_potcar/POT_GGA_PAW_PBE/POTCAR.Al.gz -------------------------------------------------------------------------------- /tests/test_files/vasp/fake_potcar/POT_GGA_PAW_PBE/POTCAR.Eu.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/tests/test_files/vasp/fake_potcar/POT_GGA_PAW_PBE/POTCAR.Eu.gz -------------------------------------------------------------------------------- /tests/test_files/vasp/fake_potcar/POT_GGA_PAW_PBE/POTCAR.Gd.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/tests/test_files/vasp/fake_potcar/POT_GGA_PAW_PBE/POTCAR.Gd.gz -------------------------------------------------------------------------------- /tests/test_files/vasp/fake_potcar/POT_GGA_PAW_PBE/POTCAR.H.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/tests/test_files/vasp/fake_potcar/POT_GGA_PAW_PBE/POTCAR.H.gz -------------------------------------------------------------------------------- /tests/test_files/vasp/fake_potcar/POT_GGA_PAW_PBE/POTCAR.O.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/tests/test_files/vasp/fake_potcar/POT_GGA_PAW_PBE/POTCAR.O.gz -------------------------------------------------------------------------------- /tests/test_files/vasp/fake_potcar/POT_GGA_PAW_PBE/POTCAR.Si.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/tests/test_files/vasp/fake_potcar/POT_GGA_PAW_PBE/POTCAR.Si.gz -------------------------------------------------------------------------------- /tests/test_files/vasp/fake_potcar/POT_GGA_PAW_PBE_54/POTCAR.La.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/tests/test_files/vasp/fake_potcar/POT_GGA_PAW_PBE_54/POTCAR.La.gz -------------------------------------------------------------------------------- /tests/test_files/vasp/fake_potcar/POT_GGA_PAW_PBE_54/POTCAR.Si.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/tests/test_files/vasp/fake_potcar/POT_GGA_PAW_PBE_54/POTCAR.Si.gz -------------------------------------------------------------------------------- /tests/test_files/vasp/mp-1245223_site_props_check.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/pymatgen-io-validation/7e5644479c44cdea00a8da1b860b6e559ae59ad1/tests/test_files/vasp/mp-1245223_site_props_check.json.gz -------------------------------------------------------------------------------- /tests/test_files/vasp/scf_incar_check_list.yaml: -------------------------------------------------------------------------------- 1 | - err_msg: LCHIMAG 2 | should_pass: false 3 | incar: 4 | LCHIMAG: true 5 | vasprun: {} 6 | - err_msg: LNMR_SYM_RED 7 | should_pass: false 8 | incar: 9 | LNMR_SYM_RED: true 10 | vasprun: {} 11 | - err_msg: LDIPOL 12 | should_pass: false 13 | incar: 14 | LDIPOL: true 15 | vasprun: {} 16 | - err_msg: IDIPOL 17 | should_pass: false 18 | incar: 19 | IDIPOL: 2 20 | vasprun: {} 21 | - err_msg: EPSILON 22 | should_pass: false 23 | incar: 24 | EPSILON: 1.5 25 | vasprun: {} 26 | - err_msg: EPSILON 27 | should_pass: true 28 | incar: 29 | EPSILON: 1 30 | vasprun: {} 31 | - err_msg: EFIELD 32 | should_pass: false 33 | incar: 34 | EFIELD: 1 35 | vasprun: {} 36 | - err_msg: EFIELD 37 | should_pass: true 38 | incar: 39 | EFIELD: 0 40 | vasprun: {} 41 | - err_msg: EDIFF 42 | should_pass: false 43 | incar: 44 | EDIFF: 0.01 45 | vasprun: {} 46 | - err_msg: EDIFF 47 | should_pass: true 48 | incar: 49 | EDIFF: 1e-08 50 | vasprun: {} 51 | - err_msg: ENINI 52 | should_pass: false 53 | incar: 54 | ENINI: 1 55 | IALGO: 48 56 | vasprun: {} 57 | - err_msg: IALGO 58 | should_pass: false 59 | incar: 60 | ENINI: 1 61 | IALGO: 48 62 | vasprun: {} 63 | - err_msg: NBANDS 64 | should_pass: false 65 | vasprun: 66 | NBANDS: 1 67 | # TODO: This test seems wrong 68 | # - err_msg: NBANDS 69 | # should_pass: true 70 | # vasprun: 71 | # NBANDS: 40 72 | - err_msg: NBANDS 73 | should_pass: false 74 | vasprun: 75 | NBANDS: 1000 76 | - err_msg: LREAL 77 | should_pass: false 78 | incar: 79 | LREAL: true 80 | - err_msg: LREAL 81 | should_pass: true 82 | vasprun: {} 83 | incar: 84 | LREAL: false 85 | - err_msg: LMAXPAW 86 | should_pass: false 87 | incar: 88 | LMAXPAW: 0 89 | vasprun: {} 90 | - err_msg: NLSPLINE 91 | should_pass: false 92 | incar: 93 | NLSPLINE: true 94 | vasprun: {} 95 | - err_msg: ADDGRID 96 | should_pass: false 97 | incar: 98 | ADDGRID: true 99 | vasprun: {} 100 | - err_msg: LHFCALC 101 | should_pass: false 102 | incar: 103 | LHFCALC: true 104 | vasprun: {} 105 | - err_msg: AEXX 106 | should_pass: false 107 | incar: 108 | AEXX: 1 109 | vasprun: {} 110 | - err_msg: AGGAC 111 | should_pass: false 112 | incar: 113 | AGGAC: 0.5 114 | vasprun: {} 115 | - err_msg: AGGAX 116 | should_pass: false 117 | incar: 118 | AGGAX: 0.5 119 | vasprun: {} 120 | - err_msg: ALDAX 121 | should_pass: false 122 | incar: 123 | ALDAX: 0.5 124 | vasprun: {} 125 | - err_msg: AMGGAX 126 | should_pass: false 127 | incar: 128 | AMGGAX: 0.5 129 | vasprun: {} 130 | - err_msg: ALDAC 131 | should_pass: false 132 | incar: 133 | ALDAC: 0.5 134 | vasprun: {} 135 | - err_msg: AMGGAC 136 | should_pass: false 137 | incar: 138 | AMGGAC: 0.5 139 | vasprun: {} 140 | - err_msg: IBRION 141 | should_pass: false 142 | incar: 143 | IBRION: 3 144 | vasprun: {} 145 | - err_msg: IBRION 146 | should_pass: true 147 | incar: 148 | IBRION: 1 149 | vasprun: {} 150 | - err_msg: IBRION 151 | should_pass: true 152 | incar: 153 | IBRION: -1 154 | NSW: 0 # This is required as VASP auto-sets IBRION = 0 if NSW > 0 and IBRION not set 155 | vasprun: {} 156 | - err_msg: PSTRESS 157 | should_pass: false 158 | incar: 159 | PSTRESS: 1 160 | vasprun: {} 161 | - err_msg: SCALEE 162 | should_pass: false 163 | incar: 164 | SCALEE: 0.9 165 | vasprun: {} 166 | - err_msg: LNONCOLLINEAR 167 | should_pass: false 168 | incar: 169 | LNONCOLLINEAR: true 170 | vasprun: {} 171 | - err_msg: LSORBIT 172 | should_pass: false 173 | incar: 174 | LSORBIT: true 175 | vasprun: {} 176 | - err_msg: DEPER 177 | should_pass: false 178 | incar: 179 | DEPER: 0.5 180 | vasprun: {} 181 | - err_msg: EBREAK 182 | should_pass: false 183 | incar: 184 | EBREAK: 0.1 185 | - err_msg: GGA_COMPAT 186 | should_pass: false 187 | incar: 188 | GGA_COMPAT: false 189 | vasprun: {} 190 | - err_msg: ICORELEVEL 191 | should_pass: false 192 | incar: 193 | ICORELEVEL: 1 194 | vasprun: {} 195 | - err_msg: IMAGES 196 | should_pass: false 197 | incar: 198 | IMAGES: 1 199 | vasprun: {} 200 | - err_msg: IVDW 201 | should_pass: false 202 | incar: 203 | IVDW: 1 204 | vasprun: {} 205 | - err_msg: LBERRY 206 | should_pass: false 207 | incar: 208 | LBERRY: true 209 | vasprun: {} 210 | - err_msg: LCALCEPS 211 | should_pass: false 212 | incar: 213 | LCALCEPS: true 214 | vasprun: {} 215 | - err_msg: LCALCPOL 216 | should_pass: false 217 | incar: 218 | LCALCPOL: true 219 | vasprun: {} 220 | - err_msg: LHYPERFINE 221 | should_pass: false 222 | incar: 223 | LHYPERFINE: true 224 | vasprun: {} 225 | - err_msg: LKPOINTS_OPT 226 | should_pass: false 227 | incar: 228 | LKPOINTS_OPT: true 229 | vasprun: {} 230 | - err_msg: LKPROJ 231 | should_pass: false 232 | incar: 233 | LKPROJ: true 234 | vasprun: {} 235 | - err_msg: LMP2LT 236 | should_pass: false 237 | incar: 238 | LMP2LT: true 239 | vasprun: {} 240 | - err_msg: LSMP2LT 241 | should_pass: false 242 | incar: 243 | LSMP2LT: true 244 | vasprun: {} 245 | - err_msg: LOCPROJ 246 | should_pass: false 247 | incar: 248 | LOCPROJ: '1 : s : Hy' 249 | vasprun: {} 250 | - err_msg: LRPA 251 | should_pass: false 252 | incar: 253 | LRPA: true 254 | vasprun: {} 255 | - err_msg: LSPECTRAL 256 | should_pass: false 257 | incar: 258 | LSPECTRAL: true 259 | vasprun: {} 260 | - err_msg: LSUBROT 261 | should_pass: false 262 | incar: 263 | LSUBROT: true 264 | vasprun: {} 265 | - err_msg: ML_LMLFF 266 | should_pass: false 267 | incar: 268 | ML_LMLFF: true 269 | vasprun: {} 270 | - err_msg: WEIMIN 271 | should_pass: false 272 | incar: 273 | WEIMIN: 0.01 274 | vasprun: {} 275 | - err_msg: WEIMIN 276 | should_pass: true 277 | incar: 278 | WEIMIN: 0.0001 279 | vasprun: {} 280 | - err_msg: IWAVPR 281 | should_pass: false 282 | vasprun: {} 283 | incar: 284 | IWAVPR: 1 285 | - err_msg: LASPH 286 | should_pass: false 287 | incar: 288 | LASPH: false 289 | vasprun: {} 290 | - err_msg: LCORR 291 | should_pass: false 292 | incar: 293 | LCORR: false 294 | IALGO: 38 295 | vasprun: {} 296 | - err_msg: LCORR 297 | should_pass: true 298 | incar: 299 | LCORR: false 300 | IALGO: 58 301 | vasprun: {} 302 | - err_msg: RWIGS 303 | should_pass: false 304 | incar: 305 | RWIGS: 306 | - 1 307 | vasprun: {} 308 | - err_msg: VCA 309 | should_pass: false 310 | incar: 311 | VCA: 312 | - 0.5 313 | vasprun: {} 314 | - err_msg: PREC 315 | should_pass: false 316 | incar: 317 | PREC: NORMAL 318 | vasprun: {} 319 | - err_msg: ROPT 320 | should_pass: false 321 | incar: 322 | ROPT: 323 | - -0.001 324 | LREAL: true 325 | # TODO: This test seems wrong 326 | # - err_msg: ICHARG 327 | # should_pass: false 328 | # incar: 329 | # ICHARG: 11 330 | # vasprun: {} 331 | - err_msg: INIWAV 332 | should_pass: false 333 | incar: 334 | INIWAV: 0 335 | vasprun: {} 336 | - err_msg: ISTART 337 | should_pass: false 338 | incar: 339 | ISTART: 3 340 | vasprun: {} 341 | - err_msg: ISYM 342 | should_pass: false 343 | incar: 344 | ISYM: 3 345 | vasprun: {} 346 | - err_msg: ISYM 347 | should_pass: true 348 | incar: 349 | ISYM: 3 350 | LHFCALC: true 351 | vasprun: {} 352 | - err_msg: SYMPREC 353 | should_pass: false 354 | incar: 355 | SYMPREC: 0.01 356 | vasprun: {} 357 | - err_msg: LDAUU 358 | should_pass: false 359 | incar: 360 | LDAU: true 361 | LDAUU: 362 | - 5 363 | - 5 364 | - err_msg: LDAUJ 365 | should_pass: false 366 | incar: 367 | LDAU: true 368 | LDAUJ: 369 | - 5 370 | - 5 371 | - err_msg: LDAUL 372 | should_pass: false 373 | incar: 374 | LDAU: true 375 | LDAUL: 376 | - 5 377 | - 5 378 | - err_msg: LDAUTYPE 379 | should_pass: false 380 | incar: 381 | LDAU: true 382 | LDAUTYPE: 383 | - 1 384 | - err_msg: NWRITE 385 | should_pass: false 386 | incar: 387 | NWRITE: 1 388 | vasprun: {} 389 | - err_msg: LEFG 390 | should_pass: false 391 | incar: 392 | LEFG: true 393 | vasprun: {} 394 | - err_msg: LOPTICS 395 | should_pass: false 396 | incar: 397 | LOPTICS: true 398 | vasprun: {} 399 | - should_pass: true 400 | err_msg: ISIF 401 | incar: 402 | ISIF: 2 403 | vasprun: {} 404 | - should_pass: true 405 | err_msg: ISIF 406 | incar: 407 | ISIF: 3 408 | vasprun: {} 409 | - should_pass: true 410 | err_msg: ISIF 411 | incar: 412 | ISIF : 4 413 | vasprun: {} 414 | - should_pass: true 415 | err_msg: ISIF 416 | incar: 417 | ISIF : 5 418 | vasprun: {} 419 | - should_pass: true 420 | err_msg: ISIF 421 | incar: 422 | ISIF : 6 423 | vasprun: {} 424 | - should_pass: true 425 | err_msg: ISIF 426 | incar: 427 | ISIF : 7 428 | vasprun: {} 429 | - should_pass: true 430 | err_msg: ISIF 431 | incar: 432 | ISIF : 8 433 | vasprun: {} 434 | - should_pass: false 435 | err_msg: ISIF 436 | incar: 437 | ISIF: 1 438 | vasprun: {} 439 | - should_pass : false 440 | err_msg : ENCUT 441 | incar : 442 | ENCUT : 1. 443 | # Check that ENCUT is appropriately updated to be finite 444 | - should_pass : true 445 | err_msg : "should be >= inf" 446 | incar : 447 | ENCUT : 1. 448 | - should_pass : false 449 | err_msg : NGX 450 | incar: 451 | NGX : 1 452 | - should_pass : false 453 | err_msg : NGXF 454 | incar: 455 | NGXF : 1 456 | - should_pass : false 457 | err_msg : NGY 458 | incar: 459 | NGY : 1 460 | - should_pass : false 461 | err_msg : NGYF 462 | incar: 463 | NGYF : 1 464 | - should_pass : false 465 | err_msg : NGZ 466 | incar: 467 | NGZ : 1 468 | - should_pass : false 469 | err_msg : NGZF 470 | incar: 471 | NGZF : 1 472 | - should_pass : false 473 | err_msg : POTIM 474 | incar: 475 | POTIM : 10. 476 | IBRION : 1 477 | - should_pass : false 478 | err_msg : LMAXTAU 479 | incar: 480 | LMAXTAU: 2 481 | METAGGA: R2SCA 482 | ICHARG: 1 483 | - should_pass : true 484 | err_msg : LMAXTAU 485 | incar: 486 | LMAXTAU: 2 487 | ICHARG: 1 488 | METAGGA: "NONE" 489 | - should_pass : false 490 | err_msg : ENAUG 491 | incar: 492 | ENAUG: 1 493 | ICHARG: 1 494 | METAGGA: "R2SCA" 495 | - should_pass : true 496 | err_msg : "ENERGY CHANGE BETWEEN LAST TWO IONIC STEPS" 497 | - should_pass : true 498 | err_msg : KNOWN BUG 499 | incar: 500 | GGA : PE 501 | METAGGA : NONE 502 | - should_pass : false 503 | err_msg : CONVERGENCE --> Did not achieve electronic 504 | incar: 505 | NELM : 1 -------------------------------------------------------------------------------- /tests/test_validation.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import copy 3 | 4 | from monty.serialization import loadfn 5 | from pymatgen.core.structure import Structure 6 | from pymatgen.io.vasp import Kpoints 7 | 8 | from pymatgen.io.validation.validation import VaspValidator 9 | from pymatgen.io.validation.common import ValidationError, VaspFiles, PotcarSummaryStats 10 | 11 | from conftest import vasp_calc_data, incar_check_list, set_fake_potcar_dir 12 | 13 | 14 | ### TODO: add tests for many other MP input sets (e.g. MPNSCFSet, MPNMRSet, MPScanRelaxSet, Hybrid sets, etc.) 15 | ### TODO: add check for an MP input set that uses an IBRION other than [-1, 1, 2] 16 | ### TODO: add in check for MP set where LEFG = True 17 | ### TODO: add in check for MP set where LOPTICS = True 18 | ### TODO: fix logic for calc_type / run_type identification in Emmet!!! Or handle how we interpret them... 19 | 20 | set_fake_potcar_dir() 21 | 22 | 23 | def run_check( 24 | vasp_files: VaspFiles, 25 | error_message_to_search_for: str, 26 | should_the_check_pass: bool, 27 | vasprun_parameters_to_change: dict = {}, # for changing the parameters read from vasprun.xml 28 | incar_settings_to_change: dict = {}, # for directly changing the INCAR file, 29 | validation_doc_kwargs: dict = {}, # any kwargs to pass to the VaspValidator class 30 | ): 31 | _new_vf = vasp_files.model_dump() 32 | _new_vf["vasprun"]["parameters"].update(**vasprun_parameters_to_change) 33 | 34 | _new_vf["user_input"]["incar"].update(**incar_settings_to_change) 35 | 36 | validator = VaspValidator.from_vasp_input(vasp_files=VaspFiles(**_new_vf), **validation_doc_kwargs) 37 | has_specified_error = any([error_message_to_search_for in reason for reason in validator.reasons]) 38 | 39 | assert (not has_specified_error) if should_the_check_pass else has_specified_error 40 | 41 | 42 | def test_validation_from_files(test_dir): 43 | 44 | dir_name = test_dir / "vasp" / "Si_uniform" 45 | validator_from_paths = VaspValidator.from_directory(dir_name) 46 | validator_from_vasp_files = VaspValidator.from_vasp_input(vasp_files=vasp_calc_data["Si_uniform"]) 47 | 48 | # Note: because the POTCAR info cannot be distributed, `validator_from_paths` 49 | # is missing POTCAR checks. 50 | assert set([r for r in validator_from_paths.reasons if "POTCAR" not in r]) == set(validator_from_vasp_files.reasons) 51 | assert set([r for r in validator_from_paths.warnings if "POTCAR" not in r]) == set( 52 | validator_from_vasp_files.warnings 53 | ) 54 | assert all( 55 | getattr(validator_from_paths.vasp_files.user_input, k) == getattr(validator_from_paths.vasp_files.user_input, k) 56 | for k in ("incar", "structure", "kpoints") 57 | ) 58 | 59 | # Ensure that user modifcation to inputs after submitting valid 60 | # input leads to subsequent validation failures. 61 | # Re-instantiate VaspValidator to ensure pointers don't get messed up 62 | validated = VaspValidator(**validator_from_paths.model_dump()) 63 | og_md5 = validated.vasp_files.md5 64 | assert validated.valid 65 | assert validated._validated_md5 == og_md5 66 | 67 | validated.vasp_files.user_input.incar["ENCUT"] = 1.0 68 | new_md5 = validated.vasp_files.md5 69 | assert new_md5 != og_md5 70 | assert not validated.valid 71 | assert validated._validated_md5 == new_md5 72 | 73 | 74 | @pytest.mark.parametrize( 75 | "object_name", 76 | [ 77 | "Si_old_double_relax", 78 | ], 79 | ) 80 | def test_potcar_validation(test_dir, object_name): 81 | vf_og = vasp_calc_data[object_name] 82 | 83 | correct_potcar_summary_stats = [ 84 | PotcarSummaryStats(**ps) for ps in loadfn(test_dir / "vasp" / "fake_Si_potcar_spec.json.gz") 85 | ] 86 | 87 | # Check POTCAR (this test should PASS, as we ARE using a MP-compatible pseudopotential) 88 | vf = copy.deepcopy(vf_og) 89 | assert vf.user_input.potcar == correct_potcar_summary_stats 90 | run_check(vf, "PSEUDOPOTENTIALS", True) 91 | 92 | # Check POTCAR (this test should FAIL, as we are NOT using an MP-compatible pseudopotential) 93 | vf = copy.deepcopy(vf_og) 94 | incorrect_potcar_summary_stats = copy.deepcopy(correct_potcar_summary_stats) 95 | incorrect_potcar_summary_stats[0].stats.data.MEAN = 999999999 96 | vf.user_input.potcar = incorrect_potcar_summary_stats 97 | run_check(vf, "PSEUDOPOTENTIALS", False) 98 | 99 | 100 | @pytest.mark.parametrize("object_name", ["Si_static", "Si_old_double_relax"]) 101 | def test_scf_incar_checks(test_dir, object_name): 102 | vf_og = vasp_calc_data[object_name] 103 | vf_og.vasprun.final_structure._charge = 0.0 # patch for old test files 104 | 105 | # Pay *very* close attention to whether a tag is modified in the incar or in the vasprun.xml's parameters! 106 | # Some parameters are validated from one or the other of these items, depending on whether VASP 107 | # changes the value between the INCAR and the vasprun.xml (which it often does) 108 | 109 | for incar_check in incar_check_list(): 110 | run_check( 111 | vf_og, 112 | incar_check["err_msg"], 113 | incar_check["should_pass"], 114 | vasprun_parameters_to_change=incar_check.get("vasprun", {}), 115 | incar_settings_to_change=incar_check.get("incar", {}), 116 | ) 117 | ### Most all of the tests below are too specific to use the kwargs in the 118 | # run_check() method. Hence, the calcs are manually modified. Apologies. 119 | 120 | # NELECT check 121 | vf = copy.deepcopy(vf_og) 122 | # must set NELECT in `incar` for NELECT checks! 123 | vf.user_input.incar["NELECT"] = 9 124 | vf.vasprun.final_structure._charge = 1.0 125 | run_check(vf, "NELECT", False) 126 | 127 | # POTIM check #2 (checks energy change between steps) 128 | vf = copy.deepcopy(vf_og) 129 | vf.user_input.incar["IBRION"] = 2 130 | temp_ionic_step_1 = copy.deepcopy(vf.vasprun.ionic_steps[0]) 131 | temp_ionic_step_2 = copy.deepcopy(temp_ionic_step_1) 132 | temp_ionic_step_1["e_fr_energy"] = 0 133 | temp_ionic_step_2["e_fr_energy"] = 10000 134 | vf.vasprun.ionic_steps = [ 135 | temp_ionic_step_1, 136 | temp_ionic_step_2, 137 | ] 138 | run_check(vf, "POTIM", False) 139 | 140 | # EDIFFG energy convergence check (this check SHOULD fail) 141 | vf = copy.deepcopy(vf_og) 142 | temp_ionic_step_1 = copy.deepcopy(vf.vasprun.ionic_steps[0]) 143 | temp_ionic_step_2 = copy.deepcopy(temp_ionic_step_1) 144 | temp_ionic_step_1["e_0_energy"] = -1 145 | temp_ionic_step_2["e_0_energy"] = -2 146 | vf.vasprun.ionic_steps = [ 147 | temp_ionic_step_1, 148 | temp_ionic_step_2, 149 | ] 150 | run_check(vf, "ENERGY CHANGE BETWEEN LAST TWO IONIC STEPS", False) 151 | 152 | # EDIFFG / force convergence check (the MP input set for R2SCAN has force convergence criteria) 153 | # (the below test should NOT fail, because final forces are 0) 154 | vf = copy.deepcopy(vf_og) 155 | vf.user_input.incar.update(METAGGA="R2SCA", ICHARG=1) 156 | vf.vasprun.ionic_steps[-1]["forces"] = [[0, 0, 0], [0, 0, 0]] 157 | run_check(vf, "MAX FINAL FORCE MAGNITUDE", True) 158 | 159 | # EDIFFG / force convergence check (the MP input set for R2SCAN has force convergence criteria) 160 | # (the below test SHOULD fail, because final forces are high) 161 | vf = copy.deepcopy(vf_og) 162 | vf.user_input.incar.update(METAGGA="R2SCA", ICHARG=1, IBRION=1, NSW=1) 163 | vf.vasprun.ionic_steps[-1]["forces"] = [[10, 10, 10], [10, 10, 10]] 164 | run_check(vf, "MAX FINAL FORCE MAGNITUDE", False) 165 | 166 | # ISMEAR wrong for nonmetal check 167 | vf = copy.deepcopy(vf_og) 168 | vf.user_input.incar["ISMEAR"] = 1 169 | vf.vasprun.bandgap = 1 170 | run_check(vf, "ISMEAR", False) 171 | 172 | # ISMEAR wrong for metal relaxation check 173 | vf = copy.deepcopy(vf_og) 174 | vf.user_input.incar.update(ISMEAR=-5, NSW=1, IBRION=1, ICHARG=9) 175 | vf.vasprun.bandgap = 0 176 | run_check(vf, "ISMEAR", False) 177 | 178 | # SIGMA too high for nonmetal with ISMEAR = 0 check 179 | vf = copy.deepcopy(vf_og) 180 | vf.user_input.incar.update(ISMEAR=0, SIGMA=0.2) 181 | vf.vasprun.bandgap = 1 182 | run_check(vf, "SIGMA", False) 183 | 184 | # SIGMA too high for nonmetal with ISMEAR = -5 check (should not error) 185 | vf = copy.deepcopy(vf_og) 186 | vf.user_input.incar.update(ISMEAR=-5, SIGMA=1e3) 187 | vf.vasprun.bandgap = 1 188 | run_check(vf, "SIGMA", True) 189 | 190 | # SIGMA too high for metal check 191 | vf = copy.deepcopy(vf_og) 192 | vf.user_input.incar.update(ISMEAR=1, SIGMA=0.5) 193 | vf.vasprun.bandgap = 0 194 | run_check(vf, "SIGMA", False) 195 | 196 | # SIGMA too large check (i.e. eentropy term is > 1 meV/atom) 197 | vf = copy.deepcopy(vf_og) 198 | vf.vasprun.ionic_steps[0]["electronic_steps"][-1]["eentropy"] = 1 199 | run_check(vf, "The entropy term (T*S)", False) 200 | 201 | # LMAXMIX check for SCF calc 202 | vf = copy.deepcopy(vf_og) 203 | vf.user_input.incar.update( 204 | LMAXMIX=0, 205 | ICHARG=1, 206 | ) 207 | validated = VaspValidator.from_vasp_input(vasp_files=vf) 208 | # should not invalidate SCF calcs based on LMAXMIX 209 | assert not any(["LMAXMIX" in reason for reason in validated.reasons]) 210 | # rather should add a warning 211 | assert any(["LMAXMIX" in warning for warning in validated.warnings]) 212 | 213 | # EFERMI check (does not matter for VASP versions before 6.4) 214 | # must check EFERMI in the *incar*, as it is saved as a numerical value after VASP 215 | # guesses it in the vasprun.xml `parameters` 216 | vf = copy.deepcopy(vf_og) 217 | vf.vasprun.vasp_version = "5.4.4" 218 | vf.user_input.incar["EFERMI"] = 5 219 | run_check(vf, "EFERMI", True) 220 | 221 | # EFERMI check (matters for VASP versions 6.4 and beyond) 222 | # must check EFERMI in the *incar*, as it is saved as a numerical value after VASP 223 | # guesses it in the vasprun.xml `parameters` 224 | vf = copy.deepcopy(vf_og) 225 | vf.vasprun.vasp_version = "6.4.0" 226 | vf.user_input.incar["EFERMI"] = 5 227 | run_check(vf, "EFERMI", False) 228 | 229 | # LORBIT check (should have magnetization values for ISPIN=2) 230 | # Should be valid for this case, as no magmoms are expected for ISPIN = 1 231 | vf = copy.deepcopy(vf_og) 232 | vf.user_input.incar["ISPIN"] = 1 233 | vf.outcar.magnetization = [] 234 | run_check(vf, "LORBIT", True) 235 | 236 | # LORBIT check (should have magnetization values for ISPIN=2) 237 | # Should be valid in this case, as magmoms are present for ISPIN = 2 238 | vf = copy.deepcopy(vf_og) 239 | vf.user_input.incar["ISPIN"] = 2 240 | vf.outcar.magnetization = ( 241 | {"s": -0.0, "p": 0.0, "d": 0.0, "tot": 0.0}, 242 | {"s": -0.0, "p": 0.0, "d": 0.0, "tot": -0.0}, 243 | ) 244 | run_check(vf, "LORBIT", True) 245 | 246 | # LORBIT check (should have magnetization values for ISPIN=2) 247 | # Should be invalid in this case, as no magmoms are present for ISPIN = 2 248 | vf = copy.deepcopy(vf_og) 249 | vf.user_input.incar["ISPIN"] = 2 250 | vf.outcar.magnetization = [] 251 | run_check(vf, "LORBIT", False) 252 | 253 | # LMAXTAU check for METAGGA calcs (A value of 4 should fail for the `La` chemsys (has f electrons)) 254 | vf = copy.deepcopy(vf_og) 255 | vf.user_input.structure = Structure( 256 | lattice=[[2.9, 0, 0], [0, 2.9, 0], [0, 0, 2.9]], 257 | species=["La", "La"], 258 | coords=[[0, 0, 0], [0.5, 0.5, 0.5]], 259 | ) 260 | vf.user_input.incar.update( 261 | LMAXTAU=4, 262 | METAGGA="R2SCA", 263 | ICHARG=1, 264 | ) 265 | run_check(vf, "LMAXTAU", False) 266 | 267 | 268 | @pytest.mark.parametrize( 269 | "object_name", 270 | [ 271 | "Si_uniform", 272 | ], 273 | ) 274 | def test_nscf_checks(object_name): 275 | vf_og = vasp_calc_data[object_name] 276 | vf_og.vasprun.final_structure._charge = 0.0 # patch for old test files 277 | 278 | # ICHARG check 279 | run_check(vf_og, "ICHARG", True, incar_settings_to_change={"ICHARG": 11}) 280 | 281 | # LMAXMIX check for NSCF calc 282 | vf = copy.deepcopy(vf_og) 283 | vf.user_input.incar["LMAXMIX"] = 0 284 | validated = VaspValidator.from_vasp_input(vasp_files=vf) 285 | # should invalidate NSCF calcs based on LMAXMIX 286 | assert any(["LMAXMIX" in reason for reason in validated.reasons]) 287 | # and should *not* create a warning for NSCF calcs 288 | assert not any(["LMAXMIX" in warning for warning in validated.warnings]) 289 | 290 | # Explicit kpoints for NSCF calc check (this should not raise any flags for NSCF calcs) 291 | vf = copy.deepcopy(vf_og) 292 | vf.user_input.kpoints = Kpoints.from_dict( 293 | { 294 | "kpoints": [[0, 0, 0], [0, 0, 0.5]], 295 | "nkpoints": 2, 296 | "kpts_weights": [0.5, 0.5], 297 | "labels": ["Gamma", "X"], 298 | "style": "line_mode", 299 | "generation_style": "line_mode", 300 | } 301 | ) 302 | run_check(vf, "INPUT SETTINGS --> KPOINTS: explicitly", True) 303 | 304 | 305 | @pytest.mark.parametrize( 306 | "object_name", 307 | [ 308 | "Si_uniform", 309 | ], 310 | ) 311 | def test_common_error_checks(object_name): 312 | vf_og = vasp_calc_data[object_name] 313 | vf_og.vasprun.final_structure._charge = 0.0 # patch for old test files 314 | 315 | # METAGGA and GGA tag check (should never be set together) 316 | with pytest.raises(ValidationError): 317 | vfd = vf_og.model_dump() 318 | vfd["user_input"]["incar"].update( 319 | GGA="PE", 320 | METAGGA="R2SCAN", 321 | ) 322 | VaspFiles(**vfd).valid_input_set 323 | 324 | # Drift forces too high check - a warning 325 | vf = copy.deepcopy(vf_og) 326 | vf.outcar.drift = [[1, 1, 1]] 327 | validated = VaspValidator.from_vasp_input(vasp_files=vf) 328 | assert any("CONVERGENCE --> Excessive drift" in w for w in validated.warnings) 329 | 330 | # Final energy too high check 331 | vf = copy.deepcopy(vf_og) 332 | vf.vasprun.final_energy = 1e8 333 | run_check(vf, "LARGE POSITIVE FINAL ENERGY", False) 334 | 335 | # Excessive final magmom check (no elements Gd or Eu present) 336 | vf = copy.deepcopy(vf_og) 337 | vf.user_input.incar["ISPIN"] = 2 338 | vf.outcar.magnetization = [ 339 | {"s": 9.0, "p": 0.0, "d": 0.0, "tot": 9.0}, 340 | {"s": 9.0, "p": 0.0, "d": 0.0, "tot": 9.0}, 341 | ] 342 | run_check(vf, "MAGNETISM", False) 343 | 344 | # Excessive final magmom check (elements Gd or Eu present) 345 | # Should pass here, as it has a final magmom < 10 346 | vf = copy.deepcopy(vf_og) 347 | vf.user_input.incar["ISPIN"] = 2 348 | vf.user_input.structure = Structure( 349 | lattice=[[2.9, 0, 0], [0, 2.9, 0], [0, 0, 2.9]], 350 | species=["Gd", "Eu"], 351 | coords=[[0, 0, 0], [0.5, 0.5, 0.5]], 352 | ) 353 | vf.outcar.magnetization = ( 354 | {"s": 9.0, "p": 0.0, "d": 0.0, "tot": 9.0}, 355 | {"s": 9.0, "p": 0.0, "d": 0.0, "tot": 9.0}, 356 | ) 357 | run_check(vf, "MAGNETISM", True) 358 | 359 | # Excessive final magmom check (elements Gd or Eu present) 360 | # Should not pass here, as it has a final magmom > 10 361 | vf = copy.deepcopy(vf_og) 362 | vf.user_input.incar["ISPIN"] = 2 363 | vf.user_input.structure = Structure( 364 | lattice=[[2.9, 0, 0], [0, 2.9, 0], [0, 0, 2.9]], 365 | species=["Gd", "Eu"], 366 | coords=[[0, 0, 0], [0.5, 0.5, 0.5]], 367 | ) 368 | vf.outcar.magnetization = ( 369 | {"s": 11.0, "p": 0.0, "d": 0.0, "tot": 11.0}, 370 | {"s": 11.0, "p": 0.0, "d": 0.0, "tot": 11.0}, 371 | ) 372 | run_check(vf, "MAGNETISM", False) 373 | 374 | # Element Po / Am present 375 | for unsupported_ele in ("Po", "Am"): 376 | vf = copy.deepcopy(vf_og) 377 | vf.user_input.structure.replace_species({ele: unsupported_ele for ele in vf.user_input.structure.elements}) 378 | with pytest.raises(KeyError): 379 | run_check(vf, "COMPOSITION", False) 380 | 381 | 382 | def _update_kpoints_for_test(vf: VaspFiles, kpoints_updates: dict | Kpoints) -> None: 383 | orig_kpoints = vf.user_input.kpoints.as_dict() if vf.user_input.kpoints else {} 384 | if isinstance(kpoints_updates, Kpoints): 385 | kpoints_updates = kpoints_updates.as_dict() 386 | orig_kpoints.update(kpoints_updates) 387 | vf.user_input.kpoints = Kpoints.from_dict(orig_kpoints) 388 | 389 | 390 | @pytest.mark.parametrize("object_name", ["Si_old_double_relax"]) 391 | def test_kpoints_checks(object_name): 392 | vf_og = vasp_calc_data[object_name] 393 | vf_og.vasprun.final_structure._charge = 0.0 # patch for old test files 394 | 395 | # Valid mesh type check (should flag HCP structures) 396 | vf = copy.deepcopy(vf_og) 397 | vf.user_input.structure = Structure( 398 | lattice=[ 399 | [0.5, -0.866025403784439, 0], 400 | [0.5, 0.866025403784439, 0], 401 | [0, 0, 1.6329931618554521], 402 | ], 403 | coords=[[0, 0, 0], [0.333333333333333, -0.333333333333333, 0.5]], 404 | species=["H", "H"], 405 | ) # HCP structure 406 | _update_kpoints_for_test(vf, {"generation_style": "monkhorst"}) 407 | run_check(vf, "INPUT SETTINGS --> KPOINTS or KGAMMA:", False) 408 | 409 | # Valid mesh type check (should flag FCC structures) 410 | vf = copy.deepcopy(vf_og) 411 | vf.user_input.structure = Structure( 412 | lattice=[[0.0, 0.5, 0.5], [0.5, 0.0, 0.5], [0.5, 0.5, 0.0]], 413 | coords=[[0, 0, 0]], 414 | species=["H"], 415 | ) # FCC structure 416 | _update_kpoints_for_test(vf, {"generation_style": "monkhorst"}) 417 | run_check(vf, "INPUT SETTINGS --> KPOINTS or KGAMMA:", False) 418 | 419 | # Valid mesh type check (should *not* flag BCC structures) 420 | vf = copy.deepcopy(vf_og) 421 | vf.user_input.structure = Structure( 422 | lattice=[[2.9, 0, 0], [0, 2.9, 0], [0, 0, 2.9]], 423 | species=["H", "H"], 424 | coords=[[0, 0, 0], [0.5, 0.5, 0.5]], 425 | ) # BCC structure 426 | _update_kpoints_for_test(vf, {"generation_style": "monkhorst"}) 427 | run_check(vf, "INPUT SETTINGS --> KPOINTS or KGAMMA:", True) 428 | 429 | # Too few kpoints check 430 | vf = copy.deepcopy(vf_og) 431 | _update_kpoints_for_test(vf, {"kpoints": [[3, 3, 3]]}) 432 | run_check(vf, "INPUT SETTINGS --> KPOINTS or KSPACING:", False) 433 | 434 | # Explicit kpoints for SCF calc check 435 | vf = copy.deepcopy(vf_og) 436 | _update_kpoints_for_test( 437 | vf, 438 | { 439 | "kpoints": [[0, 0, 0], [0, 0, 0.5]], 440 | "nkpoints": 2, 441 | "kpts_weights": [0.5, 0.5], 442 | "style": "reciprocal", 443 | "generation_style": "Reciprocal", 444 | }, 445 | ) 446 | run_check(vf, "INPUT SETTINGS --> KPOINTS: explicitly", False) 447 | 448 | # Shifting kpoints for SCF calc check 449 | vf = copy.deepcopy(vf_og) 450 | _update_kpoints_for_test(vf, {"usershift": [0.5, 0, 0]}) 451 | run_check(vf, "INPUT SETTINGS --> KPOINTS: shifting", False) 452 | 453 | 454 | @pytest.mark.parametrize("object_name", ["Si_old_double_relax"]) 455 | def test_vasp_version_check(object_name): 456 | vf_og = vasp_calc_data[object_name] 457 | vf_og.vasprun.final_structure._charge = 0.0 # patch for old test files 458 | 459 | vasp_version_list = [ 460 | {"vasp_version": "4.0.0", "should_pass": False}, 461 | {"vasp_version": "5.0.0", "should_pass": False}, 462 | {"vasp_version": "5.4.0", "should_pass": False}, 463 | {"vasp_version": "5.4.4", "should_pass": True}, 464 | {"vasp_version": "6.0.0", "should_pass": True}, 465 | {"vasp_version": "6.1.3", "should_pass": True}, 466 | {"vasp_version": "6.4.2", "should_pass": True}, 467 | ] 468 | 469 | for check_info in vasp_version_list: 470 | vf = copy.deepcopy(vf_og) 471 | vf.vasprun.vasp_version = check_info["vasp_version"] 472 | run_check(vf, "VASP VERSION", check_info["should_pass"]) 473 | 474 | # Check for obscure VASP 5 bug with spin-polarized METAGGA calcs (should fail) 475 | vf = copy.deepcopy(vf_og) 476 | vf.vasprun.vasp_version = "5.0.0" 477 | vf.user_input.incar.update( 478 | METAGGA="R2SCAN", 479 | ISPIN=2, 480 | ) 481 | run_check(vf, "POTENTIAL BUG --> We believe", False) 482 | 483 | 484 | def test_fast_mode(): 485 | vf = vasp_calc_data["Si_uniform"] 486 | validated = VaspValidator.from_vasp_input(vasp_files=vf, check_potcar=False) 487 | 488 | # Without POTCAR check, this doc is valid 489 | assert validated.valid 490 | 491 | # Now introduce sequence of changes to test how fast validation works 492 | # Check order: 493 | # 1. VASP version 494 | # 2. Common errors (known bugs, missing output, etc.) 495 | # 3. K-point density 496 | # 4. POTCAR check 497 | # 5. INCAR check 498 | 499 | og_kpoints = vf.user_input.kpoints 500 | # Introduce series of errors, then ablate them 501 | # use unacceptable version and set METAGGA and GGA simultaneously -> 502 | # should only get version error in reasons 503 | vf.vasprun.vasp_version = "4.0.0" 504 | vf.vasprun.parameters["NBANDS"] = -5 505 | # bad_incar_updates = { 506 | # "METAGGA": "R2SCAN", 507 | # "GGA": "PE", 508 | # } 509 | # vf.user_input.incar.update(bad_incar_updates) 510 | # print(vf.user_input.kpoints.as_dict) 511 | _update_kpoints_for_test(vf, {"kpoints": [[1, 1, 2]]}) 512 | 513 | validated = VaspValidator.from_vasp_input(vasp_files=vf, check_potcar=True, fast=True) 514 | assert len(validated.reasons) == 1 515 | assert "VASP VERSION" in validated.reasons[0] 516 | 517 | # Now correct version, should just get METAGGA / GGA bug 518 | vf.vasprun.vasp_version = "6.3.2" 519 | # validated = VaspValidator.from_vasp_input(vf, check_potcar=True, fast=True) 520 | # assert len(validated.reasons) == 1 521 | # assert "KNOWN BUG" in validated.reasons[0] 522 | 523 | # Now remove GGA tag, get k-point density error 524 | # vf.user_input.incar.pop("GGA") 525 | validated = VaspValidator.from_vasp_input(vasp_files=vf, check_potcar=True, fast=True) 526 | assert len(validated.reasons) == 1 527 | assert "INPUT SETTINGS --> KPOINTS or KSPACING:" in validated.reasons[0] 528 | 529 | # Now restore k-points and don't check POTCAR --> get error 530 | _update_kpoints_for_test(vf, og_kpoints) 531 | validated = VaspValidator.from_vasp_input(vasp_files=vf, check_potcar=False, fast=True) 532 | assert len(validated.reasons) == 1 533 | assert "NBANDS" in validated.reasons[0] 534 | 535 | # Fix NBANDS, get no errors 536 | vf.vasprun.parameters["NBANDS"] = 10 537 | validated = VaspValidator.from_vasp_input(vasp_files=vf, check_potcar=True, fast=True) 538 | assert len(validated.reasons) == 0 539 | 540 | # Remove POTCAR, should fail validation 541 | vf.user_input.potcar = None 542 | validated = VaspValidator.from_vasp_input(vasp_files=vf, check_potcar=True, fast=True) 543 | assert "PSEUDOPOTENTIALS" in validated.reasons[0] 544 | 545 | 546 | def test_site_properties(test_dir): 547 | 548 | vf = VaspFiles(**loadfn(test_dir / "vasp" / "mp-1245223_site_props_check.json.gz")) 549 | vd = VaspValidator.from_vasp_input(vasp_files=vf) 550 | 551 | assert not vd.valid 552 | assert any("selective dynamics" in reason.lower() for reason in vd.reasons) 553 | 554 | # map non-zero velocities to input structure and re-check 555 | vf.user_input.structure.add_site_property( 556 | "velocities", [[1.0, 2.0, 3.0] for _ in range(len(vf.user_input.structure))] 557 | ) 558 | vd = VaspValidator.from_vasp_input(vasp_files=vf) 559 | assert any("non-zero velocities" in warning.lower() for warning in vd.warnings) 560 | -------------------------------------------------------------------------------- /tests/test_validation_without_potcar.py: -------------------------------------------------------------------------------- 1 | """Test validation without using a library of fake POTCARs.""" 2 | 3 | from tempfile import TemporaryDirectory 4 | 5 | from monty.serialization import loadfn 6 | import pytest 7 | from pymatgen.io.vasp import PotcarSingle 8 | from pymatgen.core import SETTINGS as PMG_SETTINGS 9 | 10 | from pymatgen.io.validation.validation import VaspValidator 11 | from pymatgen.io.validation.common import VaspFiles, PotcarSummaryStats 12 | 13 | 14 | def test_validation_without_potcars(test_dir): 15 | with TemporaryDirectory() as tmp_dir: 16 | 17 | pytest.MonkeyPatch().setitem(PMG_SETTINGS, "PMG_VASP_PSP_DIR", tmp_dir) 18 | 19 | # ensure that potcar library is unset to empty temporary directory 20 | with pytest.raises(FileNotFoundError): 21 | PotcarSingle.from_symbol_and_functional(symbol="Si", functional="PBE") 22 | 23 | # Add summary stats to input files 24 | ref_titel = "PAW_PBE Si 05Jan2001" 25 | ref_pspec = PotcarSingle._potcar_summary_stats["PBE"][ref_titel.replace(" ", "")][0] 26 | vf = loadfn(test_dir / "vasp" / "Si_uniform.json.gz") 27 | vf["user_input"]["potcar"] = [PotcarSummaryStats(titel=ref_titel, lexch="PE", **ref_pspec)] 28 | vf["user_input"]["potcar_functional"] = "PBE" 29 | vasp_files = VaspFiles(**vf) 30 | 31 | validated = VaspValidator(vasp_files=vasp_files) 32 | assert validated.valid 33 | --------------------------------------------------------------------------------