├── .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:
- SIGMA must be $\leq 0.05$ for non-metals (bandgap $> 0$).
- SIGMA must be $\leq 0.2$ for a metal (bandgap = 0).
- 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 |
--------------------------------------------------------------------------------