├── .github └── workflows │ ├── install.yml │ ├── stale_issues.yml │ ├── test_core.yml │ └── test_full.yml ├── .gitignore ├── .gitlab-ci.yml ├── .pre-commit-config.yaml ├── LICENSE.txt ├── README.md ├── core ├── poetry.lock └── pyproject.toml ├── docs ├── .gitignore ├── Makefile ├── _templates │ ├── class.rst │ ├── member.rst │ ├── module.rst │ └── package.rst ├── calculation.rst ├── conf.py ├── control.rst ├── convert.yaml ├── exception.rst ├── generate.rst ├── index.rst ├── make.bat └── raw.rst ├── makefile ├── poetry.lock ├── pyproject.toml ├── src └── py4vasp │ ├── __init__.py │ ├── _analysis │ └── mlff.py │ ├── _batch.py │ ├── _calculation │ ├── _CONTCAR.py │ ├── __init__.py │ ├── _dispersion.py │ ├── _stoichiometry.py │ ├── band.py │ ├── bandgap.py │ ├── base.py │ ├── born_effective_charge.py │ ├── current_density.py │ ├── density.py │ ├── dielectric_function.py │ ├── dielectric_tensor.py │ ├── dos.py │ ├── elastic_modulus.py │ ├── electronic_minimization.py │ ├── energy.py │ ├── exciton_density.py │ ├── exciton_eigenvector.py │ ├── force.py │ ├── force_constant.py │ ├── internal_strain.py │ ├── kpoint.py │ ├── local_moment.py │ ├── nics.py │ ├── pair_correlation.py │ ├── partial_density.py │ ├── phonon.py │ ├── phonon_band.py │ ├── phonon_dos.py │ ├── phonon_mode.py │ ├── piezoelectric_tensor.py │ ├── polarization.py │ ├── potential.py │ ├── projector.py │ ├── selection.py │ ├── slice_.py │ ├── stress.py │ ├── structure.py │ ├── system.py │ ├── velocity.py │ └── workfunction.py │ ├── _combine │ ├── base.py │ ├── energies.py │ ├── forces.py │ └── stresses.py │ ├── _config.py │ ├── _control │ ├── __init__.py │ ├── base.py │ ├── incar.py │ ├── kpoints.py │ └── poscar.py │ ├── _raw │ ├── access.py │ ├── data.py │ ├── data_wrapper.py │ ├── definition.py │ ├── mapping.py │ ├── read.py │ ├── schema.py │ └── write.py │ ├── _third_party │ ├── __init__.py │ ├── graph │ │ ├── __init__.py │ │ ├── contour.py │ │ ├── graph.py │ │ ├── mixin.py │ │ ├── plot.py │ │ ├── series.py │ │ └── trace.py │ ├── interactive.py │ └── view │ │ ├── __init__.py │ │ ├── mixin.py │ │ └── view.py │ ├── _util │ ├── check.py │ ├── convert.py │ ├── density.py │ ├── documentation.py │ ├── import_.py │ ├── index.py │ ├── parse.py │ ├── reader.py │ ├── select.py │ ├── slicing.py │ └── suggest.py │ ├── cli.py │ ├── combine.py │ ├── control.py │ ├── exception.py │ ├── raw.py │ └── scripts │ ├── __init__.py │ └── error_analysis.py └── tests ├── __init__.py ├── analysis ├── __init__.py └── test_mlff.py ├── batch └── test_batch.py ├── calculation ├── __init__.py ├── conftest.py ├── test_band.py ├── test_bandgap.py ├── test_base.py ├── test_born_effective_charge.py ├── test_class.py ├── test_contcar.py ├── test_current_density.py ├── test_default_calculation.py ├── test_density.py ├── test_dielectric_function.py ├── test_dielectric_tensor.py ├── test_dispersion.py ├── test_dos.py ├── test_elastic_modulus.py ├── test_electronic_minimization.py ├── test_energy.py ├── test_exciton_density.py ├── test_exciton_eigenvector.py ├── test_force.py ├── test_force_constant.py ├── test_internal_strain.py ├── test_kpoint.py ├── test_local_moment.py ├── test_nics.py ├── test_pair_correlation.py ├── test_partial_density.py ├── test_phonon_band.py ├── test_phonon_dos.py ├── test_phonon_mode.py ├── test_piezoelectric_tensor.py ├── test_polarization.py ├── test_potential.py ├── test_projector.py ├── test_repr.py ├── test_slice_mixin.py ├── test_stoichiometry.py ├── test_stress.py ├── test_structure.py ├── test_system.py ├── test_velocity.py └── test_workfunction.py ├── cli └── test_cli.py ├── conftest.py ├── control ├── __init__.py ├── test_base.py ├── test_incar.py ├── test_kpoints.py └── test_poscar.py ├── doctest └── test_calculation.py ├── raw ├── conftest.py ├── test_access.py ├── test_data.py ├── test_definition.py ├── test_mapping.py ├── test_read.py ├── test_schema.py ├── test_write.py └── util.py ├── scripts └── test_error_analysis.py ├── test_version.py ├── third_party ├── graph │ ├── test_graph.py │ ├── test_graph_mixin.py │ └── test_plot.py ├── test_interactive.py └── view │ ├── test_view.py │ └── test_view_mixin.py └── util ├── test_check.py ├── test_convert.py ├── test_density.py ├── test_documentation.py ├── test_import.py ├── test_index.py ├── test_parse.py ├── test_reader.py ├── test_select.py ├── test_slicing.py └── test_suggest.py /.github/workflows/install.yml: -------------------------------------------------------------------------------- 1 | name: install 2 | 3 | on: 4 | schedule: 5 | - cron: "0 2 * * 6" 6 | push: 7 | branches: [master] 8 | pull_request: 9 | branches: [master] 10 | 11 | jobs: 12 | tests: 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | os: [ubuntu-latest, macos-latest, windows-latest] 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Set up conda 22 | uses: conda-incubator/setup-miniconda@v3 23 | with: 24 | auto-update-conda: true 25 | python-version: 3.9 26 | - name: Install py4vasp and testing tools 27 | shell: bash -el {0} 28 | run: | 29 | python -m pip install --progress-bar=off --upgrade pip 30 | pip install --progress-bar=off . 31 | pip install --progress-bar=off pytest hypothesis 32 | pip install --progress-bar=off ipykernel 33 | - name: Install mdtraj with conda 34 | shell: bash -el {0} 35 | run: | 36 | conda info 37 | conda install -q -c conda-forge mdtraj 38 | - name: Test with pytest 39 | shell: bash -el {0} 40 | run: | 41 | pytest --version 42 | pytest 43 | -------------------------------------------------------------------------------- /.github/workflows/stale_issues.yml: -------------------------------------------------------------------------------- 1 | name: Close inactive issues 2 | on: 3 | schedule: 4 | - cron: "30 1 * * *" 5 | 6 | jobs: 7 | close-issues: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | issues: write 11 | pull-requests: write 12 | steps: 13 | - uses: actions/stale@v5 14 | with: 15 | days-before-issue-stale: 300 16 | days-before-issue-close: 65 17 | stale-issue-label: "stale" 18 | stale-issue-message: "We consider issues as stale if there has been no activity in them for 10 months." 19 | close-issue-message: "This issue was closed after 1 year of inactivity." 20 | exempt-issue-labels: "bug,enhancement" 21 | days-before-pr-stale: -1 22 | days-before-pr-close: -1 23 | repo-token: ${{ secrets.GITHUB_TOKEN }} 24 | -------------------------------------------------------------------------------- /.github/workflows/test_core.yml: -------------------------------------------------------------------------------- 1 | name: test-core 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | tests: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | os: [ubuntu-latest, macos-latest, windows-latest] 15 | python-version: ["3.9", "3.10", "3.11", "3.12"] 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Set up Python ${{ matrix.python-version }} 20 | uses: actions/setup-python@v5 21 | with: 22 | python-version: ${{ matrix.python-version }} 23 | - name: Set up poetry 24 | run: | 25 | python --version 26 | python -m pip install --progress-bar=off --upgrade pip 27 | pip install --progress-bar=off "poetry!=1.4.1" 28 | - name: Install py4vasp-core 29 | run: | 30 | cp core/* . 31 | poetry --version 32 | poetry install 33 | - name: Test with pytest 34 | run: | 35 | poetry run pytest --version 36 | poetry run pytest 37 | -------------------------------------------------------------------------------- /.github/workflows/test_full.yml: -------------------------------------------------------------------------------- 1 | name: test-full 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | tests: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | os: [ubuntu-latest, macos-latest, windows-latest] 15 | python-version: ["3.9", "3.10", "3.11", "3.12"] 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Set up conda 20 | uses: conda-incubator/setup-miniconda@v3 21 | with: 22 | auto-update-conda: true 23 | python-version: ${{ matrix.python-version }} 24 | - name: Set up poetry 25 | shell: bash -el {0} 26 | run: | 27 | python --version 28 | python -m pip install --progress-bar=off --upgrade pip 29 | pip install --progress-bar=off "poetry<2" 30 | - name: Install py4vasp 31 | shell: bash -el {0} 32 | run: | 33 | poetry --version 34 | poetry install 35 | - name: Install mdtraj with conda 36 | shell: bash -el {0} 37 | run: | 38 | conda info 39 | conda install -q -c conda-forge "mdtraj<1.10.2" 40 | - name: Test with pytest 41 | shell: bash -el {0} 42 | run: | 43 | poetry run pytest --version 44 | poetry run pytest --cov=py4vasp --cov-report term 45 | - name: Check code style 46 | shell: bash -el {0} 47 | run: | 48 | poetry run isort --version 49 | poetry run isort --check src 50 | poetry run isort --check tests 51 | poetry run black --version 52 | poetry run black --check src 53 | poetry run black --check tests 54 | 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | # File open for editing in vi 3 | *.swp 4 | # autogenerated by dephell 5 | setup.py 6 | # autogenerated by poetry2conda 7 | py4vasp-env.yml 8 | 9 | # Byte-compiled / optimized / DLL files 10 | __pycache__/ 11 | *.py[cod] 12 | *$py.class 13 | 14 | # C extensions 15 | *.so 16 | 17 | # Distribution / packaging 18 | .Python 19 | build/ 20 | develop-eggs/ 21 | dist/ 22 | downloads/ 23 | eggs/ 24 | .eggs/ 25 | lib/ 26 | lib64/ 27 | parts/ 28 | sdist/ 29 | var/ 30 | wheels/ 31 | pip-wheel-metadata/ 32 | share/python-wheels/ 33 | *.egg-info/ 34 | .installed.cfg 35 | *.egg 36 | MANIFEST 37 | 38 | # PyInstaller 39 | # Usually these files are written by a python script from a template 40 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 41 | *.manifest 42 | *.spec 43 | 44 | # Installer logs 45 | pip-log.txt 46 | pip-delete-this-directory.txt 47 | 48 | # Unit test / coverage reports 49 | htmlcov/ 50 | .tox/ 51 | .nox/ 52 | .coverage 53 | .coverage.* 54 | .cache 55 | nosetests.xml 56 | coverage.xml 57 | *.cover 58 | *.py,cover 59 | .hypothesis/ 60 | .pytest_cache/ 61 | 62 | # Translations 63 | *.mo 64 | *.pot 65 | 66 | # Django stuff: 67 | *.log 68 | local_settings.py 69 | db.sqlite3 70 | db.sqlite3-journal 71 | 72 | # Flask stuff: 73 | instance/ 74 | .webassets-cache 75 | 76 | # Scrapy stuff: 77 | .scrapy 78 | 79 | # Sphinx documentation 80 | docs/_build/ 81 | 82 | # PyBuilder 83 | target/ 84 | 85 | # Jupyter Notebook 86 | .ipynb_checkpoints 87 | 88 | # IPython 89 | profile_default/ 90 | ipython_config.py 91 | 92 | # pyenv 93 | .python-version 94 | 95 | # pipenv 96 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 97 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 98 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 99 | # install all needed dependencies. 100 | #Pipfile.lock 101 | 102 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 103 | __pypackages__/ 104 | 105 | # Celery stuff 106 | celerybeat-schedule 107 | celerybeat.pid 108 | 109 | # SageMath parsed files 110 | *.sage.py 111 | 112 | # Environments 113 | .env 114 | .venv 115 | env/ 116 | venv/ 117 | ENV/ 118 | env.bak/ 119 | venv.bak/ 120 | 121 | # Spyder project settings 122 | .spyderproject 123 | .spyproject 124 | 125 | # Rope project settings 126 | .ropeproject 127 | 128 | # mkdocs documentation 129 | /site 130 | 131 | # mypy 132 | .mypy_cache/ 133 | .dmypy.json 134 | dmypy.json 135 | 136 | # Pyre type checker 137 | .pyre/ 138 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: python:latest 2 | 3 | variables: 4 | PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" 5 | 6 | cache: 7 | paths: 8 | - .cache/pip 9 | 10 | before_script: 11 | - python -V 12 | - pip install virtualenv 13 | - virtualenv venv 14 | - source venv/bin/activate 15 | 16 | stages: 17 | - install 18 | - unit-test 19 | - code-style 20 | 21 | Installation: 22 | stage: install 23 | tags: 24 | - rhodan 25 | script: 26 | - pip install poetry 27 | - poetry -V 28 | - poetry install 29 | 30 | Unit tests: 31 | stage: unit-test 32 | tags: 33 | - rhodan 34 | script: 35 | - poetry run pytest --cov=py4vasp 36 | 37 | Code style: 38 | stage: code-style 39 | tags: 40 | - rhodan 41 | script: 42 | - poetry run black --check src 43 | - poetry run black --check tests 44 | 45 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/ambv/black 3 | rev: 22.6.0 4 | hooks: 5 | - id: black 6 | - repo: https://github.com/pycqa/isort 7 | rev: 5.12.0 8 | hooks: 9 | - id: isort 10 | name: isort (python) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # py4vasp 2 | 3 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 4 | [![test-full](https://github.com/vasp-dev/py4vasp/actions/workflows/test_full.yml/badge.svg)](https://github.com/vasp-dev/py4vasp/actions/workflows/test_full.yml) 5 | [![test-core](https://github.com/vasp-dev/py4vasp/actions/workflows/test_core.yml/badge.svg)](https://github.com/vasp-dev/py4vasp/actions/workflows/test_core.yml) 6 | 7 | > Please note that this document is intended mostly for developers that want to use 8 | > the version of py4vasp provided on Github. If you just want to install py4vasp to 9 | > use it, please follow the [official documentation](https://vasp.at/py4vasp/latest). 10 | 11 | ## Installation 12 | 13 | We use the [poetry dependency manager](https://python-poetry.org/) which takes care of 14 | all dependencies and maintains a virtual environment to check the code. If you want to 15 | test something in the virtual environment, just use e.g. `poetry run jupyter-notebook`. 16 | 17 | We recommend installing py4vasp in a conda environment to resolve issues related to 18 | installing `mdtraj` with pip. To do this please use the following steps. The last step 19 | will test whether everything worked 20 | ~~~shell 21 | conda create --name py4vasp-env python=3.9 22 | git clone git@github.com:vasp-dev/py4vasp.git 23 | conda activate py4vasp-env 24 | pip install "poetry<2" 25 | cd py4vasp 26 | poetry install 27 | conda install -c conda-forge mdtraj 28 | poetry run pytest 29 | ~~~ 30 | Note that this will install py4vasp into the conda environment. This isolates the code 31 | from all packages you have installed in other conda environments. Using poetry makes 32 | sure that when you modify the code all the relevant dependencies are tracked. 33 | 34 | ## py4vasp core 35 | 36 | If you want to use py4vasp to develop your own scripts, you may want to limit the amount 37 | of external dependencies. To this end, we provide alternative configuration files that 38 | only install numpy, h5py, and the development dependencies. To install this core package 39 | replace the configurations files in the root folder with the ones in the `core` folder 40 | ~~~shell 41 | cp core/* . 42 | ~~~ 43 | Then you can install py4vasp with the same steps as above. Alternatively, since 44 | py4vasp-core does not use mdtraj, you can also install everything in a virtual environment 45 | mangaged by poetry 46 | ~~~shell 47 | pip install poetry 48 | poetry install 49 | poetry run pytest 50 | ~~~ 51 | Note that some tests will be skipped because they require the external packages to run. 52 | If you want to exclude even the development dependencies, you can run 53 | ~~~shell 54 | poetry install --without dev 55 | ~~~ 56 | for the minimal installation. 57 | 58 | ## Code style 59 | 60 | Code style is enforced, but is not something the developer should spend time on, so we 61 | decided on using black and isort. Please run 62 | ~~~shell 63 | black src tests 64 | isort src tests 65 | ~~~ 66 | before committing the code. This will autoformat your code and sort the import 67 | statements in a consistent order. If you would like this code formatting to be done 68 | along with each commit, you can run 69 | ~~~shell 70 | pre-commit install 71 | ~~~ 72 | 73 | ## Contributing to py4vasp 74 | 75 | We welcome contributions to py4vasp. To improve the code please follow this workflow 76 | 77 | * Create an issue for the bugfix or feature you plan to work on, this gives the option 78 | to provide some input before work is invested. 79 | * Implement your work in a fork of the repository and create a pull request for it. 80 | Please make sure to test your code thoroughly and commit the tests in the pull 81 | request in the tests directory. 82 | * In the message to your merge request mention the issue the code attempts to solve. 83 | * We will try to include your merge request rapidly when all the tests pass and your 84 | code is covered by tests. 85 | 86 | Please limit the size of a pull request to approximately 200 lines of code 87 | otherwise reviewing the changes gets unwieldy. Prefer splitting the work into 88 | multiple smaller chunks if necessary. 89 | -------------------------------------------------------------------------------- /core/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "py4vasp-core" 3 | version = "0.10.2" 4 | description = "Tool for assisting with the analysis and setup of VASP calculations." 5 | authors = [ 6 | "VASP Software GmbH ", 7 | "Martin Schlipf ", 8 | "Henrique Miranda ", 9 | "Orest Dubay ", 10 | "Jonathan Lahnsteiner ", 11 | "Eisuke Kawashima ", 12 | "Sudarshan Vijay ", 13 | "Marie-Therese Huebsch ", 14 | "Michael Wolloch ", 15 | "Andreas Singraber ", 16 | "Alexey Tal ", 17 | "Tomáš Bučko ", 18 | "Max Liebetreu ", 19 | ] 20 | license = "Apache-2.0" 21 | readme = "README.md" 22 | packages = [{include = "py4vasp", from = "src"}] 23 | homepage = "https://vasp.at/py4vasp/latest" 24 | repository = "https://github.com/vasp-dev/py4vasp" 25 | 26 | [tool.poetry.urls] 27 | "Support Forum" = "https://vasp.at/forum/" 28 | 29 | 30 | [tool.poetry.dependencies] 31 | python = ">=3.9" 32 | numpy = ">=1.23" 33 | h5py = ">=3.7.0" 34 | 35 | [tool.poetry.group.dev.dependencies] 36 | pytest = ">=7.1.2" 37 | pytest-cov = ">=3.0.0" 38 | pylint = ">=2.15" 39 | hypothesis = ">=6.48.1" 40 | black = ">=22.6.0" 41 | isort = ">=5.10.1" 42 | 43 | [tool.isort] 44 | profile = "black" 45 | 46 | 47 | [build-system] 48 | requires = ["poetry-core"] 49 | build-backend = "poetry.core.masonry.api" 50 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | api 2 | _packages 3 | _classes 4 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/_templates/class.rst: -------------------------------------------------------------------------------- 1 | {{ name | escape | underline }} 2 | 3 | .. autoclass:: {{ fullname }} 4 | :members: 5 | -------------------------------------------------------------------------------- /docs/_templates/member.rst: -------------------------------------------------------------------------------- 1 | {{ name | escape | underline }} 2 | 3 | .. container:: quantity 4 | 5 | .. currentmodule:: py4vasp.calculation 6 | .. data:: {{ name }} 7 | 8 | .. currentmodule:: py4vasp.calculation._{{name}} 9 | .. autoclass:: {% 10 | if name == "CONTCAR" -%} 11 | CONTCAR 12 | {%- else -%} 13 | {%- for part in name.split("_") -%} 14 | {{ part.capitalize() }} 15 | {%- endfor -%} 16 | {%- endif %} 17 | :members: 18 | :inherited-members: 19 | :exclude-members: from_data, from_file, from_path, path 20 | -------------------------------------------------------------------------------- /docs/_templates/module.rst: -------------------------------------------------------------------------------- 1 | {{ name | escape | underline }} 2 | 3 | .. automodule:: {{ fullname }} 4 | 5 | .. autosummary:: 6 | 7 | {% for function in functions %} 8 | {{ function }} 9 | {% endfor %} 10 | 11 | {% for function in functions %} 12 | .. autofunction:: {{ function }} 13 | {% endfor %} 14 | -------------------------------------------------------------------------------- /docs/_templates/package.rst: -------------------------------------------------------------------------------- 1 | {{ name | escape | underline }} 2 | 3 | .. automodule:: {{ fullname }} 4 | 5 | .. rubric:: Attributes 6 | 7 | .. autosummary:: 8 | :toctree: 9 | :template: member.rst 10 | 11 | {% for member in members %} 12 | {{ member }} 13 | {% endfor %} 14 | -------------------------------------------------------------------------------- /docs/calculation.rst: -------------------------------------------------------------------------------- 1 | calculation 2 | =========== 3 | 4 | .. autodata:: py4vasp.calculation 5 | :annotation: 6 | 7 | .. autoclass:: py4vasp.Calculation 8 | :members: from_path, from_file, path 9 | 10 | 11 | .. jinja:: 12 | .. autosummary:: 13 | :nosignatures: 14 | {% for quantity in calculation.QUANTITIES %} 15 | {% if not quantity.startswith("_") -%} 16 | ~py4vasp._calculation.{{ quantity }}. 17 | {%- for part in quantity.split("_") -%} 18 | {{ part.capitalize() }} 19 | {%- endfor -%} 20 | {%- endif -%} 21 | {% endfor %} 22 | 23 | 24 | .. .. autosummary:: 25 | :recursive: 26 | py4vasp.Calculation 27 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = "py4vasp" 21 | copyright = "2024, VASP Software GmbH" 22 | author = "VASP Software GmbH" 23 | 24 | # The full version, including alpha/beta/rc tags 25 | release = "0.10.0" 26 | 27 | 28 | # -- General configuration --------------------------------------------------- 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = ["sphinx.ext.napoleon", "sphinx_automodapi.automodapi"] 34 | automodapi_inheritance_diagram = False 35 | autosummary_ignore_module_all = False 36 | 37 | # Add any paths that contain templates here, relative to this directory. 38 | templates_path = ["_templates"] 39 | 40 | # List of patterns, relative to source directory, that match files and 41 | # directories to ignore when looking for source files. 42 | # This pattern also affects html_static_path and html_extra_path. 43 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 44 | 45 | 46 | # -- Options for HTML output ------------------------------------------------- 47 | 48 | # The theme to use for HTML and HTML Help pages. See the documentation for 49 | # a list of builtin themes. 50 | 51 | # # default theme 52 | # html_theme = "nature" 53 | 54 | # a minimal theme for the website 55 | html_theme = "basic" 56 | html_show_sphinx = False 57 | html_show_copyright = False 58 | html_domain_indices = False 59 | html_use_index = False 60 | 61 | # Add any paths that contain custom static files (such as style sheets) here, 62 | # relative to this directory. They are copied after the builtin static files, 63 | # so a file named "default.css" will overwrite the builtin "default.css". 64 | html_static_path = ["_static"] 65 | 66 | # remove common py4vasp prefix from index 67 | modindex_common_prefix = ["py4vasp."] 68 | 69 | 70 | # -- Custom extension of Sphinx ---------------------------------------------- 71 | from docutils import nodes 72 | from docutils.parsers.rst import Directive 73 | from docutils.statemachine import ViewList 74 | from jinja2 import Template 75 | 76 | from py4vasp import _calculation, _util 77 | 78 | 79 | # defines an INCAR tag 80 | def tag_role(name, rawtext, text, lineno, inliner, options={}, content=[]): 81 | url = f"https://www.vasp.at/wiki/index.php/{text}" 82 | node = nodes.reference(rawtext, text, refuri=url, **options) 83 | return [node], [] 84 | 85 | 86 | class JinjaDirective(Directive): 87 | has_content = True 88 | 89 | def run(self): 90 | template_string = "\n".join(self.content) 91 | template = Template(template_string) 92 | rendered_content = template.render(calculation=_calculation) 93 | filename = self.state.document.current_source 94 | view_list = ViewList(rendered_content.split("\n"), filename) 95 | node = nodes.Element() 96 | self.state.nested_parse(view_list, 0, node) 97 | return node.children 98 | 99 | 100 | def setup(app): 101 | app.add_directive("jinja", JinjaDirective) 102 | app.add_role("tag", tag_role) 103 | return 104 | -------------------------------------------------------------------------------- /docs/control.rst: -------------------------------------------------------------------------------- 1 | control 2 | ======= 3 | 4 | .. automodapi:: py4vasp.control 5 | -------------------------------------------------------------------------------- /docs/convert.yaml: -------------------------------------------------------------------------------- 1 | latest: 2 | - dirhtml 3 | - calculation: 4 | - dirhtml/_packages/py4vasp.calculation 5 | - dirhtml/_classes/py4vasp.Calculation 6 | - dirhtml/_packages/py4vasp.calculation.* 7 | - exception: 8 | - dirhtml/exception 9 | - dirhtml/api/py4vasp.exception.* 10 | -------------------------------------------------------------------------------- /docs/exception.rst: -------------------------------------------------------------------------------- 1 | exception 2 | ========= 3 | 4 | .. automodapi:: py4vasp.exception 5 | -------------------------------------------------------------------------------- /docs/generate.rst: -------------------------------------------------------------------------------- 1 | generate 2 | ======== 3 | 4 | .. jinja:: 5 | 6 | {% for quantity in calculation.QUANTITIES %} 7 | {% if not quantity.startswith("_") %} 8 | .. automodule:: py4vasp._calculation.{{ quantity }} 9 | :members: 10 | {% endif %} 11 | {% endfor %} 12 | 13 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/raw.rst: -------------------------------------------------------------------------------- 1 | raw 2 | === 3 | 4 | .. automodapi:: py4vasp.raw 5 | :allowed-package-names: py4vasp._raw 6 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | .PHONY: test format style import coverage 4 | 5 | TEST ?= tests 6 | 7 | test: 8 | poetry run pytest $(TEST) 9 | 10 | coverage: 11 | poetry run pytest --cov=py4vasp --cov-report html 12 | 13 | format: import style 14 | 15 | style: 16 | poetry run black . 17 | 18 | import: 19 | poetry run isort . 20 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "py4vasp" 3 | version = "0.10.2" 4 | description = "Tool for assisting with the analysis and setup of VASP calculations." 5 | authors = [ 6 | "VASP Software GmbH ", 7 | "Martin Schlipf ", 8 | "Henrique Miranda ", 9 | "Orest Dubay ", 10 | "Jonathan Lahnsteiner ", 11 | "Eisuke Kawashima ", 12 | "Sudarshan Vijay ", 13 | "Marie-Therese Huebsch ", 14 | "Michael Wolloch ", 15 | "Andreas Singraber ", 16 | "Alexey Tal ", 17 | "Tomáš Bučko ", 18 | "Max Liebetreu " 19 | ] 20 | license = "Apache-2.0" 21 | readme = "README.md" 22 | homepage = "https://vasp.at/py4vasp/latest" 23 | repository = "https://github.com/vasp-dev/py4vasp" 24 | 25 | [tool.poetry.urls] 26 | "Support Forum" = "https://vasp.at/forum/" 27 | 28 | 29 | [tool.poetry.dependencies] 30 | python = ">=3.9" 31 | numpy = ">=1.23" 32 | h5py = ">=3.7.0" 33 | pandas = ">=1.4.3" 34 | nglview = ">=3.0.5" 35 | ase = ">=3.22.1" 36 | plotly = ">=5.9.0" 37 | kaleido = ">=0.2.1,<0.2.1.post1 || >0.2.1.post1" 38 | ipython = ">=8.12" 39 | scipy = ">=1.12.0" 40 | click = ">=8.1.8" 41 | 42 | [tool.poetry.group.dev.dependencies] 43 | pytest = ">=7.1.2" 44 | pytest-cov = ">=3.0.0" 45 | pylint = ">=2.15" 46 | hypothesis = ">=6.48.1" 47 | black = ">=22.6.0" 48 | isort = ">=5.10.1" 49 | ipykernel = ">=6.25.0" 50 | pre-commit = ">=3.3.3" 51 | 52 | [tool.poetry.group.doc.dependencies] 53 | sphinx = ">=5.0.2" 54 | sphinx-automodapi = ">=0.14.1" 55 | 56 | [tool.poetry.scripts] 57 | py4vasp = "py4vasp.cli:cli" 58 | error-analysis = "py4vasp.scripts.error_analysis:main" 59 | 60 | [tool.isort] 61 | profile = "black" 62 | 63 | 64 | [build-system] 65 | requires = ["poetry-core"] 66 | build-backend = "poetry.core.masonry.api" 67 | -------------------------------------------------------------------------------- /src/py4vasp/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | from py4vasp._analysis.mlff import MLFFErrorAnalysis 4 | from py4vasp._batch import Batch 5 | from py4vasp._calculation import Calculation, calculation 6 | from py4vasp._third_party.graph import plot 7 | from py4vasp._third_party.interactive import set_error_handling 8 | 9 | __version__ = "0.10.2" 10 | set_error_handling("Minimal") 11 | -------------------------------------------------------------------------------- /src/py4vasp/_calculation/born_effective_charge.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | from py4vasp._calculation import base, structure 4 | 5 | 6 | class BornEffectiveCharge(base.Refinery, structure.Mixin): 7 | """The Born effective charge tensors couple electric field and atomic displacement. 8 | 9 | You can use this class to extract the Born effective charges of a linear 10 | response calculation. The Born effective charges describes the effective charge of 11 | an ion in a crystal lattice when subjected to an external electric field. 12 | These charges account for the displacement of the ion positions in response to the 13 | field, reflecting the distortion of the crystal structure. Born effective charges 14 | help understanding the material's response to external stimuli, such as 15 | piezoelectric and ferroelectric behavior. 16 | """ 17 | 18 | @base.data_access 19 | def __str__(self): 20 | data = self.to_dict() 21 | result = """ 22 | BORN EFFECTIVE CHARGES (including local field effects) (in |e|, cumulative output) 23 | --------------------------------------------------------------------------------- 24 | """.strip() 25 | generator = zip(data["structure"]["elements"], data["charge_tensors"]) 26 | vec_to_string = lambda vec: " ".join(f"{x:11.5f}" for x in vec) 27 | for ion, (element, charge_tensor) in enumerate(generator): 28 | result += f""" 29 | ion {ion + 1:4d} {element} 30 | 1 {vec_to_string(charge_tensor[0])} 31 | 2 {vec_to_string(charge_tensor[1])} 32 | 3 {vec_to_string(charge_tensor[2])}""" 33 | return result 34 | 35 | @base.data_access 36 | def to_dict(self): 37 | """Read structure information and Born effective charges into a dictionary. 38 | 39 | The structural information is added to inform about which atoms are included 40 | in the array. The Born effective charges array contains the mixed second 41 | derivative with respect to an electric field and an atomic displacement for 42 | all atoms and possible directions. 43 | 44 | Returns 45 | ------- 46 | dict 47 | Contains structural information as well as the Born effective charges. 48 | """ 49 | return { 50 | "structure": self._structure.read(), 51 | "charge_tensors": self._raw_data.charge_tensors[:], 52 | } 53 | -------------------------------------------------------------------------------- /src/py4vasp/_calculation/dielectric_tensor.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | from py4vasp import exception 4 | from py4vasp._calculation import base 5 | from py4vasp._util import convert 6 | 7 | 8 | class DielectricTensor(base.Refinery): 9 | """The dielectric tensor is the static limit of the :attr:`dielectric function`. 10 | 11 | The dielectric tensor represents how a material's response to an external electric 12 | field varies with direction. It is a symmetric 3x3 matrix, encapsulating the 13 | anisotropic nature of a material's dielectric properties. Each element of the 14 | tensor corresponds to the dielectric function along a specific crystallographic 15 | axis.""" 16 | 17 | @base.data_access 18 | def to_dict(self): 19 | """Read the dielectric tensor into a dictionary. 20 | 21 | Returns 22 | ------- 23 | dict 24 | Contains the dielectric tensor and a string describing the method it 25 | was obtained. 26 | """ 27 | return { 28 | "clamped_ion": self._raw_data.electron[:], 29 | "relaxed_ion": self._read_relaxed_ion(), 30 | "independent_particle": self._read_independent_particle(), 31 | "method": convert.text_to_string(self._raw_data.method), 32 | } 33 | 34 | @base.data_access 35 | def __str__(self): 36 | data = self.to_dict() 37 | return f""" 38 | Macroscopic static dielectric tensor (dimensionless) 39 | {_description(data["method"])} 40 | ------------------------------------------------------ 41 | {_dielectric_tensor_string(data["clamped_ion"], "clamped-ion")} 42 | {_dielectric_tensor_string(data["relaxed_ion"], "relaxed-ion")} 43 | """.strip() 44 | 45 | def _read_relaxed_ion(self): 46 | if self._raw_data.ion.is_none(): 47 | return None 48 | else: 49 | return self._raw_data.electron[:] + self._raw_data.ion[:] 50 | 51 | def _read_independent_particle(self): 52 | if self._raw_data.independent_particle.is_none(): 53 | return None 54 | else: 55 | return self._raw_data.independent_particle[:] 56 | 57 | 58 | def _dielectric_tensor_string(tensor, label): 59 | if tensor is None: 60 | return "" 61 | row_to_string = lambda row: 6 * " " + " ".join(f"{x:12.6f}" for x in row) 62 | rows = (row_to_string(row) for row in tensor) 63 | return f"{label:^55}".rstrip() + "\n" + "\n".join(rows) 64 | 65 | 66 | def _description(method): 67 | if method == "dft": 68 | return "including local field effects in DFT" 69 | elif method == "rpa": 70 | return "including local field effects in RPA (Hartree)" 71 | elif method == "scf": 72 | return "including local field effects" 73 | elif method == "nscf": 74 | return "excluding local field effects" 75 | message = f"The method {method} is not implemented in this version of py4vasp." 76 | raise exception.NotImplemented(message) 77 | -------------------------------------------------------------------------------- /src/py4vasp/_calculation/elastic_modulus.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import numpy as np 4 | 5 | from py4vasp._calculation import base 6 | 7 | 8 | class ElasticModulus(base.Refinery): 9 | """The elastic modulus is the second derivative of the energy with respect to strain. 10 | 11 | The elastic modulus, also known as the modulus of elasticity, is a measure of a 12 | material's stiffness and its ability to deform elastically in response to an 13 | applied force. It quantifies the ratio of stress (force per unit area) to strain 14 | (deformation) in a material within its elastic limit. You can use this class to 15 | extract the elastic modulus of a linear response calculation. There are two 16 | variants of the elastic modulus: (i) in the clamped-ion one, the cell is deformed 17 | but the ions are kept in their positions; (ii) in the relaxed-ion one the 18 | atoms are allowed to relax when the cell is deformed. 19 | """ 20 | 21 | @base.data_access 22 | def to_dict(self): 23 | """Read the clamped-ion and relaxed-ion elastic modulus into a dictionary. 24 | 25 | Returns 26 | ------- 27 | dict 28 | Contains the level of approximation and its associated elastic modulus. 29 | """ 30 | return { 31 | "clamped_ion": self._raw_data.clamped_ion[:], 32 | "relaxed_ion": self._raw_data.relaxed_ion[:], 33 | } 34 | 35 | @base.data_access 36 | def __str__(self): 37 | return f"""Elastic modulus (kBar) 38 | Direction XX YY ZZ XY YZ ZX 39 | -------------------------------------------------------------------------------- 40 | {_elastic_modulus_string(self._raw_data.clamped_ion[:], "clamped-ion")} 41 | {_elastic_modulus_string(self._raw_data.relaxed_ion[:], "relaxed-ion")}""" 42 | 43 | 44 | def _elastic_modulus_string(tensor, label): 45 | compact_tensor = _compact(_compact(tensor).T).T 46 | line = lambda dir_, vec: dir_ + 6 * " " + " ".join(f"{x:11.4f}" for x in vec) 47 | directions = ("XX", "YY", "ZZ", "XY", "YZ", "ZX") 48 | lines = (line(dir_, vec) for dir_, vec in zip(directions, compact_tensor)) 49 | return f"{label:^79}".rstrip() + "\n" + "\n".join(lines) 50 | 51 | 52 | def _compact(tensor): 53 | x, y, z = range(3) 54 | symmetrized = ( 55 | tensor[x, x], 56 | tensor[y, y], 57 | tensor[z, z], 58 | 0.5 * (tensor[x, y] + tensor[y, x]), 59 | 0.5 * (tensor[y, z] + tensor[z, y]), 60 | 0.5 * (tensor[z, x] + tensor[x, z]), 61 | ) 62 | return np.array(symmetrized) 63 | -------------------------------------------------------------------------------- /src/py4vasp/_calculation/exciton_eigenvector.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | from py4vasp._calculation import _dispersion, base 4 | from py4vasp._util import convert 5 | 6 | 7 | class ExcitonEigenvector(base.Refinery): 8 | """BSE can compute excitonic properties of materials. 9 | 10 | The Bethe-Salpeter Equation (BSE) accounts for electron-hole interactions 11 | involved in excitonic processes. For systems, where excitonic excitations 12 | matter the BSE method is an important tool. One can visualize excitonic 13 | contributions as so-called "fatbands" plots. Here, the width of the band is 14 | adjusted such that the band is wider the larger the contribution is. This 15 | approach helps understanding of the electronic transitions and excitonic 16 | behavior in materials. 17 | """ 18 | 19 | @base.data_access 20 | def __str__(self): 21 | shape = self._raw_data.bse_index.shape 22 | return f"""BSE eigenvector data: 23 | {shape[1]} k-points 24 | {shape[3]} valence bands 25 | {shape[2]} conduction bands""" 26 | 27 | @base.data_access 28 | def to_dict(self): 29 | """Read the data into a dictionary. 30 | 31 | Returns 32 | ------- 33 | dict 34 | The dictionary contains the relevant k-point distances and labels as well as 35 | the electronic band eigenvalues. To produce fatband plots, use the array 36 | *bse_index* to access the relevant quantities of the BSE eigenvectors. Note 37 | that the dimensions of the bse_index array are **k** points, conduction 38 | bands, valence bands and that the conduction and valence band indices may 39 | be offset by first_valence_band and first_conduction_band, respectively. 40 | """ 41 | eigenvectors = convert.to_complex(self._raw_data.eigenvectors[:]) 42 | dispersion = self._dispersion.read() 43 | shifted_eigenvalues = ( 44 | dispersion.pop("eigenvalues") - self._raw_data.fermi_energy 45 | ) 46 | return { 47 | **dispersion, 48 | "bands": shifted_eigenvalues, 49 | "bse_index": self._raw_data.bse_index[:] - 1, 50 | "eigenvectors": eigenvectors, 51 | "fermi_energy": self._raw_data.fermi_energy, 52 | "first_valence_band": self._raw_data.first_valence_band[:] - 1, 53 | "first_conduction_band": self._raw_data.first_conduction_band[:] - 1, 54 | } 55 | 56 | @property 57 | def _dispersion(self): 58 | return _dispersion.Dispersion.from_data(self._raw_data.dispersion) 59 | -------------------------------------------------------------------------------- /src/py4vasp/_calculation/force.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import numpy as np 4 | 5 | from py4vasp import _config 6 | from py4vasp._calculation import base, slice_, structure 7 | from py4vasp._third_party import view 8 | from py4vasp._util import documentation, reader 9 | 10 | 11 | @documentation.format(examples=slice_.examples("force")) 12 | class Force(slice_.Mixin, base.Refinery, structure.Mixin, view.Mixin): 13 | """The forces determine the path of the atoms in a trajectory. 14 | 15 | You can use this class to analyze the forces acting on the atoms. The forces 16 | are the first derivative of the DFT total energy. The forces being small is 17 | an important criterion for the convergence of a relaxation calculation. The 18 | size of the forces is also related to the maximal time step in MD simulations. 19 | When you choose a too large time step, the forces become large and the atoms 20 | may move too much in a single step leading to an unstable trajectory. You can 21 | use this class to visualize the forces in a trajectory or read the values to 22 | analyze them numerically. 23 | 24 | {examples} 25 | """ 26 | 27 | force_rescale = 1.5 28 | "Scaling constant to convert forces to Å." 29 | 30 | @base.data_access 31 | def __str__(self): 32 | "Convert the forces to a format similar to the OUTCAR file." 33 | result = """ 34 | POSITION TOTAL-FORCE (eV/Angst) 35 | ----------------------------------------------------------------------------------- 36 | """.strip() 37 | step = self._last_step_in_slice 38 | position_to_string = lambda position: " ".join(f"{x:12.5f}" for x in position) 39 | positions = self._structure[step].cartesian_positions() 40 | force_to_string = lambda force: " ".join(f"{x:13.6f}" for x in force) 41 | for position, force in zip(positions, self._force[step]): 42 | result += f"\n{position_to_string(position)} {force_to_string(force)}" 43 | return result 44 | 45 | @base.data_access 46 | @documentation.format(examples=slice_.examples("force", "to_dict")) 47 | def to_dict(self): 48 | """Read the forces and associated structural information for one or more 49 | selected steps of the trajectory. 50 | 51 | Returns 52 | ------- 53 | dict 54 | Contains the forces for all selected steps and the structural information 55 | to know on which atoms the forces act. 56 | 57 | {examples} 58 | """ 59 | return { 60 | "structure": self._structure[self._steps].read(), 61 | "forces": self._force[self._steps], 62 | } 63 | 64 | @base.data_access 65 | @documentation.format(examples=slice_.examples("force", "to_view")) 66 | def to_view(self, supercell=None): 67 | """Visualize the forces showing arrows at the atoms. 68 | 69 | Parameters 70 | ---------- 71 | supercell : int or np.ndarray 72 | If present the structure is replicated the specified number of times 73 | along each direction. 74 | 75 | Returns 76 | ------- 77 | View 78 | Shows the structure with cell and all atoms adding arrows to the atoms 79 | sized according to the strength of the force. 80 | 81 | {examples} 82 | """ 83 | viewer = self._structure.plot(supercell) 84 | forces = self.force_rescale * self._force[self._steps] 85 | if forces.ndim == 2: 86 | forces = forces[np.newaxis] 87 | ion_arrow = view.IonArrow( 88 | quantity=forces, 89 | label="forces", 90 | color=_config.VASP_COLORS["purple"], 91 | radius=0.2, 92 | ) 93 | viewer.ion_arrows = [ion_arrow] 94 | return viewer 95 | 96 | @property 97 | def _force(self): 98 | return _ForceReader(self._raw_data.forces) 99 | 100 | 101 | class _ForceReader(reader.Reader): 102 | def error_message(self, key, err): 103 | key = np.array(key) 104 | steps = key if key.ndim == 0 else key[0] 105 | return ( 106 | f"Error reading the forces. Please check if the steps " 107 | f"`{steps}` are properly formatted and within the boundaries. " 108 | "Additionally, you may consider the original error message:\n" + err.args[0] 109 | ) 110 | -------------------------------------------------------------------------------- /src/py4vasp/_calculation/internal_strain.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | from py4vasp._calculation import base, structure 4 | 5 | 6 | class InternalStrain(base.Refinery, structure.Mixin): 7 | """The internal strain is the derivative of energy with respect to displacement and strain. 8 | 9 | The internal strain tensor characterizes the deformation within a material at 10 | a microscopic level. It is a symmetric 3 x 3 matrix per displacement and 11 | describes the coupling between the displacement of atoms and the strain on 12 | the system. Specifically, it reveals how atoms would move under strain or which 13 | stress occurs when the atoms are displaced. VASP computes the internal strain 14 | with linear response and this class provides access to the resulting data. 15 | """ 16 | 17 | @base.data_access 18 | def __str__(self): 19 | result = """ 20 | Internal strain tensor (eV/Å): 21 | ion displ X Y Z XY YZ ZX 22 | --------------------------------------------------------------------------------- 23 | """ 24 | for ion, tensor in enumerate(self._raw_data.internal_strain): 25 | ion_string = f"{ion + 1:4d}" 26 | for displacement, matrix in zip("xyz", tensor): 27 | result += _add_matrix_string(ion_string, displacement, matrix) 28 | ion_string = " " 29 | return result.strip() 30 | 31 | @base.data_access 32 | def to_dict(self): 33 | """Read the internal strain to a dictionary. 34 | 35 | Returns 36 | ------- 37 | dict 38 | The dictionary contains the structure of the system. As well as the internal 39 | strain tensor for all ions. The internal strain is the derivative of the 40 | energy with respect to ionic position and strain of the cell. 41 | """ 42 | return { 43 | "structure": self._structure.read(), 44 | "internal_strain": self._raw_data.internal_strain[:], 45 | } 46 | 47 | 48 | def _add_matrix_string(ion_string, displacement, matrix): 49 | x, y, z = range(3) 50 | symmetrized_matrix = ( 51 | matrix[x, x], 52 | matrix[y, y], 53 | matrix[z, z], 54 | 0.5 * (matrix[x, y] + matrix[y, x]), 55 | 0.5 * (matrix[y, z] + matrix[z, y]), 56 | 0.5 * (matrix[z, x] + matrix[x, z]), 57 | ) 58 | matrix_string = " ".join(f"{x:11.5f}" for x in symmetrized_matrix) 59 | return f"{ion_string} {displacement} {matrix_string}" + "\n" 60 | -------------------------------------------------------------------------------- /src/py4vasp/_calculation/phonon.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | from py4vasp._calculation import _stoichiometry, base 4 | from py4vasp._util import select 5 | 6 | selection_doc = """\ 7 | selection : str 8 | A string specifying the projection of the phonon modes onto atoms and directions. 9 | Please specify selections using one of the following: 10 | 11 | - To specify the **atom**, you can either use its element name (Si, Al, ...) 12 | or its index as given in the input file (1, 2, ...). For the latter 13 | option it is also possible to specify ranges (e.g. 1:4). 14 | - To select a particular **direction** specify the Cartesian direction (x, y, z). 15 | 16 | You separate multiple selections by commas or whitespace and can nest them using 17 | parenthesis, e.g. `Sr(x)` or `z(1, 2)`. The order of the selections does not matter, 18 | but it is case sensitive to distinguish y (Cartesian direction) from Y (yttrium). 19 | You can also add or subtract different selections e.g. `Sr - Ti`. 20 | 21 | If you are unsure what selections exist, please use the `selections` routine which 22 | will return all possibilities. 23 | """ 24 | 25 | 26 | class Mixin: 27 | "Provide functionality common to Phonon classes." 28 | 29 | @base.data_access 30 | def selections(self): 31 | "Return a dictionary specifying which atoms and directions can be used as selection." 32 | atoms = self._init_atom_dict().keys() 33 | return { 34 | "atom": sorted(atoms, key=self._sort_key), 35 | "direction": ["x", "y", "z"], 36 | } 37 | 38 | def _stoichiometry(self): 39 | return _stoichiometry.Stoichiometry.from_data(self._raw_data.stoichiometry) 40 | 41 | def _init_atom_dict(self): 42 | return { 43 | key: value.indices 44 | for key, value in self._stoichiometry().read().items() 45 | if key != select.all 46 | } 47 | 48 | def _init_direction_dict(self): 49 | return { 50 | "x": slice(0, 1), 51 | "y": slice(1, 2), 52 | "z": slice(2, 3), 53 | } 54 | 55 | def _sort_key(self, key): 56 | return key.isdecimal() 57 | -------------------------------------------------------------------------------- /src/py4vasp/_calculation/phonon_band.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import numpy as np 4 | 5 | from py4vasp._calculation import _dispersion, base, phonon 6 | from py4vasp._third_party import graph 7 | from py4vasp._util import convert, documentation, index, select 8 | 9 | 10 | class PhononBand(phonon.Mixin, base.Refinery, graph.Mixin): 11 | """The phonon band structure contains the **q**-resolved phonon eigenvalues. 12 | 13 | The phonon band structure is a graphical representation of the phonons. It 14 | illustrates the relationship between the frequency of modes and their corresponding 15 | wave vectors in the Brillouin zone. Each line or branch in the band structure 16 | represents a specific phonon, and the slope of these branches provides information 17 | about their velocity. 18 | 19 | The phonon band structure includes the dispersion relations of phonons, which reveal 20 | how vibrational frequencies vary with direction in the crystal lattice. The presence 21 | of band gaps or band crossings indicates the material's ability to conduct or 22 | insulate heat. Additionally, the branches near the high-symmetry points in the 23 | Brillouin zone offer insights into the material's anharmonicity and thermal 24 | conductivity. Furthermore, phonons with imaginary frequencies indicate the presence 25 | of a structural instability. 26 | """ 27 | 28 | @base.data_access 29 | def __str__(self): 30 | return f"""phonon band data: 31 | {self._raw_data.dispersion.eigenvalues.shape[0]} q-points 32 | {self._raw_data.dispersion.eigenvalues.shape[1]} modes 33 | {self._stoichiometry()}""" 34 | 35 | @base.data_access 36 | def to_dict(self): 37 | """Read the phonon band structure into a dictionary. 38 | 39 | Returns 40 | ------- 41 | dict 42 | Contains the **q**-point path for plotting phonon band structures and 43 | the phonon bands. In addition the phonon modes are returned. 44 | """ 45 | dispersion = self._dispersion().read() 46 | return { 47 | "qpoint_distances": dispersion["kpoint_distances"], 48 | "qpoint_labels": dispersion["kpoint_labels"], 49 | "bands": dispersion["eigenvalues"], 50 | "modes": self._modes(), 51 | } 52 | 53 | @base.data_access 54 | @documentation.format(selection=phonon.selection_doc) 55 | def to_graph(self, selection=None, width=1.0): 56 | """Generate a graph of the phonon bands. 57 | 58 | Parameters 59 | ---------- 60 | {selection} 61 | width : float 62 | Specifies the width illustrating the projections. 63 | 64 | Returns 65 | ------- 66 | Graph 67 | Contains the phonon band structure for all the **q** points. If a 68 | selection is provided, the width of the bands is adjusted according to 69 | the projection. 70 | """ 71 | projections = self._projections(selection, width) 72 | graph = self._dispersion().plot(projections) 73 | graph.ylabel = "ω (THz)" 74 | return graph 75 | 76 | def _dispersion(self): 77 | return _dispersion.Dispersion.from_data(self._raw_data.dispersion) 78 | 79 | def _modes(self): 80 | return convert.to_complex(self._raw_data.eigenvectors[:]) 81 | 82 | def _projections(self, selection, width): 83 | if not selection: 84 | return None 85 | maps = {2: self._init_atom_dict(), 3: self._init_direction_dict()} 86 | selector = index.Selector(maps, np.abs(self._modes()), use_number_labels=True) 87 | tree = select.Tree.from_selection(selection) 88 | return { 89 | selector.label(selection): width * selector[selection] 90 | for selection in tree.selections() 91 | } 92 | -------------------------------------------------------------------------------- /src/py4vasp/_calculation/phonon_dos.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | from py4vasp._calculation import base, phonon 4 | from py4vasp._third_party import graph 5 | from py4vasp._util import documentation, index, select 6 | 7 | 8 | class PhononDos(phonon.Mixin, base.Refinery, graph.Mixin): 9 | """The phonon density of states (DOS) describes the number of modes per energy. 10 | 11 | The phonon density of states (DOS) is a representation of the distribution of 12 | phonons in a material across different frequencies. It provides a histogram of the 13 | number of phonon states per frequency interval. Peaks and features in the DOS reveal 14 | the density of vibrational modes at specific frequencies. One can related these 15 | properties to study e.g. the heat capacity or the thermal conductivity. 16 | 17 | Projecting the phonon density of states (DOS) onto specific atoms highlights the 18 | contribution of each atomic species to the vibrational spectrum. This analysis helps 19 | to understand the role of individual elements' impact on the material's thermal and 20 | mechanical properties. The projected phonon DOS can guide towards engineering these 21 | properties by substitution of specific atoms. Additionally, the atom-specific 22 | projection allows for the identification of localized modes or vibrations associated 23 | with specific atomic species. 24 | """ 25 | 26 | @base.data_access 27 | def __str__(self): 28 | energies = self._raw_data.energies 29 | stoichiometry = self._stoichiometry() 30 | return f"""phonon DOS: 31 | [{energies[0]:0.2f}, {energies[-1]:0.2f}] mesh with {len(energies)} points 32 | {3 * stoichiometry.number_atoms()} modes 33 | {stoichiometry}""" 34 | 35 | @base.data_access 36 | @documentation.format(selection=phonon.selection_doc) 37 | def to_dict(self, selection=None): 38 | """Read the phonon DOS into a dictionary. 39 | 40 | Parameters 41 | ---------- 42 | {selection} 43 | 44 | Returns 45 | ------- 46 | dict 47 | Contains the energies at which the phonon DOS was computed. The total 48 | DOS is returned and any possible projected DOS selected by the *selection* 49 | argument. 50 | """ 51 | return { 52 | "energies": self._raw_data.energies[:], 53 | "total": self._raw_data.dos[:], 54 | **self._read_data(selection), 55 | } 56 | 57 | @base.data_access 58 | @documentation.format(selection=phonon.selection_doc) 59 | def to_graph(self, selection=None): 60 | """Generate a graph of the selected DOS. 61 | 62 | Parameters 63 | ---------- 64 | {selection} 65 | 66 | Returns 67 | ------- 68 | Graph 69 | The graph contains the total DOS. If a selection is given, in addition the 70 | projected DOS is shown.""" 71 | data = self.to_dict(selection) 72 | return graph.Graph( 73 | series=list(_series(data)), 74 | xlabel="ω (THz)", 75 | ylabel="DOS (1/THz)", 76 | ) 77 | 78 | def _read_data(self, selection): 79 | if not selection: 80 | return {} 81 | maps = {0: self._init_atom_dict(), 1: self._init_direction_dict()} 82 | selector = index.Selector( 83 | maps, self._raw_data.projections, use_number_labels=True 84 | ) 85 | tree = select.Tree.from_selection(selection) 86 | return { 87 | selector.label(selection): selector[selection] 88 | for selection in tree.selections() 89 | } 90 | 91 | 92 | def _series(data): 93 | energies = data["energies"] 94 | for name, dos in data.items(): 95 | if name == "energies": 96 | continue 97 | yield graph.Series(energies, dos, name) 98 | -------------------------------------------------------------------------------- /src/py4vasp/_calculation/phonon_mode.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import numpy as np 4 | 5 | from py4vasp._calculation import base, structure 6 | from py4vasp._util import convert 7 | 8 | 9 | class PhononMode(base.Refinery, structure.Mixin): 10 | """Describes a collective vibration of atoms in a crystal. 11 | 12 | A phonon mode represents a specific way in which atoms in a solid oscillate 13 | around their equilibrium positions. Each mode is characterized by a frequency 14 | and a displacement pattern that shows how atoms move relative to each other. 15 | Low-frequency modes correspond to long-wavelength vibrations, while 16 | high-frequency modes involve more localized atomic motion.""" 17 | 18 | @base.data_access 19 | def __str__(self): 20 | phonon_frequencies = "\n".join( 21 | self._frequency_to_string(index, frequency) 22 | for index, frequency in enumerate(self.frequencies()) 23 | ) 24 | return f"""\ 25 | Eigenvalues of the dynamical matrix 26 | ----------------------------------- 27 | {phonon_frequencies} 28 | """ 29 | 30 | def _frequency_to_string(self, index, frequency): 31 | if frequency.real >= frequency.imag: 32 | label = f"{index + 1:4} f " 33 | else: 34 | label = f"{index + 1:4} f/i" 35 | frequency = np.abs(frequency) 36 | freq_meV = f"{frequency * 1000:12.6f} meV" 37 | eV_to_THz = 241.798934781 38 | freq_THz = f"{frequency * eV_to_THz:11.6f} THz" 39 | freq_2PiTHz = f"{2 * np.pi * frequency * eV_to_THz:12.6f} 2PiTHz" 40 | eV_to_cm1 = 8065.610420 41 | freq_cm1 = f"{frequency * eV_to_cm1:12.6f} cm-1" 42 | return f"{label}= {freq_THz} {freq_2PiTHz}{freq_cm1} {freq_meV}" 43 | 44 | @base.data_access 45 | def to_dict(self): 46 | """Read structure data and properties of the phonon mode into a dictionary. 47 | 48 | The frequency and eigenvector describe with how atoms move under the influence 49 | of a particular phonon mode. Structural information is added to understand 50 | what the displacement correspond to. 51 | 52 | Returns 53 | ------- 54 | dict 55 | Structural information, phonon frequencies and eigenvectors. 56 | """ 57 | return { 58 | "structure": self._structure.read(), 59 | "frequencies": self.frequencies(), 60 | "eigenvectors": self._raw_data.eigenvectors[:], 61 | } 62 | 63 | @base.data_access 64 | def frequencies(self): 65 | "Read the phonon frequencies as a numpy array." 66 | return convert.to_complex(self._raw_data.frequencies[:]) 67 | -------------------------------------------------------------------------------- /src/py4vasp/_calculation/piezoelectric_tensor.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import numpy as np 4 | 5 | from py4vasp._calculation import base 6 | 7 | 8 | class PiezoelectricTensor(base.Refinery): 9 | """The piezoelectric tensor is the derivative of the energy with respect to strain and field. 10 | 11 | The piezoelectric tensor represents the coupling between mechanical stress and 12 | electrical polarization in a material. VASP computes the piezoelectric tensor with 13 | a linear response calculation. The piezoelectric tensor is a 3x3 matrix that relates 14 | the three components of stress to the three components of polarization. 15 | Specifically, it describes how the application of mechanical stress induces an 16 | electric polarization and, conversely, how an applied electric field results in 17 | a deformation. 18 | 19 | The piezoelectric tensor helps to characterize the efficiency and anisotropy of the 20 | piezoelectric response. A large piezoelectric tensor is useful e.g. for sensors 21 | and actuators. Moreover, the tensor's symmetry properties are coupled to the crystal 22 | structure and symmetry. Therefore a mismatch of the symmetry properties between 23 | calculations and experiment can reveal underlying flaws in the characterization of 24 | the crystal structure. 25 | """ 26 | 27 | @base.data_access 28 | def __str__(self): 29 | data = self.to_dict() 30 | return f"""Piezoelectric tensor (C/m²) 31 | XX YY ZZ XY YZ ZX 32 | --------------------------------------------------------------------------- 33 | {_tensor_to_string(data["clamped_ion"], "clamped-ion")} 34 | {_tensor_to_string(data["relaxed_ion"], "relaxed-ion")}""" 35 | 36 | @base.data_access 37 | def to_dict(self): 38 | """Read the ionic and electronic contribution to the piezoelectric tensor 39 | into a dictionary. 40 | 41 | It will combine both terms as the total piezoelectric tensor (relaxed_ion) 42 | but also give the pure electronic contribution, so that you can separate the 43 | parts. 44 | 45 | Returns 46 | ------- 47 | dict 48 | The clamped ion and relaxed ion data for the piezoelectric tensor. 49 | """ 50 | electron_data = self._raw_data.electron[:] 51 | return { 52 | "clamped_ion": electron_data, 53 | "relaxed_ion": electron_data + self._raw_data.ion[:], 54 | } 55 | 56 | 57 | def _tensor_to_string(tensor, label): 58 | compact_tensor = _compact(tensor.T).T 59 | line = lambda dir_, vec: dir_ + " " + " ".join(f"{x:11.5f}" for x in vec) 60 | directions = (" x", " y", " z") 61 | lines = (line(dir_, vec) for dir_, vec in zip(directions, compact_tensor)) 62 | return f"{label:^75}".rstrip() + "\n" + "\n".join(lines) 63 | 64 | 65 | def _compact(tensor): 66 | x, y, z = range(3) 67 | symmetrized = ( 68 | tensor[x, x], 69 | tensor[y, y], 70 | tensor[z, z], 71 | 0.5 * (tensor[x, y] + tensor[y, x]), 72 | 0.5 * (tensor[y, z] + tensor[z, y]), 73 | 0.5 * (tensor[z, x] + tensor[x, z]), 74 | ) 75 | return np.array(symmetrized) 76 | -------------------------------------------------------------------------------- /src/py4vasp/_calculation/polarization.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | from py4vasp._calculation import base 4 | 5 | 6 | class Polarization(base.Refinery): 7 | """The static polarization describes the electric dipole moment per unit volume. 8 | 9 | Static polarization arises in a material in response to a constant external electric 10 | field. In VASP, we compute the linear response of the system when applying a 11 | :tag:`EFIELD`. Static polarization is a key characteristic of ferroelectric 12 | materials that exhibit a spontaneous electric polarization that persists even in 13 | the absence of an external electric field. 14 | 15 | Note that the polarization is only well defined relative to a reference 16 | system. The absolute value can change by a polarization quantum if some 17 | charge or ion leaves one side of the unit cell and reenters at the opposite 18 | side. Therefore you always need to compare changes of polarization. 19 | """ 20 | 21 | @base.data_access 22 | def __str__(self): 23 | vec_to_string = lambda vec: " ".join(f"{x:11.5f}" for x in vec) 24 | return f""" 25 | Polarization (|e|Å) 26 | ------------------------------------------------------------- 27 | ionic dipole moment: {vec_to_string(self._raw_data.ion[:])} 28 | electronic dipole moment: {vec_to_string(self._raw_data.electron[:])} 29 | """.strip() 30 | 31 | @base.data_access 32 | def to_dict(self): 33 | """Read electronic and ionic polarization into a dictionary 34 | 35 | Returns 36 | ------- 37 | dict 38 | Contains the electronic and ionic dipole moments. 39 | """ 40 | return { 41 | "electron_dipole": self._raw_data.electron[:], 42 | "ion_dipole": self._raw_data.ion[:], 43 | } 44 | -------------------------------------------------------------------------------- /src/py4vasp/_calculation/selection.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | from __future__ import annotations 4 | 5 | from dataclasses import dataclass 6 | 7 | 8 | @dataclass 9 | class Selection: 10 | "Helper class specifying which indices to extract their label." 11 | 12 | indices: Iterable[int] 13 | "Indices from which the specified quantity is read." 14 | label: str = "" 15 | "Label identifying the quantity." 16 | -------------------------------------------------------------------------------- /src/py4vasp/_calculation/slice_.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import copy 4 | 5 | from py4vasp import exception 6 | 7 | 8 | def examples(instance_name, function_name=None, step="step"): 9 | if function_name is None: 10 | function_name = "read" 11 | access = "a method of this class" 12 | depend_on = f"the {step}s" 13 | else: 14 | access = "this method" 15 | depend_on = f"the {step}s of the class" 16 | return f""" 17 | Examples 18 | -------- 19 | If you access {access}, the result will depend on {depend_on} that 20 | you selected with the [] operator. Without any selection the results from the 21 | final {step} will be used. 22 | 23 | >>> calculation.{instance_name}.{function_name}() 24 | 25 | To select the results for all {step}s, you don't specify the array boundaries. 26 | 27 | >>> calculation.{instance_name}[:].{function_name}() 28 | 29 | You can also select specific {step}s or a subset of {step}s as follows 30 | 31 | >>> calculation.{instance_name}[5].{function_name}() 32 | >>> calculation.{instance_name}[1:6].{function_name}()""".strip() 33 | 34 | 35 | class Mixin: 36 | def __init__(self, *args, **kwargs): 37 | super().__init__(*args, **kwargs) 38 | self._set_steps_and_slice(-1) 39 | self._original = True 40 | 41 | def __getitem__(self, steps): 42 | self._raise_error_if_not_original() 43 | new = copy.copy(self) 44 | new._original = False 45 | return new._set_steps_and_slice(steps) 46 | 47 | def _set_steps_and_slice(self, steps): 48 | self._steps = steps 49 | self._is_slice = isinstance(steps, slice) 50 | if self._is_slice: 51 | self._slice = steps 52 | elif steps == -1: 53 | self._slice = slice(-1, None) 54 | else: 55 | self._slice = _create_slice_for_current_step_if_possible(steps) 56 | return self 57 | 58 | @property 59 | def _last_step_in_slice(self): 60 | return (self._slice.stop or 0) - 1 61 | 62 | def _raise_error_if_not_original(self): 63 | if not self._original: 64 | message = "Taking nested slices is not implemented. Please derive all slices from the original Refinery." 65 | raise exception.NotImplemented(message) 66 | 67 | 68 | def _create_slice_for_current_step_if_possible(steps): 69 | try: 70 | return slice(steps, steps + 1) 71 | except TypeError as error: 72 | message = f"Error creating slice [{steps}:{steps} + 1], please check the access operator argument." 73 | raise exception.IncorrectUsage(message) from error 74 | -------------------------------------------------------------------------------- /src/py4vasp/_calculation/stress.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import numpy as np 4 | 5 | from py4vasp._calculation import base, slice_, structure 6 | from py4vasp._util import documentation, reader 7 | 8 | 9 | @documentation.format(examples=slice_.examples("stress")) 10 | class Stress(slice_.Mixin, base.Refinery, structure.Mixin): 11 | """The stress describes the force acting on the shape of the unit cell. 12 | 13 | The stress refers to the force applied to the cell per unit area. Specifically, 14 | VASP computes the stress for a given unit cell and relaxing to vanishing stress 15 | determines the predicted ground-state cell. The stress is 3 x 3 matrix; the trace 16 | indicates changes to the volume and the rest of the matrix changes the shape of 17 | the cell. You can impose an external stress with the tag :tag:`PSTRESS`. 18 | 19 | When you relax the system or in a MD simulation, VASP computes and stores the 20 | stress in every iteration. You can use this class to read the stress for specific 21 | steps along the trajectory. 22 | 23 | {examples} 24 | """ 25 | 26 | @base.data_access 27 | def __str__(self): 28 | "Convert the stress to a format similar to the OUTCAR file." 29 | step = self._last_step_in_slice 30 | eV_to_kB = 1.602176634e3 / self._structure[step].volume() 31 | stress = _symmetry_reduce(self._stress[step]) 32 | stress_to_string = lambda stress: " ".join(f"{x:11.5f}" for x in stress) 33 | return f""" 34 | FORCE on cell =-STRESS in cart. coord. units (eV): 35 | Direction XX YY ZZ XY YZ ZX 36 | ------------------------------------------------------------------------------------- 37 | Total {stress_to_string(stress / eV_to_kB)} 38 | in kB {stress_to_string(stress)} 39 | """.strip() 40 | 41 | @base.data_access 42 | @documentation.format(examples=slice_.examples("stress", "to_dict")) 43 | def to_dict(self): 44 | """Read the stress and associated structural information for one or more 45 | selected steps of the trajectory. 46 | 47 | Returns 48 | ------- 49 | dict 50 | Contains the stress for all selected steps and the structural information 51 | to know on which cell the stress acts. 52 | 53 | {examples} 54 | """ 55 | return { 56 | "stress": self._stress[self._steps], 57 | "structure": self._structure[self._steps].read(), 58 | } 59 | 60 | @property 61 | def _stress(self): 62 | return _StressReader(self._raw_data.stress) 63 | 64 | 65 | class _StressReader(reader.Reader): 66 | def error_message(self, key, err): 67 | key = np.array(key) 68 | steps = key if key.ndim == 0 else key[0] 69 | return ( 70 | f"Error reading the stress. Please check if the steps " 71 | f"`{steps}` are properly formatted and within the boundaries. " 72 | "Additionally, you may consider the original error message:\n" + err.args[0] 73 | ) 74 | 75 | 76 | def _symmetry_reduce(stress_tensor): 77 | symmetry_reduced_tensor = [ 78 | stress_tensor[0, 0], 79 | stress_tensor[1, 1], 80 | stress_tensor[2, 2], 81 | 0.5 * (stress_tensor[0, 1] + stress_tensor[1, 0]), 82 | 0.5 * (stress_tensor[1, 2] + stress_tensor[2, 1]), 83 | 0.5 * (stress_tensor[0, 2] + stress_tensor[2, 0]), 84 | ] 85 | return np.array(symmetry_reduced_tensor) 86 | -------------------------------------------------------------------------------- /src/py4vasp/_calculation/system.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | from py4vasp._calculation import base 4 | from py4vasp._util import convert 5 | 6 | 7 | class System(base.Refinery): 8 | "The :tag:`SYSTEM` tag in the INCAR file is a title you choose for a VASP calculation." 9 | 10 | @base.data_access 11 | def __str__(self): 12 | return convert.text_to_string(self._raw_data.system) 13 | 14 | @base.data_access 15 | def to_dict(self): 16 | "Returns a dictionary containing the system tag." 17 | return {"system": str(self)} 18 | -------------------------------------------------------------------------------- /src/py4vasp/_calculation/velocity.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import numpy as np 4 | 5 | from py4vasp import _config, exception 6 | from py4vasp._calculation import base, slice_, structure 7 | from py4vasp._third_party import view 8 | from py4vasp._util import documentation, reader 9 | 10 | 11 | @documentation.format(examples=slice_.examples("velocity")) 12 | class Velocity(slice_.Mixin, base.Refinery, structure.Mixin, view.Mixin): 13 | """The velocities describe the ionic motion during an MD simulation. 14 | 15 | The velocities of the ions are a metric for the temperature of the system. Most 16 | of the time, it is not necessary to consider them explicitly. VASP will set the 17 | velocities automatically according to the temperature settings (:tag:`TEBEG` and 18 | :tag:`TEEND`) unless you set them explicitly in the POSCAR file. Since the 19 | velocities are not something you typically need, VASP will only store them during 20 | the simulation if you set :tag:`VELOCITY` = T in the INCAR file. In that case you 21 | can read the velocities of each step along the trajectory. If you are only 22 | interested in the final velocities, please consider the :data:'~py4vasp.data.CONTCAR` 23 | class. 24 | 25 | {examples} 26 | """ 27 | 28 | velocity_rescale = 200 29 | 30 | @base.data_access 31 | def __str__(self): 32 | step = self._last_step_in_slice 33 | velocities = self._vectors_to_string(self._velocity[step]) 34 | return f"{self._structure[step]}\n\n{velocities}" 35 | 36 | def _vectors_to_string(self, vectors): 37 | return "\n".join(self._vector_to_string(vector) for vector in vectors) 38 | 39 | def _vector_to_string(self, vector): 40 | return " ".join(self._element_to_string(element) for element in vector) 41 | 42 | def _element_to_string(self, element): 43 | return f"{element:21.16f}" 44 | 45 | @base.data_access 46 | @documentation.format(examples=slice_.examples("velocity", "to_dict")) 47 | def to_dict(self): 48 | """Return the structure and ion velocities in a dictionary 49 | 50 | Returns 51 | ------- 52 | dict 53 | The dictionary contains the ion velocities as well as the structural 54 | information for reference. 55 | 56 | {examples} 57 | """ 58 | return { 59 | "structure": self._structure[self._steps].read(), 60 | "velocities": self._velocity[self._steps], 61 | } 62 | 63 | @base.data_access 64 | @documentation.format(examples=slice_.examples("velocity", "to_view")) 65 | def to_view(self, supercell=None): 66 | """Plot the velocities as vectors in the structure. 67 | 68 | Parameters 69 | ---------- 70 | supercell : int or np.ndarray 71 | If present the structure is replicated the specified number of times 72 | along each direction. 73 | 74 | Returns 75 | ------- 76 | View 77 | Contains all atoms and the velocities are drawn as vectors. 78 | 79 | {examples} 80 | """ 81 | viewer = self._structure.plot(supercell) 82 | velocities = self.velocity_rescale * self._velocity[self._steps] 83 | if velocities.ndim == 2: 84 | velocities = velocities[np.newaxis] 85 | ion_arrow = view.IonArrow( 86 | quantity=velocities, 87 | label="velocities", 88 | color=_config.VASP_COLORS["gray"], 89 | radius=0.2, 90 | ) 91 | viewer.ion_arrows = [ion_arrow] 92 | return viewer 93 | 94 | @property 95 | def _velocity(self): 96 | return _VelocityReader(self._raw_data.velocities) 97 | 98 | 99 | class _VelocityReader(reader.Reader): 100 | def error_message(self, key, err): 101 | key = np.array(key) 102 | steps = key if key.ndim == 0 else key[0] 103 | return ( 104 | f"Error reading the velocities. Please check if the steps " 105 | f"`{steps}` are properly formatted and within the boundaries. " 106 | "Additionally, you may consider the original error message:\n" + err.args[0] 107 | ) 108 | -------------------------------------------------------------------------------- /src/py4vasp/_calculation/workfunction.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | from py4vasp._calculation import bandgap, base 4 | from py4vasp._third_party import graph 5 | 6 | 7 | class Workfunction(base.Refinery, graph.Mixin): 8 | """The workfunction describes the energy required to remove an electron to the vacuum. 9 | 10 | The workfunction of a material is the minimum energy required to remove an 11 | electron from its most loosely bound state and move it to an energy level just 12 | outside the material's surface. In other words, it represents the energy barrier 13 | that electrons must overcome to escape the material. The workfunction helps 14 | understanding electronic emission phenomena in surface science and materials 15 | engineering. In VASP, you can compute the workfunction by setting the :tag:`IDIPOL` 16 | flag in the INCAR file. This class provides then the functionality to analyze the 17 | resulting potential. 18 | """ 19 | 20 | @base.data_access 21 | def __str__(self): 22 | data = self.to_dict() 23 | return f"""workfunction along {data["direction"]}: 24 | vacuum potential: {data["vacuum_potential"][0]:.3f} {data["vacuum_potential"][1]:.3f} 25 | Fermi energy: {data["fermi_energy"]:.3f} 26 | valence band maximum: {data["valence_band_maximum"]:.3f} 27 | conduction band minimum: {data["conduction_band_minimum"]:.3f}""" 28 | 29 | @base.data_access 30 | def to_dict(self): 31 | """Reports useful information about the workfunction as a dictionary. 32 | 33 | In addition to the vacuum potential, the dictionary contains typical reference 34 | energies such as the valence band maximum, the conduction band minimum, and the 35 | Fermi energy. Furthermore you obtain the average potential, so you can use a 36 | different algorithm to determine the vacuum potential if desired. 37 | 38 | Returns 39 | ------- 40 | dict 41 | Contains vacuum potential, average potential and relevant reference energies 42 | within the surface. 43 | """ 44 | gap = bandgap.Bandgap.from_data(self._raw_data.reference_potential) 45 | return { 46 | "direction": f"lattice vector {self._raw_data.idipol}", 47 | "distance": self._raw_data.distance[:], 48 | "average_potential": self._raw_data.average_potential[:], 49 | "vacuum_potential": self._raw_data.vacuum_potential[:], 50 | "valence_band_maximum": gap.valence_band_maximum(), 51 | "conduction_band_minimum": gap.conduction_band_minimum(), 52 | "fermi_energy": self._raw_data.fermi_energy, 53 | } 54 | 55 | @base.data_access 56 | def to_graph(self): 57 | """Plot the average potential along the lattice vector selected by IDIPOL. 58 | 59 | Returns 60 | ------- 61 | Graph 62 | A plot where the distance in the unit cell along the selected lattice vector 63 | is on the x axis and the averaged potential across the plane of the other 64 | two lattice vectors is on the y axis. 65 | """ 66 | data = self.to_dict() 67 | series = graph.Series(data["distance"], data["average_potential"], "potential") 68 | return graph.Graph( 69 | series=series, 70 | xlabel=f"distance along {data['direction']} (Å)", 71 | ylabel="average potential (eV)", 72 | ) 73 | -------------------------------------------------------------------------------- /src/py4vasp/_combine/base.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import inspect 4 | import pathlib 5 | from typing import Dict, List 6 | 7 | import py4vasp 8 | from py4vasp import exception 9 | from py4vasp._util import convert 10 | 11 | 12 | def _match_combine_with_refinement(combine_name: str): 13 | combine_to_refinement_name = { 14 | "Energies": "energy", 15 | "Forces": "force", 16 | "Stresses": "stress", 17 | } 18 | quantity = combine_to_refinement_name[combine_name] 19 | module = getattr(py4vasp._calculation, quantity) 20 | class_name = convert.to_camelcase(quantity) 21 | return getattr(module, class_name) 22 | # for _, class_ in inspect.getmembers(data_depr, inspect.isclass): 23 | # if class_.__name__ == combine_to_refinement_name[combine_name]: 24 | # return class_ 25 | # else: 26 | # raise exception.IncorrectUsage( 27 | # f"Could not find refinement class for {combine_name}." 28 | # ) 29 | 30 | 31 | class BaseCombine: 32 | """A class to handle multiple refinements all at once. 33 | 34 | This class combines the functionality of the refinement class for more than one 35 | refinement. Create a BaseCombine object using either a wildcard for a set of 36 | paths or files or pass in paths and files directly. Then you can access the 37 | properties of all refinements via the attributes of the object. 38 | 39 | Notes 40 | ----- 41 | To create new instances, you should use the classmethod :meth:`from_paths` or 42 | :meth:`from_files`. This will ensure that the paths to your VASP calculations are 43 | properly set and all features work as intended. Note that this is an alpha version 44 | and the API might change in the future. 45 | """ 46 | 47 | def __init__(self): 48 | pass 49 | 50 | @classmethod 51 | def from_paths(cls, paths: Dict[str, List[pathlib.Path]]): 52 | """Set up a BaseCombine object for paths. 53 | 54 | Setup the object for paths by passing in a dictionary with the name of the 55 | calculation as key and the path to the calculation as value. 56 | 57 | Parameters 58 | ---------- 59 | paths : Dict[str, List[pathlib.Path]] 60 | A dictionary with the name of the calculation as key and the path to the 61 | calculation as value. 62 | """ 63 | base = cls() 64 | refinement = _match_combine_with_refinement(cls.__name__) 65 | setattr(base, f"_{cls.__name__.lower()}", {}) 66 | for key, path in paths.items(): 67 | all_refinements = [refinement.from_path(_path) for _path in path] 68 | base.__getattribute__(f"_{cls.__name__.lower()}")[key] = all_refinements 69 | return base 70 | 71 | @classmethod 72 | def from_files(cls, files: Dict[str, List[pathlib.Path]]): 73 | """Set up a BaseCombine object for files. 74 | 75 | Setup the object for files by passing in a dictionary with the name of the 76 | calculation as key and the path to the calculation as value. 77 | 78 | Parameters 79 | ---------- 80 | files : Dict[str, List[pathlib.Path]] 81 | A dictionary with the name of the calculation as key and the path to the 82 | calculation as value. 83 | """ 84 | base = cls() 85 | refinement = _match_combine_with_refinement(cls.__name__) 86 | setattr(base, f"_{cls.__name__.lower()}", {}) 87 | for key, file in files.items(): 88 | all_refinements = [refinement.from_file(_file) for _file in file] 89 | base.__getattribute__(f"_{cls.__name__.lower()}")[key] = all_refinements 90 | return base 91 | 92 | def _to_dict(self, *args, **kwargs): 93 | _data = {} 94 | _class_name = f"_{self.__class__.__name__.lower()}" 95 | keyval_refinements = self.__getattribute__(_class_name).items() 96 | for key, refinement in keyval_refinements: 97 | _data[key] = [ 98 | _refinement.read(*args, **kwargs) for _refinement in refinement 99 | ] 100 | return _data 101 | 102 | def read(self, *args, **kwargs): 103 | """Read the data from the :meth:`read` method of the refinement class.""" 104 | return self._to_dict(*args, **kwargs) 105 | -------------------------------------------------------------------------------- /src/py4vasp/_combine/energies.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | 4 | from py4vasp._combine.base import BaseCombine 5 | 6 | 7 | class Energies(BaseCombine): 8 | def __init__(self): 9 | pass 10 | -------------------------------------------------------------------------------- /src/py4vasp/_combine/forces.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | 4 | from py4vasp._combine.base import BaseCombine 5 | 6 | 7 | class Forces(BaseCombine): 8 | def __init__(self): 9 | pass 10 | -------------------------------------------------------------------------------- /src/py4vasp/_combine/stresses.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | 4 | from py4vasp._combine.base import BaseCombine 5 | 6 | 7 | class Stresses(BaseCombine): 8 | def __init__(self): 9 | pass 10 | -------------------------------------------------------------------------------- /src/py4vasp/_config.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | VASP_COLORS = { 4 | "purple": "#8342A4", 5 | "cyan": "#35CABF", 6 | "blue": "#3E70EA", 7 | "red": "#A82C35", 8 | "gray": "#424242", 9 | "dark": "#202429", 10 | "green": "#89AD01", 11 | } 12 | -------------------------------------------------------------------------------- /src/py4vasp/_control/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vasp-dev/py4vasp/22e0fa935a750fca129370fc23c30e85c3f71ec3/src/py4vasp/_control/__init__.py -------------------------------------------------------------------------------- /src/py4vasp/_control/base.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | from pathlib import Path 4 | 5 | 6 | class InputFile: 7 | def __init__(self, path): 8 | self._path = path 9 | 10 | @classmethod 11 | def from_string(cls, string, path=None): 12 | """Generate the file from a given string and store it. 13 | 14 | If no path is provided, the content of the file is stored in memory otherwise 15 | it is stored in the path. 16 | 17 | Parameters 18 | ---------- 19 | string : str 20 | Content of the file. 21 | path : str or Path 22 | If provided should define where the file is stored. 23 | """ 24 | obj = cls(path) 25 | obj.write(string) 26 | return obj 27 | 28 | def print(self): 29 | "Write the contents of the file to screen." 30 | print(self) 31 | 32 | def write(self, string): 33 | "Store the given string in the file." 34 | if self._path is not None: 35 | self._write_to_file(string) 36 | else: 37 | self._write_to_memory(string) 38 | 39 | def read(self): 40 | "Return the content of the file as a string." 41 | if self._path is not None: 42 | return self._read_from_file() 43 | else: 44 | return self._read_from_memory() 45 | 46 | def _repr_pretty_(self, p, cycle): 47 | p.text(str(self)) 48 | 49 | def _write_to_file(self, string): 50 | with open(Path(self._path) / self.__class__.__name__, "w") as file: 51 | file.write(string) 52 | 53 | def _write_to_memory(self, string): 54 | self._content = string 55 | 56 | def _read_from_file(self): 57 | with open(Path(self._path) / self.__class__.__name__, "r") as file: 58 | return file.read() 59 | 60 | def _read_from_memory(self): 61 | return self._content 62 | 63 | def __str__(self): 64 | return self.read() 65 | -------------------------------------------------------------------------------- /src/py4vasp/_control/incar.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | from py4vasp._control import base 4 | 5 | 6 | class INCAR(base.InputFile): 7 | """The INCAR file defining the input parameters of a VASP calculation. 8 | 9 | Parameters 10 | ---------- 11 | path : str or Path 12 | Defines where the INCAR file is stored. If set to None, the file will be kept 13 | in memory. 14 | """ 15 | -------------------------------------------------------------------------------- /src/py4vasp/_control/kpoints.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | from py4vasp._control import base 4 | 5 | 6 | class KPOINTS(base.InputFile): 7 | """The KPOINTS file defining the **k**-point grid for the VASP calculation. 8 | 9 | Parameters 10 | ---------- 11 | path : str or Path 12 | Defines where the KPOINTS file is stored. If set to None, the file will be kept 13 | in memory. 14 | """ 15 | -------------------------------------------------------------------------------- /src/py4vasp/_control/poscar.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | from py4vasp._calculation.structure import Structure 4 | from py4vasp._control import base 5 | from py4vasp._third_party import view 6 | 7 | 8 | class POSCAR(base.InputFile, view.Mixin): 9 | """The POSCAR file defining the structure used in the VASP calculation. 10 | 11 | Parameters 12 | ---------- 13 | path : str or Path 14 | Defines where the POSCAR file is stored. If set to None, the file will be kept 15 | in memory. 16 | """ 17 | 18 | def to_view(self, supercell=None, *, elements=None): 19 | """Generate a 3d representation of the structure in the file. 20 | 21 | Parameters 22 | ---------- 23 | supercell : int or np.ndarray 24 | If present the structure is replicated the specified number of times 25 | along each direction. 26 | 27 | elements : list[str] 28 | Name of the elements in the order they appear in the POSCAR file. If the 29 | elements are specified in the POSCAR file, this argument is optional and 30 | if set it will overwrite the choice in the POSCAR file. Old POSCAR files 31 | do not specify the name of the elements; in that case this argument is 32 | required. 33 | 34 | Returns 35 | ------- 36 | View 37 | Visualize the structure as a 3d figure. 38 | """ 39 | structure = Structure.from_POSCAR(self, elements=elements) 40 | return structure.plot(supercell) 41 | -------------------------------------------------------------------------------- /src/py4vasp/_raw/data_wrapper.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import textwrap 4 | 5 | import numpy as np 6 | 7 | from py4vasp import exception 8 | 9 | 10 | class VaspData(np.lib.mixins.NDArrayOperatorsMixin): 11 | """Wraps the data produced by the VASP calculation. 12 | 13 | Instead of exposing the underlying file structure directly, the data is wrapped in 14 | this container. This allows changing the way the data is internally represented 15 | without affecting the user. In particular, the data is possibly only lazily loaded 16 | when it is actually necessary. 17 | 18 | By inheriting from NDArrayOperatorsMixin most numpy functionality except for the 19 | class attributes should work. If any other feature is needed any instance of this 20 | class can be passed into a numpy array. Please be aware that using the data in 21 | this way will access the file. If performance is an issue, make sure that this 22 | file I/O is reduced as much as possible. 23 | 24 | Parameters 25 | ---------- 26 | data 27 | The data wrapped by this container. 28 | """ 29 | 30 | def __init__(self, data): 31 | if data is None: 32 | self._data = None 33 | self._repr_data = "None" 34 | return 35 | if isinstance(data, VaspData): 36 | self._data = data._data 37 | self._repr_data = data._repr_data 38 | return 39 | self._repr_data = repr(data) 40 | if not hasattr(data, "__array__"): 41 | data = np.array(data) 42 | if data.ndim == 0: 43 | self._data = _parse_scalar(data) 44 | else: 45 | self._data = data 46 | 47 | def __array__(self, *args, **kwargs): 48 | return np.array(self.data, *args, **kwargs) 49 | 50 | def __getitem__(self, key): 51 | return self.data[key] 52 | 53 | def __repr__(self): 54 | return f"{self.__class__.__name__}({self._repr_data})" 55 | 56 | def __len__(self): 57 | return len(self.data) 58 | 59 | def is_none(self): 60 | return self._data is None 61 | 62 | @property 63 | def data(self): 64 | if self.is_none(): 65 | message = """\ 66 | Could not find data in output, please make sure that the provided input 67 | should produce this data and that the VASP calculation already finished. 68 | Also check that VASP did not exit with an error.""" 69 | raise exception.NoData(textwrap.dedent(message)) 70 | else: 71 | return self._data 72 | 73 | @property 74 | def ndim(self): 75 | "The number of dimensions of the data." 76 | return self.data.ndim 77 | 78 | @property 79 | def size(self): 80 | "The total number of elements of the data." 81 | return self.data.size 82 | 83 | @property 84 | def shape(self): 85 | "The shape of the data. Empty tuple for scalar data." 86 | return self.data.shape 87 | 88 | @property 89 | def dtype(self): 90 | "Describes the type of the contained data." 91 | return self.data.dtype 92 | 93 | def astype(self, *args, **kwargs): 94 | "Copy of the array, cast to a specified type." 95 | if self.is_none(): 96 | return self 97 | return self.data.astype(*args, **kwargs) 98 | 99 | 100 | def _parse_scalar(data): 101 | if data.dtype.type == np.bytes_: 102 | data = data[()].decode() 103 | return np.array(data) 104 | -------------------------------------------------------------------------------- /src/py4vasp/_raw/mapping.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | from __future__ import annotations 4 | 5 | import dataclasses 6 | from collections import abc 7 | 8 | from py4vasp import exception 9 | from py4vasp._util import suggest 10 | 11 | 12 | @dataclasses.dataclass 13 | class Mapping(abc.Mapping): 14 | valid_indices: Sequence 15 | 16 | def __len__(self): 17 | return len(self.valid_indices) 18 | 19 | def __iter__(self): 20 | return iter(self.valid_indices) 21 | 22 | def __getitem__(self, key): 23 | index = self.try_to_find_key_in_valid_indices(key) 24 | elements = { 25 | key: value[index] if isinstance(value, list) else value 26 | for key, value in self._as_dict().items() 27 | if key != "valid_indices" 28 | } 29 | return dataclasses.replace(self, valid_indices=[key], **elements) 30 | 31 | def try_to_find_key_in_valid_indices(self, key): 32 | try: 33 | return self.valid_indices.index(key) 34 | except ValueError: 35 | did_you_mean = suggest.did_you_mean(key, self.valid_indices) 36 | message = f"""\ 37 | Could not find the selection "{key}" in the valid selections. {did_you_mean}\ 38 | Please check for possible spelling errors. The following selections are possible: \ 39 | {", ".join(f'"{index}"' for index in self.valid_indices)}.""" 40 | raise exception.IncorrectUsage(message) 41 | 42 | def _as_dict(self): 43 | # shallow copy of dataclass to dictionary 44 | return { 45 | field.name: getattr(self, field.name) 46 | for field in dataclasses.fields(self) 47 | if getattr(self, field.name) is not None 48 | } 49 | -------------------------------------------------------------------------------- /src/py4vasp/_raw/read.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | from py4vasp._util import parse 4 | 5 | 6 | def structure(filename): 7 | contcar = CONTCAR(filename) 8 | return contcar.structure 9 | 10 | 11 | def CONTCAR(filename): 12 | with open(filename, "r") as file: 13 | return parse.POSCAR(file.read()) 14 | -------------------------------------------------------------------------------- /src/py4vasp/_raw/write.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import dataclasses 4 | 5 | from py4vasp._raw.definition import DEFAULT_SOURCE, schema 6 | from py4vasp._raw.schema import Length, Link 7 | from py4vasp._util import check, convert 8 | 9 | 10 | def write(h5f, raw_data): 11 | quantity = convert.quantity_name(raw_data.__class__.__name__) 12 | source = schema.sources[quantity][DEFAULT_SOURCE] 13 | for field in dataclasses.fields(source.data): 14 | target = getattr(source.data, field.name) 15 | data = getattr(raw_data, field.name) 16 | _write_dataset(h5f, target, data) 17 | 18 | 19 | def _write_dataset(h5f, target, data): 20 | if isinstance(target, Link): 21 | write(h5f, data) 22 | elif check.is_none(data) or isinstance(target, Length) or target in h5f: 23 | return 24 | else: 25 | h5f[target] = data 26 | -------------------------------------------------------------------------------- /src/py4vasp/_third_party/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vasp-dev/py4vasp/22e0fa935a750fca129370fc23c30e85c3f71ec3/src/py4vasp/_third_party/__init__.py -------------------------------------------------------------------------------- /src/py4vasp/_third_party/graph/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import copy 4 | 5 | from py4vasp._config import VASP_COLORS 6 | from py4vasp._util import import_ 7 | 8 | from .contour import Contour 9 | from .graph import Graph 10 | from .mixin import Mixin 11 | from .plot import plot 12 | from .series import Series 13 | 14 | go = import_.optional("plotly.graph_objects") 15 | pio = import_.optional("plotly.io") 16 | 17 | if import_.is_imported(go) and import_.is_imported(pio): 18 | axis_format = {"showexponent": "all", "exponentformat": "power"} 19 | contour = copy.copy(pio.templates["ggplot2"].data.contour[0]) 20 | begin_red = [0, VASP_COLORS["red"]] 21 | middle_white = [0.5, "white"] 22 | end_blue = [1, VASP_COLORS["blue"]] 23 | contour.colorscale = [begin_red, middle_white, end_blue] 24 | data = {"contour": (contour,)} 25 | colorway = list(VASP_COLORS.values()) 26 | layout = {"colorway": colorway, "xaxis": axis_format, "yaxis": axis_format} 27 | pio.templates["vasp"] = go.layout.Template(data=data, layout=layout) 28 | pio.templates["ggplot2"].layout.shapedefaults = {} 29 | pio.templates.default = "ggplot2+vasp" 30 | -------------------------------------------------------------------------------- /src/py4vasp/_third_party/graph/mixin.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import abc 4 | import os 5 | 6 | from py4vasp._third_party.graph.graph import Graph 7 | from py4vasp._util import convert 8 | 9 | """Use the Mixin for all quantities that define an option to produce an x-y graph. This 10 | will automatically implement all the common functionality to turn this graphs into 11 | different formats.""" 12 | 13 | 14 | class Mixin(abc.ABC): 15 | @abc.abstractmethod 16 | def to_graph(self, *args, **kwargs): 17 | pass 18 | 19 | def plot(self, *args, **kwargs): 20 | """Almost same as the :py:meth:`to_graph` function. 21 | 22 | All arguments will be passed to to_graph. If the :py:meth:`to_graph` would 23 | produce multiple graphs this method will merge them into a single one.""" 24 | graph_or_graphs = self.to_graph(*args, **kwargs) 25 | if isinstance(graph_or_graphs, Graph): 26 | return graph_or_graphs 27 | else: 28 | return _merge_graphs(graph_or_graphs) 29 | 30 | def to_plotly(self, *args, **kwargs): 31 | """Produces a graph and convertes it to a plotly figure. 32 | 33 | The arguments to this function are passed on to the :py:meth:`to_graph` method. 34 | Takes the resulting graph and converts it to a plotly figure.""" 35 | return self.to_graph(*args, **kwargs).to_plotly() 36 | 37 | def to_image(self, *args, filename=None, **kwargs): 38 | """Read the data and generate an image writing to the given filename. 39 | 40 | The filetype is automatically deduced from the filename; possible 41 | are common raster (png, jpg) and vector (svg, pdf) formats. 42 | If no filename is provided a default filename is deduced from the 43 | name of the class and the picture has png format. 44 | 45 | Note that the filename must be a keyword argument, i.e., you explicitly 46 | need to write *filename="name_of_file"* because the arguments are passed 47 | on to the :py:meth:`to_graph` method. Please check the documentation of that 48 | method to learn which arguments are allowed.""" 49 | fig = self.to_plotly(*args, **kwargs) 50 | classname = convert.quantity_name(self.__class__.__name__).strip("_") 51 | filename = filename if filename is not None else f"{classname}.png" 52 | if os.path.isabs(filename): 53 | writeout_path = filename 54 | else: 55 | writeout_path = self._path / filename 56 | fig.write_image(writeout_path) 57 | 58 | def to_frame(self, *args, **kwargs): 59 | """Convert data to pandas dataframe. 60 | 61 | This will first convert use the :py:meth:`to_graph` method to convert to a 62 | Graph. All arguments are passed to that method. The resulting graph is then 63 | converted to a dataframe. 64 | 65 | Returns 66 | ------- 67 | Dataframe 68 | Pandas dataframe corresponding to data in the graph 69 | """ 70 | graph = self.to_graph(*args, **kwargs) 71 | return graph.to_frame() 72 | 73 | def to_csv(self, *args, filename=None, **kwargs): 74 | """Writes the data to a csv file. 75 | 76 | Writes out a csv file for data stored in a dataframe generated with 77 | the :py:meth:`to_frame` method. Useful for creating external plots 78 | for further analysis. 79 | 80 | If no filename is provided a default filename is deduced from the 81 | name of the class. 82 | 83 | Note that the filename must be a keyword argument, i.e., you explicitly 84 | need to write *filename="name_of_file"* because the arguments are passed 85 | on to the :py:meth:`to_graph` method. Please check the documentation of that 86 | method to learn which arguments are allowed. 87 | 88 | Parameters 89 | ---------- 90 | filename: str | Path 91 | Name of the csv file which the data is exported to. 92 | """ 93 | classname = convert.quantity_name(self.__class__.__name__).strip("_") 94 | filename = filename if filename is not None else f"{classname}.csv" 95 | if os.path.isabs(filename): 96 | writeout_path = filename 97 | else: 98 | writeout_path = self._path / filename 99 | df = self.to_frame(*args, **kwargs) 100 | df.to_csv(writeout_path, index=False) 101 | 102 | 103 | def _merge_graphs(graphs): 104 | result = Graph([]) 105 | for label, graph in graphs.items(): 106 | result = result + graph.label(label) 107 | return result 108 | -------------------------------------------------------------------------------- /src/py4vasp/_third_party/graph/plot.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | from py4vasp._third_party.graph.graph import Graph 4 | from py4vasp._third_party.graph.series import Series 5 | 6 | 7 | def plot(x, y, label=None, **kwargs): 8 | """Plot the given data, modifying the look with some optional arguments. 9 | 10 | The intent of this function is not to provide a full fledged plotting functionality 11 | but as a convenient wrapper around the objects used by py4vasp. This gives a 12 | similar look and feel for the tutorials and facilitates simple plots with a very 13 | minimal interface. Use a proper plotting library (e.g. matplotlib or plotly) to 14 | realize more advanced plots. 15 | 16 | Parameters 17 | ---------- 18 | x : np.ndarray 19 | The x values of the coordinates. 20 | y : np.ndarray 21 | The y values of the coordinates. 22 | label : str 23 | If set this will be used to label the series. 24 | **kwargs 25 | All additional arguments will be passed to initialize Series and Graph. 26 | 27 | Returns 28 | ------- 29 | Graph 30 | A graph containing all given series and optional styles. 31 | 32 | Examples 33 | -------- 34 | Plot simple x-y data with an optional label 35 | 36 | >>> plot(x, y, "label") 37 | 38 | Plot two series in the same graph 39 | 40 | >>> plot(x1, y1) + plot(x2, y2) 41 | 42 | Attributes of the graph are modified by keyword arguments 43 | 44 | >>> plot(x, y, xlabel="xaxis", ylabel="yaxis") 45 | """ 46 | series = _parse_series(x, y, label, **kwargs) 47 | for_graph = {key: val for key, val in kwargs.items() if key in Graph._fields} 48 | return Graph(series, **for_graph) 49 | 50 | 51 | def _parse_series(x, y, label, **kwargs): 52 | for_series = {key: val for key, val in kwargs.items() if key in Series._fields} 53 | return Series(x, y, label, **for_series) 54 | -------------------------------------------------------------------------------- /src/py4vasp/_third_party/graph/trace.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | from abc import ABC, abstractmethod 4 | 5 | 6 | class Trace(ABC): 7 | """Defines a base class with all methods that need to be implemented for Graph to 8 | work as intended""" 9 | 10 | @abstractmethod 11 | def to_plotly(self): 12 | """Use yield to generate one or more plotly traces. Each returned element should 13 | be a tuple (trace, dict) where the trace can be used as data for plotly and the 14 | options modify the generation of the figure.""" 15 | -------------------------------------------------------------------------------- /src/py4vasp/_third_party/interactive.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import contextlib 4 | import os 5 | 6 | from py4vasp._util import import_ 7 | 8 | IPython = import_.optional("IPython") 9 | _ERROR_VERBOSITY = "Minimal" 10 | 11 | 12 | def set_error_handling(verbosity): 13 | global _ERROR_VERBOSITY 14 | ipython = _get_ipython() 15 | if ipython is None: 16 | _ERROR_VERBOSITY = verbosity 17 | else: 18 | ipython.InteractiveTB.set_mode(verbosity) 19 | 20 | 21 | def error_handling(): 22 | ipython = _get_ipython() 23 | if ipython is None: 24 | return _ERROR_VERBOSITY 25 | else: 26 | return ipython.InteractiveTB.mode 27 | 28 | 29 | def _get_ipython(): 30 | if import_.is_imported(IPython): 31 | return IPython.get_ipython() 32 | else: 33 | return None 34 | -------------------------------------------------------------------------------- /src/py4vasp/_third_party/view/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | from .mixin import Mixin 4 | from .view import GridQuantity, IonArrow, Isosurface, View 5 | -------------------------------------------------------------------------------- /src/py4vasp/_third_party/view/mixin.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import abc 4 | 5 | """Use the Mixin for all quantities that define an option to produce a structure view. 6 | This will automatically implement all the common functionality to visualize this data.turn this graphs into 7 | different formats.""" 8 | 9 | 10 | class Mixin(abc.ABC): 11 | @abc.abstractmethod 12 | def to_view(self, *args, **kwargs): 13 | pass 14 | 15 | def plot(self, *args, **kwargs): 16 | """Wrapper around :meth:`to_view` method. 17 | 18 | This method will visualize the quantity in the structure. Please refer to 19 | the :meth:`to_view` method for a documentation of the allowed arguments. 20 | 21 | Returns 22 | ------- 23 | View 24 | A visualization of the quantity within the crystal structure. 25 | """ 26 | return self.to_view(*args, **kwargs) 27 | 28 | def to_ngl(self, *args, **kwargs): 29 | """Convert the view to an NGL widget. 30 | 31 | This method wraps the :meth:`to_view` method and converts the resulting View 32 | to an NGL widget. The :meth:`to_view` method documents all the possible 33 | arguments of this function. 34 | 35 | Returns 36 | ------- 37 | NGLWidget 38 | A widget to display the structure and other quantities in the unit cell. 39 | """ 40 | return self.to_view(*args, **kwargs).to_ngl() 41 | -------------------------------------------------------------------------------- /src/py4vasp/_util/check.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import inspect 4 | import numbers 5 | 6 | from py4vasp import exception, raw 7 | 8 | 9 | def is_none(obj): 10 | if isinstance(obj, raw.VaspData): 11 | return obj.is_none() 12 | return obj is None 13 | 14 | 15 | def raise_error_if_not_string(test_if_string, error_message): 16 | if test_if_string.__class__ != str: 17 | raise exception.IncorrectUsage(error_message) 18 | 19 | 20 | def raise_error_if_not_number(test_if_number, error_message): 21 | if not isinstance(test_if_number, numbers.Number): 22 | raise exception.IncorrectUsage(error_message) 23 | 24 | 25 | def raise_error_if_not_callable(function, *args, **kwargs): 26 | signature = inspect.signature(function) 27 | try: 28 | signature.bind(*args, **kwargs) 29 | except TypeError as error: 30 | message = f"You tried to call {function.__name__}, but the arguments are incorrect! Please double check your input." 31 | raise exception.IncorrectUsage(message) from error 32 | -------------------------------------------------------------------------------- /src/py4vasp/_util/convert.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import re 4 | import textwrap 5 | 6 | import numpy as np 7 | 8 | 9 | def text_to_string(text): 10 | "Text can be either bytes or string" 11 | try: 12 | return text.decode() 13 | except (UnicodeDecodeError, AttributeError): 14 | return text 15 | 16 | 17 | def to_complex(array): 18 | assert array.dtype == np.float64 19 | assert array.shape[-1] == 2 20 | return array.view(np.complex128).reshape(array.shape[:-1]) 21 | 22 | 23 | def quantity_name(quantity): 24 | if quantity in ["CONTCAR"]: 25 | return quantity 26 | else: 27 | return _to_snakecase(quantity) 28 | 29 | 30 | # NOTE: to_snakecase is the function underscore from the inflection package 31 | # (Copyright (C) 2012-2020 Janne Vanhala) 32 | def _to_snakecase(word: str) -> str: 33 | """ 34 | Make an underscored, lowercase form from the expression in the string. 35 | Example:: 36 | >>> underscore("DeviceType") 37 | 'device_type' 38 | As a rule of thumb you can think of :func:`underscore` as the inverse of 39 | :func:`camelize`, though there are cases where that does not hold:: 40 | >>> camelize(underscore("IOError")) 41 | 'IoError' 42 | """ 43 | word = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", word) 44 | word = re.sub(r"([a-z\d])([A-Z])", r"\1_\2", word) 45 | word = word.replace("-", "_") 46 | return word.lower() 47 | 48 | 49 | # NOTE: to_camelcase is based on the function camelize from the inflection package 50 | # (Copyright (C) 2012-2020 Janne Vanhala) 51 | def to_camelcase(string: str, uppercase_first_letter: bool = True) -> str: 52 | """Convert strings to CamelCase. 53 | 54 | Examples:: 55 | 56 | >>> camelize("device_type") 57 | 'DeviceType' 58 | >>> camelize("device_type", False) 59 | 'deviceType' 60 | 61 | :func:`camelize` can be thought of as a inverse of :func:`underscore`, 62 | although there are some cases where that does not hold:: 63 | 64 | >>> camelize(underscore("IOError")) 65 | 'IoError' 66 | 67 | :param uppercase_first_letter: if set to `True` :func:`camelize` converts 68 | strings to UpperCamelCase. If set to `False` :func:`camelize` produces 69 | lowerCamelCase. Defaults to `True`. 70 | """ 71 | if uppercase_first_letter: 72 | return re.sub(r"(?:_|^)(.)", lambda m: m.group(1).upper(), string) 73 | else: 74 | return string[0].lower() + to_camelcase(string)[1:] 75 | 76 | 77 | def to_rgb(hex): 78 | "Convert a HEX color code to fractional RGB." 79 | hex = hex.lstrip("#") 80 | return np.array([int(part, 16) for part in textwrap.wrap(hex, 2)]) / 255 81 | 82 | 83 | def to_lab(hex): 84 | "Convert a HEX color code to CIELAB color space." 85 | rgb = to_rgb(hex) 86 | rgb_to_xyz = np.array( 87 | ( 88 | [0.4124564, 0.3575761, 0.1804375], 89 | [0.2126729, 0.7151522, 0.0721750], 90 | [0.0193339, 0.1191920, 0.9503041], 91 | ) 92 | ) 93 | rgb = np.where(rgb > 0.04045, ((rgb + 0.055) / 1.055) ** 2.4, rgb / 12.92) 94 | x, y, z = rgb_to_xyz @ rgb 95 | xn = 0.950489 96 | zn = 1.088840 97 | t0 = (6 / 29) ** 3 98 | f = lambda t: t ** (1 / 3) if t > t0 else 2 / 29 * (t / t0 + 2) 99 | l = 116 * f(y) - 16 100 | a = 500 * (f(x / xn) - f(y)) 101 | b = 200 * (f(y) - f(z / zn)) 102 | return np.array((l, a, b)) 103 | -------------------------------------------------------------------------------- /src/py4vasp/_util/documentation.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import inspect 4 | 5 | 6 | def format(**kwargs): 7 | def format_documentation_of_function(func): 8 | clean_kwargs = {key: str(value).rstrip() for key, value in kwargs.items()} 9 | func.__doc__ = inspect.getdoc(func).format(**clean_kwargs) 10 | return func 11 | 12 | return format_documentation_of_function 13 | -------------------------------------------------------------------------------- /src/py4vasp/_util/import_.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import importlib 4 | 5 | from py4vasp import exception 6 | 7 | 8 | class _ModulePlaceholder: 9 | def __init__(self, name): 10 | self._name = name 11 | 12 | def __getattr__(self, attr): 13 | raise exception.ModuleNotInstalled( 14 | "You use an optional part of py4vasp that relies on the package " 15 | f"'{self._name}'. Please install the package to use this functionality." 16 | ) 17 | 18 | 19 | def optional(name): 20 | try: 21 | return importlib.import_module(name) 22 | except: 23 | return _ModulePlaceholder(name) 24 | 25 | 26 | def is_imported(module): 27 | return not isinstance(module, _ModulePlaceholder) 28 | -------------------------------------------------------------------------------- /src/py4vasp/_util/reader.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import numpy as np 4 | 5 | from py4vasp import exception 6 | 7 | 8 | class Reader: 9 | "Helper class to deal with error handling of the array reading." 10 | 11 | def __init__(self, array): 12 | self._array = array 13 | self.shape = np.shape(array) 14 | 15 | def error_message(self, key, err): 16 | "We can overload this message in a subclass to make it more specific" 17 | return ( 18 | "Error reading from the array, please check that the shape of the " 19 | "array is consistent with the access key." 20 | ) 21 | 22 | def __getitem__(self, key): 23 | try: 24 | return self._array[key] 25 | except (ValueError, IndexError, TypeError) as err: 26 | raise exception.IncorrectUsage(self.error_message(key, err)) from err 27 | -------------------------------------------------------------------------------- /src/py4vasp/_util/suggest.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import difflib 4 | 5 | 6 | def did_you_mean(selection, possibilities): 7 | """Returns 'Did you mean X? ' if any X in `possibilities` is close to the `selection` 8 | input, otherwise it returns an empty string. 9 | 10 | Note that the trailing empty space allows including it in error messages directly. 11 | It will convert the inputs to strings to parse as much as possible. 12 | 13 | Parameters 14 | ---------- 15 | selection : str 16 | The current selection used by the user, which is supposed to match one of the 17 | possibilities. The input is converted to string. 18 | possibilities : Sequence[str] 19 | A list of possible values for the selection. We find the closest match of the 20 | given selection to any of the given values. All values are converted to string. 21 | 22 | Returns 23 | ------- 24 | str 25 | The string contains the closest possible match if any was found. 26 | """ 27 | selection = str(selection) 28 | possibilities = [str(possibility) for possibility in possibilities] 29 | best_choice = difflib.get_close_matches(selection, possibilities, n=1) 30 | if best_choice: 31 | return f'Did you mean "{best_choice[0]}"? ' 32 | else: 33 | return "" 34 | -------------------------------------------------------------------------------- /src/py4vasp/cli.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import pathlib 4 | 5 | import click 6 | 7 | import py4vasp 8 | from py4vasp import exception 9 | 10 | 11 | @click.group() 12 | def cli(): 13 | pass 14 | 15 | 16 | @cli.command() 17 | @click.argument("quantity", type=click.Choice(("structure",)), metavar="QUANTITY") 18 | @click.argument("format", type=click.STRING) 19 | @click.option( 20 | "-f", 21 | "--from", 22 | "path", 23 | type=click.Path(exists=True, readable=True), 24 | help="Overwrite the default path where py4vasp looks for the quantity.", 25 | ) 26 | @click.option( 27 | "-s", 28 | "--selection", 29 | type=click.STRING, 30 | help="String to further clarify the specific source of the quantity.", 31 | ) 32 | def convert(quantity, format, path, selection): 33 | """Convert a quantity to a different format. 34 | 35 | Specify which QUANTITY you want to convert into which FORMAT. 36 | """ 37 | if format.lower() != "lammps": 38 | raise click.UsageError(f"Converting {quantity} to {format} is not implemented.") 39 | path = pathlib.Path.cwd() if path is None else pathlib.Path(path) 40 | try: 41 | result = _convert_to_lammps(path, selection) 42 | except exception.Py4VaspError as error: 43 | raise click.ClickException(*error.args) from error 44 | print(result) 45 | 46 | 47 | def _convert_to_lammps(path, selection): 48 | if path.is_file(): 49 | calculation = py4vasp.Calculation.from_file(path) 50 | else: 51 | calculation = py4vasp.Calculation.from_path(path) 52 | if selection is None: 53 | result = calculation.structure.to_lammps() 54 | else: 55 | result = calculation.structure.to_lammps(selection=selection) 56 | return result 57 | -------------------------------------------------------------------------------- /src/py4vasp/combine.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | from py4vasp._combine.energies import Energies 4 | from py4vasp._combine.forces import Forces 5 | from py4vasp._combine.stresses import Stresses 6 | -------------------------------------------------------------------------------- /src/py4vasp/control.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | """Setup input data for VASP calculations. 4 | 5 | VASP requires several input files to execute. We provide some simple helper classes and 6 | routines to generate these files from python. You can also use the routines to extract 7 | the input files from a path. 8 | """ 9 | from py4vasp._control.incar import INCAR 10 | from py4vasp._control.kpoints import KPOINTS 11 | from py4vasp._control.poscar import POSCAR 12 | -------------------------------------------------------------------------------- /src/py4vasp/exception.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | """Deals with the possible exceptions in py4vasp. 4 | 5 | The design goal is that all foreseeable exceptions in py4vasp issue an exception of the Py4VaspException class. Any other kind of exception would indicate a bug in the code. If possible the part standard users interact with should not raise any exception, but should give advice on how to overcome the issue. 6 | """ 7 | 8 | 9 | class Py4VaspError(Exception): 10 | """Base class for all exceptions raised by py4vasp""" 11 | 12 | 13 | class RefinementError(Py4VaspError): 14 | """When refining the raw dataclass into the class handling e.g. reading and 15 | plotting of the data an error occurred""" 16 | 17 | 18 | class IncorrectUsage(Py4VaspError): 19 | """The user provided input is not suitable for processing""" 20 | 21 | 22 | class DataMismatch(Py4VaspError): 23 | """The data provided is not suitable to be processed by the called function.""" 24 | 25 | 26 | class NotImplemented(Py4VaspError): 27 | """Exception raised when a function is called that is not implemented.""" 28 | 29 | 30 | class NoData(Py4VaspError): 31 | """Exception raised when certain data is not present, because the corresponding 32 | INCAR flags have not been set.""" 33 | 34 | 35 | class FileAccessError(Py4VaspError): 36 | """Exception raised when error occurs during accessing the HDF5 file.""" 37 | 38 | 39 | class OutdatedVaspVersion(Py4VaspError): 40 | """Exception raised when the py4vasp features used are not available in the 41 | used version of Vasp.""" 42 | 43 | 44 | class ModuleNotInstalled(Py4VaspError): 45 | """Exception raised when a functionality is used that relies on an optional 46 | dependency of py4vasp but that dependency is not installed.""" 47 | 48 | 49 | class StopExecution(Py4VaspError): 50 | """Exception raised when an error occurred in the user interface. This prevents 51 | further cells from being executed.""" 52 | 53 | def _render_traceback_(self): 54 | "This exception is silent and does not produce any traceback." 55 | pass 56 | 57 | 58 | class ParserError(Py4VaspError): 59 | """Exception raised when the parser encounters an error.""" 60 | 61 | 62 | class _Py4VaspInternalError(Exception): 63 | """This error should not propagate to the user. It should be raised when a local 64 | routine encounters unexpected behavior that the routine cannot deal with. Then 65 | the calling routine should resolve the error or reraise it as a Py4VaspError.""" 66 | -------------------------------------------------------------------------------- /src/py4vasp/raw.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | """Extract the raw data from the HDF5 file and transform it into dataclasses. 4 | 5 | In the HDF5 file, the raw data is stored with specific keys. To avoid 6 | propagating the name of these keys to the higher tier modules, we transform 7 | everything into dataclasses. This enables the introduction of new file formats by 8 | replacing the `access` function. 9 | 10 | Notes 11 | ----- 12 | The data from the HDF5 file is lazily loaded except for scalars. This avoids 13 | memory issues when the HDF5 file contains a lot of data, because only what is 14 | needed is read. However, this has the consequence that you need to 15 | enforce the read operation before the file is closed. 16 | """ 17 | 18 | from py4vasp._raw.access import access 19 | from py4vasp._raw.data import * 20 | from py4vasp._raw.definition import get_schema, selections 21 | -------------------------------------------------------------------------------- /src/py4vasp/scripts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vasp-dev/py4vasp/22e0fa935a750fca129370fc23c30e85c3f71ec3/src/py4vasp/scripts/__init__.py -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vasp-dev/py4vasp/22e0fa935a750fca129370fc23c30e85c3f71ec3/tests/__init__.py -------------------------------------------------------------------------------- /tests/analysis/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vasp-dev/py4vasp/22e0fa935a750fca129370fc23c30e85c3f71ec3/tests/analysis/__init__.py -------------------------------------------------------------------------------- /tests/calculation/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vasp-dev/py4vasp/22e0fa935a750fca129370fc23c30e85c3f71ec3/tests/calculation/__init__.py -------------------------------------------------------------------------------- /tests/calculation/conftest.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import inspect 4 | from unittest.mock import MagicMock, patch 5 | 6 | import pytest 7 | 8 | from py4vasp import exception 9 | from py4vasp._util import convert, import_ 10 | 11 | formatters = import_.optional("IPython.core.formatters") 12 | 13 | TEST_FILENAME = "read_data_from_this_file" 14 | SELECTION = "alternative" 15 | 16 | 17 | @pytest.fixture 18 | def mock_schema(): 19 | mock = MagicMock() 20 | mock.selections.return_value = ("default", SELECTION) 21 | with patch("py4vasp._raw.definition.schema", mock): 22 | yield mock 23 | 24 | 25 | @pytest.fixture 26 | def check_factory_methods(mock_schema, not_core): 27 | def inner(cls, data, parameters={}): 28 | instance = cls.from_path() 29 | check_instance_accesses_data(instance, data, parameters) 30 | instance = cls.from_file(TEST_FILENAME) 31 | check_instance_accesses_data(instance, data, parameters, file=TEST_FILENAME) 32 | 33 | return inner 34 | 35 | 36 | def check_instance_accesses_data(instance, data, parameters, file=None): 37 | failed = [] 38 | for name, method in inspect.getmembers(instance, inspect.ismethod): 39 | if should_test_method(name, parameters): 40 | kwargs = parameters.get(name, {}) 41 | try: 42 | check_method_accesses_data(data, method, file, **kwargs) 43 | except (AttributeError, AssertionError): 44 | failed.append(name) 45 | if failed: 46 | message = ( 47 | f"The method(s) {', '.join(failed)} do not load the data from file." 48 | " The most likely issue is a missing @base.data_access decorator." 49 | ) 50 | raise AssertionError(message) 51 | 52 | 53 | def should_test_method(name, parameters): 54 | if name in parameters: 55 | return True 56 | if name in ("__str__", "_repr_html_"): 57 | return True 58 | if name.startswith("from") or name.startswith("_"): 59 | return False 60 | if name == "to_image": # would have side effects 61 | return False 62 | if name == "to_csv": 63 | return False 64 | return True 65 | 66 | 67 | def check_method_accesses_data(data, method_under_test, file, **kwargs): 68 | quantity = convert.quantity_name(data.__class__.__name__) 69 | with patch("py4vasp.raw.access") as mock_access: 70 | mock_access.return_value.__enter__.side_effect = lambda *_: data 71 | execute_method(method_under_test, **kwargs) 72 | check_mock_called(mock_access, quantity, file) 73 | mock_access.reset_mock() 74 | if "selection" in kwargs: 75 | kwargs = kwargs.copy() 76 | kwargs.pop("selection") 77 | execute_method(method_under_test, selection=SELECTION, **kwargs) 78 | check_mock_called(mock_access, quantity, file, selection=SELECTION) 79 | 80 | 81 | def execute_method(method_under_test, **kwargs): 82 | try: 83 | method_under_test(**kwargs) 84 | except (exception.NotImplemented, exception.IncorrectUsage, exception.DataMismatch): 85 | # ignore py4vasp error 86 | pass 87 | 88 | 89 | def check_mock_called(mock_access, quantity, file, selection=None): 90 | mock_access.assert_called_once() 91 | args, kwargs = mock_access.call_args 92 | assert (quantity,) == args 93 | assert kwargs.get("selection") == selection 94 | assert kwargs.get("file") == file 95 | 96 | 97 | @pytest.fixture 98 | def format_(not_core): 99 | return formatters.DisplayFormatter().format 100 | -------------------------------------------------------------------------------- /tests/calculation/test_born_effective_charge.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import types 4 | 5 | import pytest 6 | 7 | from py4vasp._calculation.born_effective_charge import BornEffectiveCharge 8 | from py4vasp._calculation.structure import Structure 9 | 10 | 11 | @pytest.fixture 12 | def Sr2TiO4(raw_data): 13 | raw_born_charges = raw_data.born_effective_charge("Sr2TiO4") 14 | born_charges = BornEffectiveCharge.from_data(raw_born_charges) 15 | born_charges.ref = types.SimpleNamespace() 16 | structure = Structure.from_data(raw_born_charges.structure) 17 | born_charges.ref.structure = structure 18 | born_charges.ref.charge_tensors = raw_born_charges.charge_tensors 19 | return born_charges 20 | 21 | 22 | def test_Sr2TiO4_read(Sr2TiO4, Assert): 23 | actual = Sr2TiO4.read() 24 | reference_structure = Sr2TiO4.ref.structure.read() 25 | for key in actual["structure"]: 26 | if key in ("elements", "names"): 27 | assert actual["structure"][key] == reference_structure[key] 28 | else: 29 | Assert.allclose(actual["structure"][key], reference_structure[key]) 30 | Assert.allclose(actual["charge_tensors"], Sr2TiO4.ref.charge_tensors) 31 | 32 | 33 | def test_Sr2TiO4_print(Sr2TiO4, format_): 34 | actual, _ = format_(Sr2TiO4) 35 | reference = """ 36 | BORN EFFECTIVE CHARGES (including local field effects) (in |e|, cumulative output) 37 | --------------------------------------------------------------------------------- 38 | ion 1 Sr 39 | 1 0.00000 1.00000 2.00000 40 | 2 3.00000 4.00000 5.00000 41 | 3 6.00000 7.00000 8.00000 42 | ion 2 Sr 43 | 1 9.00000 10.00000 11.00000 44 | 2 12.00000 13.00000 14.00000 45 | 3 15.00000 16.00000 17.00000 46 | ion 3 Ti 47 | 1 18.00000 19.00000 20.00000 48 | 2 21.00000 22.00000 23.00000 49 | 3 24.00000 25.00000 26.00000 50 | ion 4 O 51 | 1 27.00000 28.00000 29.00000 52 | 2 30.00000 31.00000 32.00000 53 | 3 33.00000 34.00000 35.00000 54 | ion 5 O 55 | 1 36.00000 37.00000 38.00000 56 | 2 39.00000 40.00000 41.00000 57 | 3 42.00000 43.00000 44.00000 58 | ion 6 O 59 | 1 45.00000 46.00000 47.00000 60 | 2 48.00000 49.00000 50.00000 61 | 3 51.00000 52.00000 53.00000 62 | ion 7 O 63 | 1 54.00000 55.00000 56.00000 64 | 2 57.00000 58.00000 59.00000 65 | 3 60.00000 61.00000 62.00000 66 | """.strip() 67 | assert actual == {"text/plain": reference} 68 | 69 | 70 | def test_factory_methods(raw_data, check_factory_methods): 71 | data = raw_data.born_effective_charge("Sr2TiO4") 72 | check_factory_methods(BornEffectiveCharge, data) 73 | -------------------------------------------------------------------------------- /tests/calculation/test_class.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import os 4 | from pathlib import Path 5 | from unittest.mock import mock_open, patch 6 | 7 | import pytest 8 | 9 | from py4vasp import Calculation, _calculation, control, exception 10 | from py4vasp._calculation import base 11 | 12 | 13 | @patch.object(base.Refinery, "from_path", autospec=True) 14 | @patch("py4vasp.raw.access", autospec=True) 15 | def test_creation_from_path(mock_access, mock_from_path): 16 | # note: in pytest __file__ defaults to absolute path 17 | absolute_path = Path(__file__) 18 | calc = Calculation.from_path(absolute_path) 19 | assert calc.path() == absolute_path 20 | relative_path = os.path.relpath(absolute_path, Path.cwd()) 21 | calc = Calculation.from_path(relative_path) 22 | assert calc.path() == absolute_path 23 | calc = Calculation.from_path("~") 24 | assert calc.path() == Path.home() 25 | mock_access.assert_not_called() 26 | mock_from_path.assert_not_called() 27 | calc.band # access the band object 28 | mock_from_path.assert_called_once() 29 | 30 | 31 | @patch.object(base.Refinery, "from_file", autospec=True) 32 | @patch("py4vasp.raw.access", autospec=True) 33 | def test_creation_from_file(mock_access, mock_from_file): 34 | # note: in pytest __file__ defaults to absolute path 35 | absolute_path = Path(__file__) 36 | absolute_file = absolute_path / "example.h5" 37 | calc = Calculation.from_file(absolute_file) 38 | assert calc.path() == absolute_path 39 | relative_file = os.path.relpath(absolute_file, Path.cwd()) 40 | calc = Calculation.from_file(relative_file) 41 | assert calc.path() == absolute_path 42 | calc = Calculation.from_file("~/example.h5") 43 | assert calc.path() == Path.home() 44 | mock_access.assert_not_called() 45 | mock_from_file.assert_not_called() 46 | calc.band # access the band object 47 | mock_from_file.assert_called() 48 | 49 | 50 | @patch("py4vasp.raw.access", autospec=True) 51 | def test_all_attributes(mock_access): 52 | calc = Calculation.from_path("test_path") 53 | for name in _calculation.QUANTITIES: # + _calculation.INPUT_FILES: 54 | assert hasattr(calc, name) 55 | for group, quantities in _calculation.GROUPS.items(): 56 | assert hasattr(calc, group) 57 | namespace = getattr(calc, group) 58 | for quantity in quantities: 59 | assert hasattr(namespace, quantity) 60 | mock_access.assert_not_called() 61 | mock_access.return_value.__enter__.assert_not_called() 62 | 63 | 64 | @pytest.mark.skip("Input files are not included in current release.") 65 | def test_input_files_from_path(): 66 | with patch("py4vasp._control.base.InputFile.__init__", return_value=None) as mock: 67 | calculation = Calculation.from_path("test_path") 68 | mock.assert_called_with(calculation.path()) 69 | calculation = Calculation.from_path("test_path") 70 | check_all_input_files(calculation) 71 | 72 | 73 | @pytest.mark.skip("Input files are not included in current release.") 74 | def test_input_files_from_file(): 75 | with patch("py4vasp._control.base.InputFile.__init__", return_value=None) as mock: 76 | calculation = Calculation.from_file("test_file") 77 | mock.assert_called_with(calculation.path()) 78 | calculation = Calculation.from_file("test_file") 79 | check_all_input_files(calculation) 80 | 81 | 82 | def check_all_input_files(calculation): 83 | input_files = [control.INCAR, control.KPOINTS, control.POSCAR] 84 | for input_file in input_files: 85 | check_one_input_file(calculation, input_file) 86 | 87 | 88 | def check_one_input_file(calculation, input_file): 89 | text = "! comment line" 90 | name = input_file.__name__ 91 | assert isinstance(getattr(calculation, name), input_file) 92 | with patch("py4vasp._control.base.open", mock_open(read_data=text)) as mock: 93 | setattr(calculation, name, text) 94 | mock.assert_called_once_with(calculation.path() / name, "w") 95 | mock.reset_mock() 96 | assert getattr(calculation, name).read() == text 97 | mock.assert_called_once_with(calculation.path() / name, "r") 98 | 99 | 100 | def test_using_constructor_raises_exception(): 101 | with pytest.raises(exception.IncorrectUsage): 102 | Calculation() 103 | with pytest.raises(exception.IncorrectUsage): 104 | Calculation("path") 105 | with pytest.raises(exception.IncorrectUsage): 106 | Calculation(key="value") 107 | -------------------------------------------------------------------------------- /tests/calculation/test_default_calculation.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import pytest 4 | 5 | from py4vasp import Calculation, calculation 6 | 7 | 8 | def test_access_of_attributes(): 9 | calc = Calculation.from_path(".") 10 | for key in filter(attribute_included, dir(calc)): 11 | getattr(calculation, key) 12 | 13 | 14 | def attribute_included(attr): 15 | if attr.startswith("_"): # do not include private attributes 16 | return False 17 | if attr.startswith("from"): # do not include classmethods 18 | return False 19 | return True 20 | 21 | 22 | @pytest.mark.skip("Input files are not included in current release.") 23 | def test_assigning_to_input_file(tmp_path, monkeypatch): 24 | monkeypatch.chdir(tmp_path) 25 | expected = "SYSTEM = demo INCAR file" 26 | calculation.INCAR = expected 27 | with open("INCAR", "r") as file: 28 | actual = file.read() 29 | assert actual == expected 30 | -------------------------------------------------------------------------------- /tests/calculation/test_dielectric_tensor.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import types 4 | 5 | import pytest 6 | 7 | from py4vasp import exception 8 | from py4vasp._calculation.dielectric_tensor import DielectricTensor 9 | 10 | 11 | @pytest.fixture 12 | def dft_tensor(raw_data): 13 | expected_description = "including local field effects in DFT" 14 | return make_reference(raw_data, "dft with_ion", expected_description) 15 | 16 | 17 | @pytest.fixture 18 | def rpa_tensor(raw_data): 19 | expected_description = "including local field effects in RPA (Hartree)" 20 | return make_reference(raw_data, "rpa without_ion", expected_description) 21 | 22 | 23 | @pytest.fixture 24 | def scf_tensor(raw_data): 25 | expected_description = "including local field effects" 26 | return make_reference(raw_data, "scf with_ion", expected_description) 27 | 28 | 29 | @pytest.fixture 30 | def nscf_tensor(raw_data): 31 | expected_description = "excluding local field effects" 32 | return make_reference(raw_data, "nscf without_ion", expected_description) 33 | 34 | 35 | def make_reference(raw_data, method, expected_description): 36 | raw_tensor = raw_data.dielectric_tensor(method) 37 | tensor = DielectricTensor.from_data(raw_tensor) 38 | tensor.ref = types.SimpleNamespace() 39 | tensor.ref.clamped_ion = raw_tensor.electron 40 | if raw_tensor.ion.is_none(): 41 | tensor.ref.relaxed_ion = None 42 | else: 43 | tensor.ref.relaxed_ion = raw_tensor.ion + raw_tensor.electron 44 | tensor.ref.independent_particle = raw_tensor.independent_particle 45 | tensor.ref.method = method.split()[0] 46 | tensor.ref.expected_description = expected_description 47 | return tensor 48 | 49 | 50 | def test_read_dft_tensor(dft_tensor, Assert): 51 | check_read_dielectric_tensor(dft_tensor, Assert) 52 | 53 | 54 | def test_read_rpa_tensor(rpa_tensor, Assert): 55 | check_read_dielectric_tensor(rpa_tensor, Assert) 56 | 57 | 58 | def test_read_scf_tensor(scf_tensor, Assert): 59 | check_read_dielectric_tensor(scf_tensor, Assert) 60 | 61 | 62 | def test_read_nscf_tensor(nscf_tensor, Assert): 63 | check_read_dielectric_tensor(nscf_tensor, Assert) 64 | 65 | 66 | def check_read_dielectric_tensor(dielectric_tensor, Assert): 67 | for method in (dielectric_tensor.read, dielectric_tensor.to_dict): 68 | actual = method() 69 | reference = dielectric_tensor.ref 70 | Assert.allclose(actual["clamped_ion"], reference.clamped_ion) 71 | Assert.allclose(actual["relaxed_ion"], reference.relaxed_ion) 72 | Assert.allclose(actual["independent_particle"], reference.independent_particle) 73 | assert actual["method"] == reference.method 74 | 75 | 76 | def test_unknown_method(raw_data): 77 | raw_tensor = raw_data.dielectric_tensor("unknown_method with_ion") 78 | with pytest.raises(exception.NotImplemented): 79 | DielectricTensor.from_data(raw_tensor).print() 80 | 81 | 82 | def test_print_dft_tensor(dft_tensor, format_): 83 | actual, _ = format_(dft_tensor) 84 | check_print_dielectric_tensor(actual, dft_tensor.ref) 85 | 86 | 87 | def test_print_rpa_tensor(rpa_tensor, format_): 88 | actual, _ = format_(rpa_tensor) 89 | check_print_dielectric_tensor(actual, rpa_tensor.ref) 90 | 91 | 92 | def test_print_scf_tensor(scf_tensor, format_): 93 | actual, _ = format_(scf_tensor) 94 | check_print_dielectric_tensor(actual, scf_tensor.ref) 95 | 96 | 97 | def test_print_dft_tensor(nscf_tensor, format_): 98 | actual, _ = format_(nscf_tensor) 99 | check_print_dielectric_tensor(actual, nscf_tensor.ref) 100 | 101 | 102 | def check_print_dielectric_tensor(actual, reference): 103 | if reference.relaxed_ion is None: 104 | relaxed_ion = "" 105 | else: 106 | relaxed_ion = """\ 107 | relaxed-ion 108 | 9.000000 11.000000 13.000000 109 | 15.000000 17.000000 19.000000 110 | 21.000000 23.000000 25.000000 111 | """ 112 | expected = f""" 113 | Macroscopic static dielectric tensor (dimensionless) 114 | {reference.expected_description} 115 | ------------------------------------------------------ 116 | clamped-ion 117 | 0.000000 1.000000 2.000000 118 | 3.000000 4.000000 5.000000 119 | 6.000000 7.000000 8.000000 120 | {relaxed_ion} 121 | """.strip() 122 | assert actual == {"text/plain": expected} 123 | 124 | 125 | def test_factory_methods(raw_data, check_factory_methods): 126 | data = raw_data.dielectric_tensor("dft with_ion") 127 | check_factory_methods(DielectricTensor, data) 128 | -------------------------------------------------------------------------------- /tests/calculation/test_elastic_modulus.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import types 4 | 5 | import pytest 6 | 7 | from py4vasp._calculation.elastic_modulus import ElasticModulus 8 | 9 | 10 | @pytest.fixture 11 | def elastic_modulus(raw_data): 12 | raw_elastic_modulus = raw_data.elastic_modulus("dft") 13 | elastic_modulus = ElasticModulus.from_data(raw_elastic_modulus) 14 | elastic_modulus.ref = types.SimpleNamespace() 15 | elastic_modulus.ref.clamped_ion = raw_elastic_modulus.clamped_ion 16 | elastic_modulus.ref.relaxed_ion = raw_elastic_modulus.relaxed_ion 17 | return elastic_modulus 18 | 19 | 20 | def test_read(elastic_modulus, Assert): 21 | actual = elastic_modulus.read() 22 | Assert.allclose(actual["clamped_ion"], elastic_modulus.ref.clamped_ion) 23 | Assert.allclose(actual["relaxed_ion"], elastic_modulus.ref.relaxed_ion) 24 | 25 | 26 | def test_print(elastic_modulus, format_): 27 | actual, _ = format_(elastic_modulus) 28 | reference = f""" 29 | Elastic modulus (kBar) 30 | Direction XX YY ZZ XY YZ ZX 31 | -------------------------------------------------------------------------------- 32 | clamped-ion 33 | XX 0.0000 4.0000 8.0000 2.0000 6.0000 4.0000 34 | YY 36.0000 40.0000 44.0000 38.0000 42.0000 40.0000 35 | ZZ 72.0000 76.0000 80.0000 74.0000 78.0000 76.0000 36 | XY 18.0000 22.0000 26.0000 20.0000 24.0000 22.0000 37 | YZ 54.0000 58.0000 62.0000 56.0000 60.0000 58.0000 38 | ZX 36.0000 40.0000 44.0000 38.0000 42.0000 40.0000 39 | relaxed-ion 40 | XX 81.0000 85.0000 89.0000 83.0000 87.0000 85.0000 41 | YY 117.0000 121.0000 125.0000 119.0000 123.0000 121.0000 42 | ZZ 153.0000 157.0000 161.0000 155.0000 159.0000 157.0000 43 | XY 99.0000 103.0000 107.0000 101.0000 105.0000 103.0000 44 | YZ 135.0000 139.0000 143.0000 137.0000 141.0000 139.0000 45 | ZX 117.0000 121.0000 125.0000 119.0000 123.0000 121.0000 46 | """.strip() 47 | assert actual == {"text/plain": reference} 48 | 49 | 50 | def test_factory_methods(raw_data, check_factory_methods): 51 | data = raw_data.elastic_modulus("dft") 52 | check_factory_methods(ElasticModulus, data) 53 | -------------------------------------------------------------------------------- /tests/calculation/test_electronic_minimization.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | 4 | import types 5 | 6 | import numpy as np 7 | import pytest 8 | 9 | from py4vasp import exception 10 | from py4vasp._calculation.electronic_minimization import ElectronicMinimization 11 | 12 | 13 | @pytest.fixture 14 | def electronic_minimization(raw_data): 15 | raw_elmin = raw_data.electronic_minimization() 16 | constructor = ElectronicMinimization.from_data 17 | electronic_minimization = ElectronicMinimization.from_data(raw_elmin) 18 | electronic_minimization.ref = types.SimpleNamespace() 19 | convergence_data = raw_elmin.convergence_data 20 | electronic_minimization.ref.N = np.int64(convergence_data[:, 0]) 21 | electronic_minimization.ref.E = convergence_data[:, 1] 22 | electronic_minimization.ref.dE = convergence_data[:, 2] 23 | electronic_minimization.ref.deps = convergence_data[:, 3] 24 | electronic_minimization.ref.ncg = convergence_data[:, 4] 25 | electronic_minimization.ref.rms = convergence_data[:, 5] 26 | electronic_minimization.ref.rmsc = convergence_data[:, 6] 27 | is_elmin_converged = [raw_elmin.is_elmin_converged == [0.0]] 28 | electronic_minimization.ref.is_elmin_converged = is_elmin_converged 29 | string_rep = "N\t\tE\t\tdE\t\tdeps\t\tncg\trms\t\trms(c)\n" 30 | format_rep = "{0:g}\t{1:0.12E}\t{2:0.6E}\t{3:0.6E}\t{4:g}\t{5:0.3E}\t{6:0.3E}\n" 31 | for idx in range(len(convergence_data)): 32 | string_rep += format_rep.format(*convergence_data[idx]) 33 | electronic_minimization.ref.string_rep = str(string_rep) 34 | return electronic_minimization 35 | 36 | 37 | def test_read(electronic_minimization, Assert): 38 | actual = electronic_minimization.read() 39 | expected = electronic_minimization.ref 40 | Assert.allclose(actual["N"], expected.N) 41 | Assert.allclose(actual["E"], expected.E) 42 | Assert.allclose(actual["dE"], expected.dE) 43 | Assert.allclose(actual["deps"], expected.deps) 44 | Assert.allclose(actual["ncg"], expected.ncg) 45 | Assert.allclose(actual["rms"], expected.rms) 46 | Assert.allclose(actual["rms(c)"], expected.rmsc) 47 | 48 | 49 | @pytest.mark.parametrize( 50 | "quantity_name", ["N", "E", "dE", "deps", "ncg", "rms", "rms(c)"] 51 | ) 52 | def test_read_selection(quantity_name, electronic_minimization, Assert): 53 | actual = electronic_minimization.read(quantity_name) 54 | name_without_parenthesis = quantity_name.replace("(", "").replace(")", "") 55 | expected = getattr(electronic_minimization.ref, name_without_parenthesis) 56 | Assert.allclose(actual[quantity_name], expected) 57 | 58 | 59 | def test_read_incorrect_selection(electronic_minimization): 60 | with pytest.raises(exception.RefinementError): 61 | electronic_minimization.read("forces") 62 | 63 | 64 | def test_slice(electronic_minimization, Assert): 65 | actual = electronic_minimization[0:1].read() 66 | expected = electronic_minimization.ref 67 | Assert.allclose(actual["N"], expected.N) 68 | Assert.allclose(actual["E"], expected.E) 69 | Assert.allclose(actual["dE"], expected.dE) 70 | Assert.allclose(actual["deps"], expected.deps) 71 | Assert.allclose(actual["ncg"], expected.ncg) 72 | Assert.allclose(actual["rms"], expected.rms) 73 | Assert.allclose(actual["rms(c)"], expected.rmsc) 74 | 75 | 76 | def test_plot(electronic_minimization, Assert): 77 | graph = electronic_minimization.plot() 78 | assert graph.xlabel == "Iteration number" 79 | assert graph.ylabel == "E" 80 | assert len(graph.series) == 1 81 | Assert.allclose(graph.series[0].x, electronic_minimization.ref.N) 82 | Assert.allclose(graph.series[0].y, electronic_minimization.ref.E) 83 | 84 | 85 | def test_print(electronic_minimization, format_): 86 | actual, _ = format_(electronic_minimization) 87 | assert actual["text/plain"] == electronic_minimization.ref.string_rep 88 | 89 | 90 | def test_is_converged(electronic_minimization): 91 | actual = electronic_minimization.is_converged() 92 | expected = electronic_minimization.ref.is_elmin_converged 93 | assert actual == expected 94 | 95 | 96 | # def test_factory_methods(raw_data, check_factory_methods): 97 | # data = raw_data.electronic_minimization() 98 | # check_factory_methods(ElectronicMinimization, data) 99 | -------------------------------------------------------------------------------- /tests/calculation/test_exciton_density.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import types 4 | 5 | import numpy as np 6 | import pytest 7 | 8 | from py4vasp import _config, exception, raw 9 | from py4vasp._calculation.exciton_density import ExcitonDensity 10 | from py4vasp._calculation.structure import Structure 11 | 12 | 13 | @pytest.fixture 14 | def exciton_density(raw_data): 15 | raw_density = raw_data.exciton_density() 16 | density = ExcitonDensity.from_data(raw_density) 17 | density.ref = types.SimpleNamespace() 18 | density.ref.structure = Structure.from_data(raw_density.structure) 19 | expected_charge = [component.T for component in raw_density.exciton_charge] 20 | density.ref.density = np.array(expected_charge) 21 | return density 22 | 23 | 24 | @pytest.fixture 25 | def empty_density(raw_data): 26 | raw_density = raw.ExcitonDensity( 27 | raw_data.structure("Sr2TiO4"), exciton_charge=raw.VaspData(None) 28 | ) 29 | return ExcitonDensity.from_data(raw_density) 30 | 31 | 32 | def test_read(exciton_density, Assert): 33 | actual = exciton_density.read() 34 | actual_structure = actual.pop("structure") 35 | Assert.same_structure(actual_structure, exciton_density.ref.structure.read()) 36 | Assert.allclose(actual["charge"], exciton_density.ref.density) 37 | 38 | 39 | def test_missing_data(empty_density): 40 | with pytest.raises(exception.NoData): 41 | empty_density.read() 42 | 43 | 44 | def test_to_numpy(exciton_density, Assert): 45 | actual = exciton_density.to_numpy() 46 | Assert.allclose(actual, exciton_density.ref.density) 47 | 48 | 49 | @pytest.mark.parametrize("selection, indices", [(None, 0), ("2", 1), ("1, 3", (0, 2))]) 50 | def test_plot_selection(exciton_density, selection, indices, Assert): 51 | indices = np.atleast_1d(indices) 52 | if selection is None: 53 | view = exciton_density.plot() 54 | else: 55 | view = exciton_density.plot(selection) 56 | Assert.same_structure_view(view, exciton_density.ref.structure.plot()) 57 | assert len(view.grid_scalars) == len(indices) 58 | for grid_scalar, index in zip(view.grid_scalars, indices): 59 | selected_exciton = exciton_density.ref.density[index] 60 | assert grid_scalar.label == str(index + 1) 61 | assert grid_scalar.quantity.ndim == 4 62 | Assert.allclose(grid_scalar.quantity, selected_exciton) 63 | assert len(grid_scalar.isosurfaces) == 1 64 | isosurface = grid_scalar.isosurfaces[0] 65 | assert isosurface.isolevel == 0.8 66 | assert isosurface.color == _config.VASP_COLORS["cyan"] 67 | assert isosurface.opacity == 0.6 68 | 69 | 70 | def test_plot_addition(exciton_density, Assert): 71 | view = exciton_density.plot("1 + 3") 72 | assert len(view.grid_scalars) == 1 73 | grid_scalar = view.grid_scalars[0] 74 | selected_exciton = exciton_density.ref.density[0] + exciton_density.ref.density[2] 75 | assert grid_scalar.label == "1 + 3" 76 | Assert.allclose(grid_scalar.quantity, selected_exciton) 77 | 78 | 79 | def test_plot_centered(exciton_density, Assert): 80 | view = exciton_density.plot() 81 | assert view.shift is None 82 | view = exciton_density.plot(center=True) 83 | Assert.allclose(view.shift, 0.5) 84 | 85 | 86 | @pytest.mark.parametrize("supercell", (2, (3, 1, 2))) 87 | def test_plot_supercell(exciton_density, supercell, Assert): 88 | view = exciton_density.plot(supercell=supercell) 89 | Assert.allclose(view.supercell, supercell) 90 | 91 | 92 | def test_plot_user_options(exciton_density): 93 | view = exciton_density.plot(isolevel=0.4, color="red", opacity=0.5) 94 | assert len(view.grid_scalars) == 1 95 | grid_scalar = view.grid_scalars[0] 96 | assert len(grid_scalar.isosurfaces) == 1 97 | isosurface = grid_scalar.isosurfaces[0] 98 | assert isosurface.isolevel == 0.4 99 | assert isosurface.color == "red" 100 | assert isosurface.opacity == 0.5 101 | 102 | 103 | def test_print(exciton_density, format_): 104 | actual, _ = format_(exciton_density) 105 | expected_text = """\ 106 | exciton charge density: 107 | structure: Sr2TiO4 108 | grid: 10, 12, 14 109 | excitons: 3""" 110 | assert actual == {"text/plain": expected_text} 111 | 112 | 113 | def test_factory_methods(raw_data, check_factory_methods): 114 | data = raw_data.exciton_density() 115 | check_factory_methods(ExcitonDensity, data) 116 | -------------------------------------------------------------------------------- /tests/calculation/test_exciton_eigenvector.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import types 4 | 5 | import numpy as np 6 | import pytest 7 | 8 | from py4vasp._calculation._dispersion import Dispersion 9 | from py4vasp._calculation.exciton_eigenvector import ExcitonEigenvector 10 | 11 | 12 | @pytest.fixture 13 | def exciton_eigenvector(raw_data): 14 | raw_eigenvector = raw_data.exciton_eigenvector("default") 15 | eigenvector = ExcitonEigenvector.from_data(raw_eigenvector) 16 | eigenvector.ref = types.SimpleNamespace() 17 | eigenvector.ref.dispersion = Dispersion.from_data(raw_eigenvector.dispersion) 18 | eigenvectors = raw_eigenvector.eigenvectors 19 | eigenvector.ref.eigenvectors = eigenvectors[:, :, 0] + eigenvectors[:, :, 1] * 1j 20 | eigenvector.ref.fermi_energy = raw_eigenvector.fermi_energy 21 | # convert to Python indices 22 | eigenvector.ref.bse_index = raw_eigenvector.bse_index - 1 23 | eigenvector.ref.first_valence_band = raw_eigenvector.first_valence_band - 1 24 | eigenvector.ref.first_conduction_band = raw_eigenvector.first_conduction_band - 1 25 | return eigenvector 26 | 27 | 28 | def test_eigenvector_read(exciton_eigenvector, Assert): 29 | actual = exciton_eigenvector.read() 30 | dispersion = exciton_eigenvector.ref.dispersion.read() 31 | Assert.allclose(actual["kpoint_distances"], dispersion["kpoint_distances"]) 32 | assert "kpoint_labels" not in actual 33 | bands = dispersion["eigenvalues"] - exciton_eigenvector.ref.fermi_energy 34 | Assert.allclose(actual["bands"], bands) 35 | assert np.all(actual["bse_index"] == exciton_eigenvector.ref.bse_index) 36 | Assert.allclose(actual["eigenvectors"], exciton_eigenvector.ref.eigenvectors) 37 | Assert.allclose(actual["fermi_energy"], exciton_eigenvector.ref.fermi_energy) 38 | assert actual["first_valence_band"] == exciton_eigenvector.ref.first_valence_band 39 | assert ( 40 | actual["first_conduction_band"] == exciton_eigenvector.ref.first_conduction_band 41 | ) 42 | 43 | 44 | def test_eigenvector_print(exciton_eigenvector, format_): 45 | actual, _ = format_(exciton_eigenvector) 46 | reference = """\ 47 | BSE eigenvector data: 48 | 48 k-points 49 | 2 valence bands 50 | 1 conduction bands""" 51 | assert actual == {"text/plain": reference} 52 | 53 | 54 | def test_factory_methods(raw_data, check_factory_methods): 55 | data = raw_data.exciton_eigenvector("default") 56 | check_factory_methods(ExcitonEigenvector, data) 57 | -------------------------------------------------------------------------------- /tests/calculation/test_internal_strain.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import types 4 | 5 | import pytest 6 | 7 | from py4vasp._calculation.internal_strain import InternalStrain 8 | from py4vasp._calculation.structure import Structure 9 | 10 | 11 | @pytest.fixture 12 | def Sr2TiO4(raw_data): 13 | raw_internal_strain = raw_data.internal_strain("Sr2TiO4") 14 | internal_strain = InternalStrain.from_data(raw_internal_strain) 15 | internal_strain.ref = types.SimpleNamespace() 16 | structure = Structure.from_data(raw_internal_strain.structure) 17 | internal_strain.ref.structure = structure 18 | internal_strain.ref.internal_strain = raw_internal_strain.internal_strain 19 | return internal_strain 20 | 21 | 22 | def test_Sr2TiO4_read(Sr2TiO4, Assert): 23 | actual = Sr2TiO4.read() 24 | reference_structure = Sr2TiO4.ref.structure.read() 25 | for key in actual["structure"]: 26 | if key in ("elements", "names"): 27 | assert actual["structure"][key] == reference_structure[key] 28 | else: 29 | Assert.allclose(actual["structure"][key], reference_structure[key]) 30 | Assert.allclose(actual["internal_strain"], Sr2TiO4.ref.internal_strain) 31 | 32 | 33 | def test_Sr2TiO4_print(Sr2TiO4, format_): 34 | actual, _ = format_(Sr2TiO4) 35 | reference = """ 36 | Internal strain tensor (eV/Å): 37 | ion displ X Y Z XY YZ ZX 38 | --------------------------------------------------------------------------------- 39 | 1 x 0.00000 4.00000 8.00000 2.00000 6.00000 4.00000 40 | y 9.00000 13.00000 17.00000 11.00000 15.00000 13.00000 41 | z 18.00000 22.00000 26.00000 20.00000 24.00000 22.00000 42 | 2 x 27.00000 31.00000 35.00000 29.00000 33.00000 31.00000 43 | y 36.00000 40.00000 44.00000 38.00000 42.00000 40.00000 44 | z 45.00000 49.00000 53.00000 47.00000 51.00000 49.00000 45 | 3 x 54.00000 58.00000 62.00000 56.00000 60.00000 58.00000 46 | y 63.00000 67.00000 71.00000 65.00000 69.00000 67.00000 47 | z 72.00000 76.00000 80.00000 74.00000 78.00000 76.00000 48 | 4 x 81.00000 85.00000 89.00000 83.00000 87.00000 85.00000 49 | y 90.00000 94.00000 98.00000 92.00000 96.00000 94.00000 50 | z 99.00000 103.00000 107.00000 101.00000 105.00000 103.00000 51 | 5 x 108.00000 112.00000 116.00000 110.00000 114.00000 112.00000 52 | y 117.00000 121.00000 125.00000 119.00000 123.00000 121.00000 53 | z 126.00000 130.00000 134.00000 128.00000 132.00000 130.00000 54 | 6 x 135.00000 139.00000 143.00000 137.00000 141.00000 139.00000 55 | y 144.00000 148.00000 152.00000 146.00000 150.00000 148.00000 56 | z 153.00000 157.00000 161.00000 155.00000 159.00000 157.00000 57 | 7 x 162.00000 166.00000 170.00000 164.00000 168.00000 166.00000 58 | y 171.00000 175.00000 179.00000 173.00000 177.00000 175.00000 59 | z 180.00000 184.00000 188.00000 182.00000 186.00000 184.00000 60 | """.strip() 61 | assert actual == {"text/plain": reference} 62 | 63 | 64 | def test_factory_methods(raw_data, check_factory_methods): 65 | data = raw_data.internal_strain("Sr2TiO4") 66 | check_factory_methods(InternalStrain, data) 67 | -------------------------------------------------------------------------------- /tests/calculation/test_pair_correlation.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | from unittest.mock import patch 4 | 5 | import pytest 6 | 7 | from py4vasp import exception 8 | from py4vasp._calculation.pair_correlation import PairCorrelation 9 | 10 | 11 | @pytest.fixture 12 | def pair_correlation(raw_data): 13 | raw_pair_correlation = raw_data.pair_correlation("Sr2TiO4") 14 | pair_correlation = PairCorrelation.from_data(raw_pair_correlation) 15 | pair_correlation.ref = raw_pair_correlation 16 | return pair_correlation 17 | 18 | 19 | def test_read_default(pair_correlation, Assert): 20 | for steps in (slice(None), slice(1, 3), 0): 21 | actual = pair_correlation[steps].read() 22 | check_read_default(pair_correlation, actual, steps, Assert) 23 | check_read_default(pair_correlation, pair_correlation.read(), -1, Assert) 24 | 25 | 26 | def check_read_default(pair_correlation, dict_, steps, Assert): 27 | assert len(dict_) == len(pair_correlation.ref.labels) + 1 28 | Assert.allclose(dict_["distances"], pair_correlation.ref.distances) 29 | for i, label in enumerate(pair_correlation.ref.labels): 30 | Assert.allclose(dict_[label], pair_correlation.ref.function[steps, i]) 31 | 32 | 33 | def test_plot_default(pair_correlation, Assert): 34 | for steps in (0, slice(None), slice(1, 3)): 35 | actual = pair_correlation[steps].plot() 36 | check_plot_default(pair_correlation, actual, steps, Assert) 37 | check_plot_default(pair_correlation, pair_correlation.plot(), -1, Assert) 38 | 39 | 40 | def check_plot_default(pair_correlation, fig, steps, Assert): 41 | assert fig.xlabel == "Distance (Å)" 42 | assert fig.ylabel == "Pair correlation" 43 | assert fig.series[0].label == "total" 44 | Assert.allclose(fig.series[0].x, pair_correlation.ref.distances) 45 | Assert.allclose(fig.series[0].y, pair_correlation.ref.function[steps, 0]) 46 | 47 | 48 | def test_plot_selection(pair_correlation, Assert): 49 | selection = "Sr~Ti O~Ti" 50 | for steps in (0, slice(None), slice(1, 3)): 51 | actual = pair_correlation[steps].plot(selection) 52 | check_plot_selection(pair_correlation, actual, steps, Assert) 53 | check_plot_selection(pair_correlation, pair_correlation.plot(selection), -1, Assert) 54 | 55 | 56 | def check_plot_selection(pair_correlation, fig, steps, Assert): 57 | assert fig.xlabel == "Distance (Å)" 58 | assert fig.ylabel == "Pair correlation" 59 | expected = {"Sr~Ti": 2, "Ti~O": 5} # note the reordering of the label 60 | for series, (label, index) in zip(fig.series, expected.items()): 61 | assert series.label == label 62 | Assert.allclose(series.x, pair_correlation.ref.distances) 63 | Assert.allclose(series.y, pair_correlation.ref.function[steps, index]) 64 | 65 | 66 | def test_labels(pair_correlation): 67 | assert pair_correlation.labels() == pair_correlation.ref.labels 68 | 69 | 70 | def test_plot_nonexisting_label(pair_correlation): 71 | with pytest.raises(exception.IncorrectUsage): 72 | pair_correlation.plot("label does exist") 73 | 74 | 75 | @patch.object(PairCorrelation, "to_graph") 76 | def test_pair_correlation_to_plotly(mock_plot, pair_correlation): 77 | fig = pair_correlation.to_plotly("selection") 78 | mock_plot.assert_called_once_with("selection") 79 | graph = mock_plot.return_value 80 | graph.to_plotly.assert_called_once() 81 | assert fig == graph.to_plotly.return_value 82 | 83 | 84 | def test_to_image(pair_correlation): 85 | check_to_image(pair_correlation, None, "pair_correlation.png") 86 | custom_filename = "custom.jpg" 87 | check_to_image(pair_correlation, custom_filename, custom_filename) 88 | 89 | 90 | def check_to_image(pair_correlation, filename_argument, expected_filename): 91 | with patch.object(PairCorrelation, "to_plotly") as plot: 92 | pair_correlation.to_image("args", filename=filename_argument, key="word") 93 | plot.assert_called_once_with("args", key="word") 94 | fig = plot.return_value 95 | expected_path = pair_correlation.path / expected_filename 96 | fig.write_image.assert_called_once_with(expected_path) 97 | 98 | 99 | def test_factory_methods(raw_data, check_factory_methods): 100 | data = raw_data.pair_correlation("Sr2TiO4") 101 | check_factory_methods(PairCorrelation, data) 102 | -------------------------------------------------------------------------------- /tests/calculation/test_phonon_dos.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import types 4 | from unittest.mock import patch 5 | 6 | import numpy as np 7 | import pytest 8 | 9 | from py4vasp._calculation.phonon_dos import PhononDos 10 | 11 | 12 | @pytest.fixture 13 | def phonon_dos(raw_data): 14 | raw_dos = raw_data.phonon_dos("default") 15 | dos = PhononDos.from_data(raw_dos) 16 | dos.ref = types.SimpleNamespace() 17 | dos.ref.energies = raw_dos.energies 18 | dos.ref.total_dos = raw_dos.dos 19 | dos.ref.Sr = np.sum(raw_dos.projections[0:2], axis=(0, 1)) 20 | dos.ref.Ti_x = raw_dos.projections[2, 0] 21 | dos.ref.y_45 = np.sum(raw_dos.projections[3:5, 1], axis=0) 22 | dos.ref.z = np.sum(raw_dos.projections[:, 2], axis=0) 23 | return dos 24 | 25 | 26 | def test_phonon_dos_read(phonon_dos, Assert): 27 | actual = phonon_dos.read() 28 | Assert.allclose(actual["energies"], phonon_dos.ref.energies) 29 | Assert.allclose(actual["total"], phonon_dos.ref.total_dos) 30 | assert "Sr" not in actual 31 | 32 | 33 | def test_phonon_dos_read_projection(phonon_dos, Assert): 34 | actual = phonon_dos.read("Sr, 3(x), y(4:5), z, Sr - Ti(x)") 35 | assert "total" in actual 36 | Assert.allclose(actual["Sr"], phonon_dos.ref.Sr) 37 | Assert.allclose(actual["Ti_1_x"], phonon_dos.ref.Ti_x) 38 | Assert.allclose(actual["4:5_y"], phonon_dos.ref.y_45) 39 | Assert.allclose(actual["z"], phonon_dos.ref.z) 40 | subtraction = phonon_dos.ref.Sr - phonon_dos.ref.Ti_x 41 | Assert.allclose(actual["Sr - Ti_x"], subtraction) 42 | 43 | 44 | def test_phonon_dos_plot(phonon_dos, Assert): 45 | graph = phonon_dos.plot() 46 | assert graph.xlabel == "ω (THz)" 47 | assert graph.ylabel == "DOS (1/THz)" 48 | assert len(graph.series) == 1 49 | Assert.allclose(graph.series[0].x, phonon_dos.ref.energies) 50 | Assert.allclose(graph.series[0].y, phonon_dos.ref.total_dos) 51 | 52 | 53 | def test_phonon_dos_plot_selection(phonon_dos, Assert): 54 | graph = phonon_dos.plot("Sr, 3(x), y(4:5), z") 55 | assert len(graph.series) == 5 56 | check_series(graph.series[0], phonon_dos.ref.total_dos, "total", Assert) 57 | check_series(graph.series[1], phonon_dos.ref.Sr, "Sr", Assert) 58 | check_series(graph.series[2], phonon_dos.ref.Ti_x, "Ti_1_x", Assert) 59 | check_series(graph.series[3], phonon_dos.ref.y_45, "4:5_y", Assert) 60 | check_series(graph.series[4], phonon_dos.ref.z, "z", Assert) 61 | 62 | 63 | def check_series(series, reference, label, Assert): 64 | assert series.label == label 65 | Assert.allclose(series.y, reference) 66 | 67 | 68 | @patch.object(PhononDos, "to_graph") 69 | def test_phonon_dos_to_plotly(mock_plot, phonon_dos): 70 | fig = phonon_dos.to_plotly("selection") 71 | mock_plot.assert_called_once_with("selection") 72 | graph = mock_plot.return_value 73 | graph.to_plotly.assert_called_once() 74 | assert fig == graph.to_plotly.return_value 75 | 76 | 77 | def test_phonon_dos_to_image(phonon_dos): 78 | check_to_image(phonon_dos, None, "phonon_dos.png") 79 | custom_filename = "custom.jpg" 80 | check_to_image(phonon_dos, custom_filename, custom_filename) 81 | 82 | 83 | def check_to_image(phonon_dos, filename_argument, expected_filename): 84 | with patch.object(PhononDos, "to_plotly") as plot: 85 | phonon_dos.to_image("args", filename=filename_argument, key="word") 86 | plot.assert_called_once_with("args", key="word") 87 | fig = plot.return_value 88 | fig.write_image.assert_called_once_with(phonon_dos._path / expected_filename) 89 | 90 | 91 | def test_selections(phonon_dos): 92 | assert phonon_dos.selections() == { 93 | "atom": ["Sr", "Ti", "O", "1", "2", "3", "4", "5", "6", "7"], 94 | "direction": ["x", "y", "z"], 95 | } 96 | 97 | 98 | def test_phonon_dos_print(phonon_dos, format_): 99 | actual, _ = format_(phonon_dos) 100 | reference = """\ 101 | phonon DOS: 102 | [0.00, 5.00] mesh with 50 points 103 | 21 modes 104 | Sr2TiO4""" 105 | assert actual == {"text/plain": reference} 106 | 107 | 108 | def test_factory_methods(raw_data, check_factory_methods): 109 | data = raw_data.phonon_dos("default") 110 | check_factory_methods(PhononDos, data) 111 | -------------------------------------------------------------------------------- /tests/calculation/test_phonon_mode.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import types 4 | 5 | import numpy as np 6 | import pytest 7 | 8 | from py4vasp._calculation.phonon_mode import PhononMode 9 | from py4vasp._calculation.structure import Structure 10 | 11 | 12 | @pytest.fixture 13 | def phonon_mode(raw_data): 14 | raw_mode = raw_data.phonon_mode("default") 15 | mode = PhononMode.from_data(raw_mode) 16 | mode.ref = types.SimpleNamespace() 17 | mode.ref.structure = Structure.from_data(raw_mode.structure) 18 | mode.ref.frequencies = raw_mode.frequencies.flatten().view(np.complex128) 19 | mode.ref.eigenvectors = raw_mode.eigenvectors 20 | return mode 21 | 22 | 23 | def test_read(phonon_mode, Assert): 24 | actual = phonon_mode.read() 25 | Assert.same_structure(actual["structure"], phonon_mode.ref.structure.read()) 26 | Assert.allclose(actual["frequencies"], phonon_mode.ref.frequencies) 27 | Assert.allclose(actual["eigenvectors"], phonon_mode.ref.eigenvectors) 28 | 29 | 30 | def test_frequencies(phonon_mode, Assert): 31 | Assert.allclose(phonon_mode.frequencies(), phonon_mode.ref.frequencies) 32 | 33 | 34 | def test_print(phonon_mode, format_): 35 | actual, _ = format_(phonon_mode) 36 | expected_text = """\ 37 | Eigenvalues of the dynamical matrix 38 | ----------------------------------- 39 | 1 f = 76.463537 THz 480.434572 2PiTHz 2550.569965 cm-1 316.227766 meV 40 | 2 f = 74.134150 THz 465.798600 2PiTHz 2472.869329 cm-1 306.594194 meV 41 | 3 f = 71.729156 THz 450.687578 2PiTHz 2392.646712 cm-1 296.647939 meV 42 | 4 f = 69.240678 THz 435.052008 2PiTHz 2309.639335 cm-1 286.356421 meV 43 | 5 f = 66.659366 THz 418.833150 2PiTHz 2223.535345 cm-1 275.680975 meV 44 | 6 f = 63.973985 THz 401.960402 2PiTHz 2133.959934 cm-1 264.575131 meV 45 | 7 f = 61.170830 THz 384.347658 2PiTHz 2040.455972 cm-1 252.982213 meV 46 | 8 f = 58.232895 THz 365.888069 2PiTHz 1942.456214 cm-1 240.831892 meV 47 | 9 f = 55.138641 THz 346.446297 2PiTHz 1839.242158 cm-1 228.035085 meV 48 | 10 f = 51.860094 THz 325.846580 2PiTHz 1729.880715 cm-1 214.476106 meV 49 | 11 f = 48.359787 THz 303.853503 2PiTHz 1613.122084 cm-1 200.000000 meV 50 | 12 f = 44.585521 THz 280.139088 2PiTHz 1487.225077 cm-1 184.390889 meV 51 | 13 f = 40.460701 THz 254.222080 2PiTHz 1349.634766 cm-1 167.332005 meV 52 | 14 f = 35.864578 THz 225.343789 2PiTHz 1196.323356 cm-1 148.323970 meV 53 | 15 f = 30.585415 THz 192.173829 2PiTHz 1020.227986 cm-1 126.491106 meV 54 | 16 f = 24.179893 THz 151.926751 2PiTHz 806.561042 cm-1 100.000000 meV 55 | 17 f = 15.292707 THz 96.086914 2PiTHz 510.113993 cm-1 63.245553 meV 56 | 18 f/i= 10.813577 THz 67.943709 2PiTHz 360.705064 cm-1 44.721360 meV 57 | 19 f/i= 21.627154 THz 135.887418 2PiTHz 721.410127 cm-1 89.442719 meV 58 | 20 f/i= 28.610036 THz 179.762157 2PiTHz 954.335895 cm-1 118.321596 meV 59 | 21 f/i= 34.195533 THz 214.856872 2PiTHz 1140.649564 cm-1 141.421356 meV 60 | """ 61 | assert actual == {"text/plain": expected_text} 62 | 63 | 64 | def test_factory_methods(raw_data, check_factory_methods): 65 | data = raw_data.phonon_mode("Sr2TiO4") 66 | check_factory_methods(PhononMode, data) 67 | -------------------------------------------------------------------------------- /tests/calculation/test_piezoelectric_tensor.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import types 4 | 5 | import pytest 6 | 7 | from py4vasp._calculation.piezoelectric_tensor import PiezoelectricTensor 8 | 9 | 10 | @pytest.fixture 11 | def piezoelectric_tensor(raw_data): 12 | raw_tensor = raw_data.piezoelectric_tensor("default") 13 | tensor = PiezoelectricTensor.from_data(raw_tensor) 14 | tensor.ref = types.SimpleNamespace() 15 | tensor.ref.clamped_ion = raw_tensor.electron 16 | tensor.ref.relaxed_ion = raw_tensor.ion + raw_tensor.electron 17 | return tensor 18 | 19 | 20 | def test_read(piezoelectric_tensor, Assert): 21 | actual = piezoelectric_tensor.read() 22 | Assert.allclose(actual["clamped_ion"], piezoelectric_tensor.ref.clamped_ion) 23 | Assert.allclose(actual["relaxed_ion"], piezoelectric_tensor.ref.relaxed_ion) 24 | 25 | 26 | def test_print(piezoelectric_tensor, format_): 27 | actual, _ = format_(piezoelectric_tensor) 28 | reference = f""" 29 | Piezoelectric tensor (C/m²) 30 | XX YY ZZ XY YZ ZX 31 | --------------------------------------------------------------------------- 32 | clamped-ion 33 | x 0.00000 4.00000 8.00000 2.00000 6.00000 4.00000 34 | y 9.00000 13.00000 17.00000 11.00000 15.00000 13.00000 35 | z 18.00000 22.00000 26.00000 20.00000 24.00000 22.00000 36 | relaxed-ion 37 | x 27.00000 35.00000 43.00000 31.00000 39.00000 35.00000 38 | y 45.00000 53.00000 61.00000 49.00000 57.00000 53.00000 39 | z 63.00000 71.00000 79.00000 67.00000 75.00000 71.00000 40 | """.strip() 41 | assert actual == {"text/plain": reference} 42 | 43 | 44 | def test_factory_methods(raw_data, check_factory_methods): 45 | data = raw_data.piezoelectric_tensor("default") 46 | check_factory_methods(PiezoelectricTensor, data) 47 | -------------------------------------------------------------------------------- /tests/calculation/test_polarization.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import types 4 | 5 | import pytest 6 | 7 | from py4vasp._calculation.polarization import Polarization 8 | 9 | 10 | @pytest.fixture 11 | def polarization(raw_data): 12 | raw_polarization = raw_data.polarization("default") 13 | polarization = Polarization.from_data(raw_polarization) 14 | polarization.ref = types.SimpleNamespace() 15 | polarization.ref.ion_dipole = raw_polarization.ion 16 | polarization.ref.electron_dipole = raw_polarization.electron 17 | return polarization 18 | 19 | 20 | def test_read(polarization, Assert): 21 | actual = polarization.read() 22 | Assert.allclose(actual["ion_dipole"], polarization.ref.ion_dipole) 23 | Assert.allclose(actual["electron_dipole"], polarization.ref.electron_dipole) 24 | 25 | 26 | def test_print(polarization, format_): 27 | actual, _ = format_(polarization) 28 | reference = f""" 29 | Polarization (|e|Å) 30 | ------------------------------------------------------------- 31 | ionic dipole moment: 4.00000 5.00000 6.00000 32 | electronic dipole moment: 1.00000 2.00000 3.00000 33 | """.strip() 34 | assert actual == {"text/plain": reference} 35 | 36 | 37 | def test_factory_methods(raw_data, check_factory_methods): 38 | data = raw_data.polarization("default") 39 | check_factory_methods(Polarization, data) 40 | -------------------------------------------------------------------------------- /tests/calculation/test_repr.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import importlib 4 | from pathlib import PosixPath, WindowsPath # these are required for the eval operation 5 | 6 | from py4vasp import _calculation, calculation 7 | from py4vasp._util import convert 8 | 9 | 10 | def test_repr(): 11 | for quantity in _calculation.QUANTITIES: 12 | instance = getattr(calculation, quantity) 13 | check_repr_is_consistent(instance, quantity) 14 | for group, quantities in _calculation.GROUPS.items(): 15 | namespace = getattr(calculation, group) 16 | for quantity in quantities: 17 | instance = getattr(namespace, quantity) 18 | check_repr_is_consistent(instance, f"{group}_{quantity}") 19 | 20 | 21 | def check_repr_is_consistent(instance, quantity): 22 | class_name = convert.to_camelcase(quantity) 23 | module = importlib.import_module(f"py4vasp._calculation.{quantity}") 24 | locals()[class_name] = getattr(module, class_name) 25 | copy = eval(repr(instance)) 26 | assert copy.__class__ == instance.__class__ 27 | -------------------------------------------------------------------------------- /tests/calculation/test_stress.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import types 4 | 5 | import pytest 6 | 7 | from py4vasp import exception 8 | from py4vasp._calculation.stress import Stress 9 | from py4vasp._calculation.structure import Structure 10 | 11 | 12 | @pytest.fixture 13 | def Sr2TiO4(raw_data): 14 | raw_stress = raw_data.stress("Sr2TiO4") 15 | stress = Stress.from_data(raw_stress) 16 | stress.ref = types.SimpleNamespace() 17 | stress.ref.structure = Structure.from_data(raw_stress.structure) 18 | stress.ref.stress = raw_stress.stress 19 | return stress 20 | 21 | 22 | @pytest.fixture 23 | def Fe3O4(raw_data): 24 | raw_stress = raw_data.stress("Fe3O4") 25 | stress = Stress.from_data(raw_stress) 26 | stress.ref = types.SimpleNamespace() 27 | stress.ref.structure = Structure.from_data(raw_stress.structure) 28 | stress.ref.stress = raw_stress.stress 29 | return stress 30 | 31 | 32 | def test_read_Sr2TiO4(Sr2TiO4, Assert): 33 | check_read_stress(Sr2TiO4.read(), Sr2TiO4.ref, -1, Assert) 34 | for steps in (slice(None), slice(1, 3), 0): 35 | check_read_stress(Sr2TiO4[steps].read(), Sr2TiO4.ref, steps, Assert) 36 | 37 | 38 | def test_read_Fe3O4(Fe3O4, Assert): 39 | check_read_stress(Fe3O4.read(), Fe3O4.ref, -1, Assert) 40 | for steps in (slice(None), slice(1, 3), 0): 41 | check_read_stress(Fe3O4[steps].read(), Fe3O4.ref, steps, Assert) 42 | 43 | 44 | def check_read_stress(actual, reference, steps, Assert): 45 | reference_structure = reference.structure[steps].read() 46 | for key in actual["structure"]: 47 | if key in ("elements", "names"): 48 | assert actual["structure"][key] == reference_structure[key] 49 | else: 50 | Assert.allclose(actual["structure"][key], reference_structure[key]) 51 | Assert.allclose(actual["stress"], reference.stress[steps]) 52 | 53 | 54 | def test_incorrect_access(Sr2TiO4): 55 | out_of_bounds = 999 56 | with pytest.raises(exception.IncorrectUsage): 57 | Sr2TiO4[out_of_bounds].read() 58 | with pytest.raises(exception.IncorrectUsage): 59 | Sr2TiO4["string instead of int"].read() 60 | 61 | 62 | def test_print_Sr2TiO4(Sr2TiO4, format_): 63 | actual, _ = format_(Sr2TiO4) 64 | ref_plain = """ 65 | FORCE on cell =-STRESS in cart. coord. units (eV): 66 | Direction XX YY ZZ XY YZ ZX 67 | ------------------------------------------------------------------------------------- 68 | Total 1.64862 1.89286 2.13710 1.77074 2.01498 1.89286 69 | in kB 27.00000 31.00000 35.00000 29.00000 33.00000 31.00000 70 | """.strip() 71 | assert actual == {"text/plain": ref_plain} 72 | # 73 | actual, _ = format_(Sr2TiO4[0]) 74 | ref_plain = """ 75 | FORCE on cell =-STRESS in cart. coord. units (eV): 76 | Direction XX YY ZZ XY YZ ZX 77 | ------------------------------------------------------------------------------------- 78 | Total 0.00000 0.24424 0.48848 0.12212 0.36636 0.24424 79 | in kB 0.00000 4.00000 8.00000 2.00000 6.00000 4.00000 80 | """.strip() 81 | assert actual == {"text/plain": ref_plain} 82 | # 83 | actual, _ = format_(Sr2TiO4[1:3]) 84 | ref_plain = """ 85 | FORCE on cell =-STRESS in cart. coord. units (eV): 86 | Direction XX YY ZZ XY YZ ZX 87 | ------------------------------------------------------------------------------------- 88 | Total 1.09908 1.34332 1.58756 1.22120 1.46544 1.34332 89 | in kB 18.00000 22.00000 26.00000 20.00000 24.00000 22.00000 90 | """.strip() 91 | assert actual == {"text/plain": ref_plain} 92 | 93 | 94 | def test_factory_methods(raw_data, check_factory_methods): 95 | data = raw_data.stress("Sr2TiO4") 96 | check_factory_methods(Stress, data) 97 | -------------------------------------------------------------------------------- /tests/calculation/test_system.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import string 4 | 5 | import pytest 6 | 7 | from py4vasp import raw 8 | from py4vasp._calculation.system import System 9 | from py4vasp._util.convert import text_to_string 10 | 11 | 12 | @pytest.fixture 13 | def string_format(): 14 | return raw.System("string format") 15 | 16 | 17 | @pytest.fixture 18 | def byte_format(): 19 | return raw.System(b"byte format") 20 | 21 | 22 | def test_system_read(string_format, byte_format): 23 | check_system_read(string_format) 24 | check_system_read(byte_format) 25 | 26 | 27 | def check_system_read(raw_system): 28 | expected = {"system": text_to_string(raw_system.system)} 29 | assert System.from_data(raw_system).read() == expected 30 | 31 | 32 | def test_system_print(string_format, byte_format, format_): 33 | check_system_print(string_format, format_) 34 | check_system_print(byte_format, format_) 35 | 36 | 37 | def check_system_print(raw_system, format_): 38 | system = System.from_data(raw_system) 39 | actual, _ = format_(system) 40 | assert actual["text/plain"] == text_to_string(raw_system.system) 41 | 42 | 43 | def test_factory_methods(string_format, check_factory_methods): 44 | check_factory_methods(System, string_format) 45 | -------------------------------------------------------------------------------- /tests/calculation/test_velocity.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import types 4 | 5 | import pytest 6 | 7 | from py4vasp import _config, exception 8 | from py4vasp._calculation.structure import Structure 9 | from py4vasp._calculation.velocity import Velocity 10 | 11 | 12 | @pytest.fixture 13 | def Sr2TiO4(raw_data): 14 | return create_velocity_data(raw_data, "Sr2TiO4") 15 | 16 | 17 | @pytest.fixture(params=["Sr2TiO4", "Fe3O4"]) 18 | def velocities(raw_data, request): 19 | return create_velocity_data(raw_data, request.param) 20 | 21 | 22 | @pytest.fixture(params=[-1, 0, slice(None), slice(1, 3)]) 23 | def steps(request): 24 | return request.param 25 | 26 | 27 | def create_velocity_data(raw_data, structure): 28 | raw_velocity = raw_data.velocity(structure) 29 | velocity = Velocity.from_data(raw_velocity) 30 | velocity.ref = types.SimpleNamespace() 31 | velocity.ref.structure = Structure.from_data(raw_velocity.structure) 32 | velocity.ref.velocities = raw_velocity.velocities 33 | return velocity 34 | 35 | 36 | def test_read(velocities, steps, Assert): 37 | actual = velocities.read() if steps == -1 else velocities[steps].read() 38 | reference_structure = velocities.ref.structure[steps].read() 39 | Assert.same_structure(actual["structure"], reference_structure) 40 | Assert.allclose(actual["velocities"], velocities.ref.velocities[steps]) 41 | 42 | 43 | @pytest.mark.parametrize("supercell", [None, 2, (3, 2, 1)]) 44 | def test_plot(velocities, steps, supercell, Assert): 45 | structure_view = velocities.ref.structure.plot(supercell) 46 | plot_method = velocities.plot if steps == -1 else velocities[steps].plot 47 | view = plot_method(supercell) if supercell else plot_method() 48 | Assert.same_structure_view(view, structure_view) 49 | assert len(view.ion_arrows) == 1 50 | arrows = view.ion_arrows[0] 51 | assert arrows.quantity.ndim == 3 52 | expected_velocities = velocities.velocity_rescale * velocities.ref.velocities[steps] 53 | Assert.allclose(arrows.quantity, expected_velocities) 54 | assert arrows.label == "velocities" 55 | assert arrows.color == _config.VASP_COLORS["gray"] 56 | assert arrows.radius == 0.2 57 | 58 | 59 | def test_incorrect_access(Sr2TiO4): 60 | out_of_bounds = 999 61 | with pytest.raises(exception.IncorrectUsage): 62 | Sr2TiO4[out_of_bounds].read() 63 | with pytest.raises(exception.IncorrectUsage): 64 | Sr2TiO4["string instead of int"].read() 65 | with pytest.raises(exception.IncorrectUsage): 66 | Sr2TiO4[out_of_bounds].plot() 67 | with pytest.raises(exception.IncorrectUsage): 68 | Sr2TiO4["string instead of int"].plot() 69 | 70 | 71 | def test_print_Sr2TiO4(Sr2TiO4, format_): 72 | actual, _ = format_(Sr2TiO4) 73 | ref_plain = f"""{Sr2TiO4.ref.structure} 74 | 75 | 63.0000000000000000 64.0000000000000000 65.0000000000000000 76 | 66.0000000000000000 67.0000000000000000 68.0000000000000000 77 | 69.0000000000000000 70.0000000000000000 71.0000000000000000 78 | 72.0000000000000000 73.0000000000000000 74.0000000000000000 79 | 75.0000000000000000 76.0000000000000000 77.0000000000000000 80 | 78.0000000000000000 79.0000000000000000 80.0000000000000000 81 | 81.0000000000000000 82.0000000000000000 83.0000000000000000""" 82 | assert actual == {"text/plain": ref_plain} 83 | # 84 | actual, _ = format_(Sr2TiO4[0]) 85 | ref_plain = f"""{Sr2TiO4.ref.structure[0]} 86 | 87 | 0.0000000000000000 1.0000000000000000 2.0000000000000000 88 | 3.0000000000000000 4.0000000000000000 5.0000000000000000 89 | 6.0000000000000000 7.0000000000000000 8.0000000000000000 90 | 9.0000000000000000 10.0000000000000000 11.0000000000000000 91 | 12.0000000000000000 13.0000000000000000 14.0000000000000000 92 | 15.0000000000000000 16.0000000000000000 17.0000000000000000 93 | 18.0000000000000000 19.0000000000000000 20.0000000000000000""" 94 | assert actual == {"text/plain": ref_plain} 95 | # 96 | actual, _ = format_(Sr2TiO4[1:3]) 97 | ref_plain = f"""{Sr2TiO4.ref.structure[2]} 98 | 99 | 42.0000000000000000 43.0000000000000000 44.0000000000000000 100 | 45.0000000000000000 46.0000000000000000 47.0000000000000000 101 | 48.0000000000000000 49.0000000000000000 50.0000000000000000 102 | 51.0000000000000000 52.0000000000000000 53.0000000000000000 103 | 54.0000000000000000 55.0000000000000000 56.0000000000000000 104 | 57.0000000000000000 58.0000000000000000 59.0000000000000000 105 | 60.0000000000000000 61.0000000000000000 62.0000000000000000""" 106 | assert actual == {"text/plain": ref_plain} 107 | 108 | 109 | def test_factory_methods(raw_data, check_factory_methods): 110 | data = raw_data.velocity("Fe3O4") 111 | check_factory_methods(Velocity, data) 112 | -------------------------------------------------------------------------------- /tests/calculation/test_workfunction.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | from unittest.mock import patch 4 | 5 | import pytest 6 | 7 | from py4vasp._calculation.workfunction import Workfunction 8 | 9 | 10 | @pytest.fixture(params=[1, 2, 3]) 11 | def workfunction(raw_data, request): 12 | raw_workfunction = raw_data.workfunction(str(request.param)) 13 | return setup_reference(raw_workfunction) 14 | 15 | 16 | def setup_reference(raw_workfunction): 17 | workfunction = Workfunction.from_data(raw_workfunction) 18 | workfunction.ref = raw_workfunction 19 | raw_gap = raw_workfunction.reference_potential 20 | workfunction.ref.lattice_vector = f"lattice vector {raw_workfunction.idipol}" 21 | workfunction.ref.vbm = raw_gap.values[-1, 0, 0] 22 | workfunction.ref.cbm = raw_gap.values[-1, 0, 1] 23 | return workfunction 24 | 25 | 26 | def test_read(workfunction, Assert): 27 | actual = workfunction.read() 28 | actual["direction"] == workfunction.ref.lattice_vector 29 | Assert.allclose(actual["distance"], workfunction.ref.distance) 30 | Assert.allclose(actual["average_potential"], workfunction.ref.average_potential) 31 | Assert.allclose(actual["vacuum_potential"], workfunction.ref.vacuum_potential) 32 | # Uncomment out these lines when vbm and cbm are added to VASP 6.5 33 | Assert.allclose(actual["valence_band_maximum"], workfunction.ref.vbm) 34 | Assert.allclose(actual["conduction_band_minimum"], workfunction.ref.cbm) 35 | Assert.allclose(actual["fermi_energy"], workfunction.ref.fermi_energy) 36 | 37 | 38 | def test_plot(workfunction, Assert): 39 | graph = workfunction.plot() 40 | assert graph.xlabel == f"distance along {workfunction.ref.lattice_vector} (Å)" 41 | assert graph.ylabel == "average potential (eV)" 42 | Assert.allclose(graph.series.x, workfunction.ref.distance) 43 | Assert.allclose(graph.series.y, workfunction.ref.average_potential) 44 | assert graph.series.label == "potential" 45 | 46 | 47 | @patch.object(Workfunction, "to_graph") 48 | def test_to_plotly(mock_plot, workfunction): 49 | fig = workfunction.to_plotly() 50 | mock_plot.assert_called_once_with() 51 | graph = mock_plot.return_value 52 | graph.to_plotly.assert_called_once() 53 | assert fig == graph.to_plotly.return_value 54 | 55 | 56 | def test_to_image(workfunction): 57 | check_to_image(workfunction, None, "workfunction.png") 58 | custom_filename = "custom.jpg" 59 | check_to_image(workfunction, custom_filename, custom_filename) 60 | 61 | 62 | def check_to_image(workfunction, filename_argument, expected_filename): 63 | with patch.object(Workfunction, "to_plotly") as plot: 64 | workfunction.to_image("args", filename=filename_argument, key="word") 65 | plot.assert_called_once_with("args", key="word") 66 | fig = plot.return_value 67 | fig.write_image.assert_called_once_with(workfunction._path / expected_filename) 68 | 69 | 70 | def test_print(workfunction, format_): 71 | actual, _ = format_(workfunction) 72 | reference = """\ 73 | workfunction along {lattice_vector}: 74 | vacuum potential: {vacuum1:.3f} {vacuum2:.3f} 75 | Fermi energy: {fermi_energy:.3f} 76 | valence band maximum: {vbm:.3f} 77 | conduction band minimum: {cbm:.3f}""" 78 | reference = reference.format( 79 | lattice_vector=workfunction.ref.lattice_vector, 80 | vacuum1=workfunction.ref.vacuum_potential[0], 81 | vacuum2=workfunction.ref.vacuum_potential[1], 82 | fermi_energy=workfunction.ref.fermi_energy, 83 | vbm=workfunction.ref.vbm, 84 | cbm=workfunction.ref.cbm, 85 | ) 86 | assert actual == {"text/plain": reference} 87 | 88 | 89 | def test_factory_methods(raw_data, check_factory_methods): 90 | raw_workfunction = raw_data.workfunction("1") 91 | check_factory_methods(Workfunction, raw_workfunction) 92 | -------------------------------------------------------------------------------- /tests/cli/test_cli.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import pathlib 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | from click.testing import CliRunner 8 | 9 | from py4vasp import exception 10 | from py4vasp.cli import cli 11 | 12 | 13 | @pytest.fixture 14 | def mock_calculation(): 15 | with patch("py4vasp.Calculation", autospec=True) as mock: 16 | yield mock 17 | 18 | 19 | @pytest.mark.parametrize("lammps", ("LAMMPS", "Lammps", "lammps")) 20 | def test_convert_lammps(mock_calculation, lammps): 21 | runner = CliRunner() 22 | result = runner.invoke(cli, ["convert", "structure", lammps]) 23 | assert result.exit_code == 0 24 | check_conversion_called(mock_calculation, result) 25 | 26 | 27 | @pytest.mark.parametrize("position", ("first", "middle", "last")) 28 | @pytest.mark.parametrize("selection", (("-s", "choice"), ("--selection", "choice"))) 29 | def test_convert_selection(mock_calculation, position, selection): 30 | runner = CliRunner() 31 | result = invoke_runner_with_options(runner, position, selection) 32 | check_conversion_called(mock_calculation, result, selection=selection[1]) 33 | 34 | 35 | @pytest.mark.parametrize("position", ("first", "middle", "last")) 36 | @pytest.mark.parametrize("argument", ("-f", "--from")) 37 | @pytest.mark.parametrize("path", ("dirname", "filename")) 38 | def test_convert_path(mock_calculation, position, argument, path, tmp_path): 39 | expected_path = tmp_path / path 40 | if path == "dirname": 41 | expected_path.mkdir() 42 | else: 43 | expected_path.touch() 44 | runner = CliRunner() 45 | result = invoke_runner_with_options(runner, position, (argument, expected_path)) 46 | check_conversion_called(mock_calculation, result, expected_path=expected_path) 47 | 48 | 49 | def invoke_runner_with_options(runner, position, options): 50 | if position == "first": 51 | return runner.invoke(cli, ["convert", *options, "structure", "lammps"]) 52 | elif position == "middle": 53 | return runner.invoke(cli, ["convert", "structure", *options, "lammps"]) 54 | elif position == "last": 55 | return runner.invoke(cli, ["convert", "structure", "lammps", *options]) 56 | else: 57 | raise NotImplementedError 58 | 59 | 60 | def check_conversion_called( 61 | mock_calculation, result, selection=None, expected_path=pathlib.Path.cwd() 62 | ): 63 | assert result.exit_code == 0 64 | if expected_path.name == "filename": 65 | constructor = mock_calculation.from_file 66 | else: 67 | constructor = mock_calculation.from_path 68 | constructor.assert_called_once_with(expected_path) 69 | structure = constructor.return_value.structure 70 | if selection is None: 71 | structure.to_lammps.assert_called_once_with() 72 | else: 73 | structure.to_lammps.assert_called_once_with(selection=selection) 74 | converted = structure.to_lammps.return_value 75 | assert f"{converted}\n" == result.output 76 | 77 | 78 | def test_convert_wrong_quantity(): 79 | runner = CliRunner() 80 | result = runner.invoke(cli, ["convert", "not_implemented"]) 81 | assert result.exit_code != 0 82 | assert "Invalid value" in result.output 83 | assert "not_implemented" in result.output 84 | 85 | 86 | def test_convert_wrong_format(mock_calculation): 87 | runner = CliRunner() 88 | result = runner.invoke(cli, ["convert", "structure", "not_implemented"]) 89 | assert result.exit_code != 0 90 | mock_calculation.from_path.assert_not_called() 91 | 92 | 93 | def test_error_in_py4vasp(mock_calculation): 94 | runner = CliRunner() 95 | error_message = "Custom error message." 96 | mock_calculation.from_path.side_effect = exception.Py4VaspError(error_message) 97 | result = runner.invoke(cli, ["convert", "structure", "lammps"]) 98 | assert result.exit_code != 0 99 | assert error_message in result.output 100 | -------------------------------------------------------------------------------- /tests/control/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vasp-dev/py4vasp/22e0fa935a750fca129370fc23c30e85c3f71ec3/tests/control/__init__.py -------------------------------------------------------------------------------- /tests/control/test_base.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | from contextlib import redirect_stdout 4 | from io import StringIO 5 | from pathlib import Path 6 | from unittest.mock import mock_open, patch 7 | 8 | import pytest 9 | 10 | from py4vasp._util import import_ 11 | 12 | pretty = import_.optional("IPython.lib.pretty") 13 | 14 | 15 | class AbstractTest: 16 | def test_from_string(self): 17 | text = "! comment line" 18 | with patch("py4vasp._control.base.open", mock_open()) as mock: 19 | instance = self.tested_class.from_string(text) 20 | mock.assert_not_called() 21 | assert str(instance) == instance.read() == text 22 | 23 | def test_from_string_to_file(self): 24 | text = "! comment line" 25 | path = "file_path" 26 | with patch("py4vasp._control.base.open", mock_open(read_data=text)) as mock: 27 | instance = self.tested_class.from_string(text, path) 28 | filename = Path(f"{path}/{self.tested_class.__name__}") 29 | mock.assert_called_once_with(filename, "w") 30 | mock().write.assert_called_once_with(text) 31 | mock.reset_mock() 32 | assert str(instance) == text 33 | mock.assert_called_once_with(filename, "r") 34 | mock().read.assert_called_once_with() 35 | 36 | def test_from_path(self): 37 | text = "! comment line" 38 | path = "file_path" 39 | with patch("py4vasp._control.base.open", mock_open(read_data=text)) as mock: 40 | instance = self.tested_class(path) 41 | assert instance.read() == text 42 | filename = Path(f"{path}/{self.tested_class.__name__}") 43 | mock.assert_called_once_with(filename, "r") 44 | mock().read.assert_called_once_with() 45 | mock.reset_mock() 46 | instance.write(text) 47 | mock.assert_called_once_with(filename, "w") 48 | mock().write.assert_called_once_with(text) 49 | 50 | def test_read_instance(self): 51 | text = "! comment line" 52 | instance = self.tested_class.from_string(text) 53 | assert instance.read() == text 54 | 55 | def test_print_instance(self): 56 | text = "! comment line" 57 | instance = self.tested_class.from_string(text) 58 | with redirect_stdout(StringIO()) as buffer: 59 | instance.print() 60 | assert buffer.getvalue().strip() == text 61 | 62 | @pytest.mark.skipif( 63 | not import_.is_imported(pretty), 64 | reason="This test requires pretty from IPython.", 65 | ) 66 | def test_pretty_instance(self): 67 | text = "! comment line" 68 | instance = self.tested_class.from_string(text) 69 | assert pretty.pretty(instance) == text 70 | -------------------------------------------------------------------------------- /tests/control/test_incar.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | from py4vasp.control import INCAR 4 | 5 | from .test_base import AbstractTest 6 | 7 | 8 | class TestIncar(AbstractTest): 9 | tested_class = INCAR 10 | -------------------------------------------------------------------------------- /tests/control/test_kpoints.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | from py4vasp.control import KPOINTS 4 | 5 | from .test_base import AbstractTest 6 | 7 | 8 | class TestKpoints(AbstractTest): 9 | tested_class = KPOINTS 10 | -------------------------------------------------------------------------------- /tests/control/test_poscar.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import pytest 4 | 5 | from py4vasp._calculation.structure import Structure 6 | from py4vasp.control import POSCAR 7 | 8 | from .test_base import AbstractTest 9 | 10 | 11 | class TestPoscar(AbstractTest): 12 | tested_class = POSCAR 13 | 14 | 15 | @pytest.mark.parametrize("supercell", [None, 2, (3, 2, 1)]) 16 | def test_plot_poscar(supercell, Assert): 17 | text = """! comment line 18 | 5.43 19 | 0.0 0.5 0.5 20 | 0.5 0.0 0.5 21 | 0.5 0.5 0.0 22 | Si 23 | 2 24 | Direct 25 | 0.00 0.00 0.00 26 | 0.25 0.25 0.25 27 | """ 28 | poscar = POSCAR.from_string(text) 29 | structure = Structure.from_POSCAR(text) 30 | structure_view = structure.plot(supercell) 31 | view = poscar.plot(supercell) if supercell else poscar.plot() 32 | Assert.same_structure_view(view, structure_view) 33 | view = poscar.to_view(supercell) if supercell else poscar.to_view() 34 | Assert.same_structure_view(view, structure_view) 35 | 36 | 37 | def test_set_elements_in_plot(Assert): 38 | text = """! comment line 39 | 4.0 40 | 1.0 0.0 0.0 41 | 0.0 1.0 0.0 42 | 0.0 0.0 1.0 43 | 1 1 3 44 | Direct 45 | 0.0 0.0 0.0 46 | 0.5 0.5 0.5 47 | 0.0 0.5 0.5 48 | 0.5 0.0 0.5 49 | 0.5 0.5 0.0 50 | """ 51 | poscar = POSCAR.from_string(text) 52 | elements = ["Sr", "Ti", "O"] 53 | structure = Structure.from_POSCAR(text, elements=elements) 54 | Assert.same_structure_view(poscar.plot(elements=elements), structure.plot()) 55 | -------------------------------------------------------------------------------- /tests/raw/conftest.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import dataclasses 4 | 5 | import pytest 6 | from util import ( 7 | VERSION, 8 | Complex, 9 | Mapping, 10 | OptionalArgument, 11 | Simple, 12 | WithLength, 13 | WithLink, 14 | ) 15 | 16 | from py4vasp import raw 17 | from py4vasp._raw.schema import Length, Link, Schema, Source 18 | 19 | 20 | @pytest.fixture 21 | def complex_schema(): 22 | def make_data(path): 23 | return Simple("custom_factory", path) 24 | 25 | simple = Simple("foo_dataset", "bar_dataset") 26 | filename = "other_file" 27 | only_mandatory = OptionalArgument("mandatory1") 28 | name = "mandatory" 29 | both = OptionalArgument("mandatory2", "optional") 30 | pointer = WithLink("baz_dataset", Link("simple", "default")) 31 | version = raw.Version(1, 2, 3) 32 | length = WithLength(Length("dataset")) 33 | mapping = Mapping( 34 | valid_indices="foo_mapping", common="common_data", variable="variable_data{}" 35 | ) 36 | list_ = Mapping( 37 | valid_indices="list_mapping", common="common", variable="variable_data_{}" 38 | ) 39 | first = Complex( 40 | Link("optional_argument", "default"), 41 | Link("with_link", "default"), 42 | Link("mapping", "default"), 43 | Link("with_length", "default"), 44 | ) 45 | second = Complex( 46 | Link("optional_argument", name), 47 | Link("with_link", "default"), 48 | Link("mapping", "my_list"), 49 | ) 50 | schema = Schema(VERSION) 51 | schema.add(Simple, file=filename, **as_dict(simple)) 52 | schema.add(Simple, name="factory", file=filename, data_factory=make_data) 53 | schema.add(OptionalArgument, name=name, **as_dict(only_mandatory)) 54 | schema.add(OptionalArgument, **as_dict(both)) 55 | schema.add(WithLink, required=version, **as_dict(pointer)) 56 | schema.add(WithLength, alias="alias_name", **as_dict(length)) 57 | schema.add(Mapping, **as_dict(mapping)) 58 | schema.add(Mapping, name="my_list", **as_dict(list_)) 59 | schema.add(Complex, **as_dict(first)) 60 | schema.add(Complex, name=name, **as_dict(second)) 61 | other_file_source = Source(simple, file=filename) 62 | data_factory_source = Source(None, file=filename, data_factory=make_data) 63 | alias_source = Source(length, alias_for="default") 64 | reference = { 65 | "version": {"default": Source(VERSION)}, 66 | "simple": {"default": other_file_source, "factory": data_factory_source}, 67 | "optional_argument": {"default": Source(both), name: Source(only_mandatory)}, 68 | "with_link": {"default": Source(pointer, required=version)}, 69 | "with_length": {"default": Source(length), "alias_name": alias_source}, 70 | "mapping": {"default": Source(mapping), "my_list": Source(list_)}, 71 | "complex": {"default": Source(first), name: Source(second)}, 72 | } 73 | return schema, reference 74 | 75 | 76 | def as_dict(dataclass): 77 | # shallow copy of dataclass to dictionary 78 | return { 79 | field.name: getattr(dataclass, field.name) 80 | for field in dataclasses.fields(dataclass) 81 | if getattr(dataclass, field.name) is not None 82 | } 83 | -------------------------------------------------------------------------------- /tests/raw/test_definition.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | from unittest.mock import patch 4 | 5 | from py4vasp import raw 6 | from py4vasp._raw.definition import schema 7 | 8 | 9 | def test_all_quantities_have_default(): 10 | for quantity, source in schema.sources.items(): 11 | if quantity == "current_density": 12 | # currently no default current density is implemented 13 | assert "default" not in source 14 | else: 15 | assert "default" in source 16 | 17 | 18 | def test_schema_is_valid(): 19 | schema.verify() 20 | 21 | 22 | def test_get_schema(complex_schema): 23 | mock_schema, _ = complex_schema 24 | with patch("py4vasp._raw.definition.schema", mock_schema): 25 | assert raw.get_schema() == str(mock_schema) 26 | 27 | 28 | def test_get_selections(complex_schema): 29 | mock_schema, _ = complex_schema 30 | with patch("py4vasp._raw.definition.schema", mock_schema): 31 | for quantity in mock_schema.sources.keys(): 32 | assert raw.selections(quantity) == mock_schema.selections(quantity) 33 | -------------------------------------------------------------------------------- /tests/raw/test_mapping.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import pytest 4 | from util import Mapping 5 | 6 | from py4vasp import exception 7 | 8 | 9 | @pytest.fixture 10 | def range_mapping(): 11 | return Mapping(range(2), "common", ["variable_1", "variable_2"]) 12 | 13 | 14 | @pytest.fixture 15 | def dict_mapping(): 16 | return Mapping(("a", "b", "c"), "common", ["foo", "bar", "baz"]) 17 | 18 | 19 | def test_access_range_mapping(range_mapping): 20 | assert range_mapping[0] == Mapping([0], "common", "variable_1") 21 | assert range_mapping[1] == Mapping([1], "common", "variable_2") 22 | 23 | 24 | def test_access_dict_mapping(dict_mapping): 25 | assert dict_mapping["a"] == Mapping(["a"], "common", "foo") 26 | assert dict_mapping["b"] == Mapping(["b"], "common", "bar") 27 | assert dict_mapping["c"] == Mapping(["c"], "common", "baz") 28 | 29 | 30 | @pytest.mark.parametrize("incorrect_index", (-1, 3, "foo", None, slice(None))) 31 | def test_incorrect_range_index(range_mapping, incorrect_index): 32 | with pytest.raises(exception.IncorrectUsage): 33 | range_mapping[incorrect_index] 34 | 35 | 36 | @pytest.mark.parametrize("incorrect_index", (1, "foo", None, slice(None))) 37 | def test_incorrect_dict_index(dict_mapping, incorrect_index): 38 | with pytest.raises(exception.IncorrectUsage): 39 | dict_mapping[incorrect_index] 40 | -------------------------------------------------------------------------------- /tests/raw/test_read.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import numpy as np 4 | 5 | from py4vasp import raw 6 | from py4vasp._calculation._CONTCAR import CONTCAR 7 | from py4vasp._calculation.structure import Structure 8 | from py4vasp._raw import read 9 | 10 | 11 | def test_read_structure(raw_data, tmp_path, Assert): 12 | raw_structure = raw_data.structure("ZnS") 13 | filename = tmp_path / "POSCAR" 14 | with open(filename, "w") as file: 15 | file.write(str(Structure.from_data(raw_structure))) 16 | structure = read.structure(filename) 17 | Assert.same_raw_structure(structure, raw_structure) 18 | 19 | 20 | def test_read_contcar(raw_data, tmp_path, Assert): 21 | raw_structure = raw_data.structure("ZnS") 22 | raw_contcar = raw.CONTCAR( 23 | structure=raw_structure, 24 | system="ZnS structure", 25 | lattice_velocities=raw.VaspData(np.linspace(-1, 1, 9).reshape(3, 3)), 26 | ion_velocities=raw.VaspData(0.2 - 0.1 * raw_structure.positions), 27 | ) 28 | filename = tmp_path / "POSCAR" 29 | with open(filename, "w") as file: 30 | file.write(str(CONTCAR.from_data(raw_contcar))) 31 | contcar = read.CONTCAR(filename) 32 | Assert.same_raw_contcar(contcar, raw_contcar) 33 | -------------------------------------------------------------------------------- /tests/raw/test_write.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import h5py 4 | 5 | from py4vasp import raw 6 | from py4vasp._raw.definition import DEFAULT_FILE 7 | from py4vasp._raw.write import write 8 | 9 | 10 | def test_write(tmp_path, raw_data, Assert): 11 | filename = tmp_path / DEFAULT_FILE 12 | raw_structure = raw_data.structure("Sr2TiO4") 13 | with h5py.File(filename, "w") as h5f: 14 | h5f = write(h5f, raw_structure) 15 | with raw.access("structure", path=tmp_path) as structure: 16 | Assert.same_raw_structure(raw_structure, structure) 17 | -------------------------------------------------------------------------------- /tests/raw/util.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import dataclasses 4 | 5 | from py4vasp import raw 6 | from py4vasp._raw import mapping 7 | 8 | VERSION = raw.Version("major_dataset", "minor_dataset", "patch_dataset") 9 | 10 | 11 | @dataclasses.dataclass 12 | class Simple: 13 | foo: str 14 | bar: str 15 | 16 | 17 | @dataclasses.dataclass 18 | class OptionalArgument: 19 | mandatory: str 20 | optional: str = None 21 | 22 | 23 | @dataclasses.dataclass 24 | class WithLink: 25 | baz: str 26 | simple: Simple 27 | 28 | 29 | @dataclasses.dataclass 30 | class WithLength: 31 | num_data: int 32 | 33 | 34 | @dataclasses.dataclass 35 | class Mapping(mapping.Mapping): 36 | common: str 37 | variable: str 38 | 39 | 40 | @dataclasses.dataclass 41 | class Complex: 42 | opt: OptionalArgument 43 | link: WithLink 44 | mapping: Mapping 45 | length: WithLength = None 46 | -------------------------------------------------------------------------------- /tests/scripts/test_error_analysis.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def test_error_analysis(not_core): 5 | errcode = os.system("error-analysis --help") 6 | assert errcode == 0 7 | -------------------------------------------------------------------------------- /tests/test_version.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import pathlib 4 | 5 | import py4vasp 6 | 7 | 8 | def test_version(): 9 | version = get_version_from_toml_file() 10 | assert version == py4vasp.__version__ 11 | 12 | 13 | def get_version_from_toml_file(): 14 | root_dir = pathlib.Path(__file__).parent.parent 15 | with open(root_dir / "pyproject.toml", "r", encoding="utf-8") as toml_file: 16 | return version_from_lines(toml_file) 17 | 18 | 19 | def version_from_lines(toml_file): 20 | for line in toml_file: 21 | parts = line.split("=") 22 | if parts[0].strip() == "version": 23 | return parts[1].strip().strip('"') 24 | -------------------------------------------------------------------------------- /tests/third_party/graph/test_plot.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import itertools 4 | 5 | import numpy as np 6 | import pytest 7 | 8 | from py4vasp import exception 9 | from py4vasp._third_party.graph import Graph, Series, plot 10 | from py4vasp._util import convert 11 | 12 | 13 | def test_plot(): 14 | x1, y1 = np.random.random((2, 50)) 15 | series0 = Series(x1, y1) 16 | series1 = Series(x1, y1, "label1") 17 | assert plot(x1, y1) == Graph(series0) 18 | assert plot(x1, y1, "label1") == Graph(series1) 19 | assert plot(x1, y1, label="label1") == Graph(series1) 20 | assert plot(x1, y1, xlabel="xaxis") == Graph(series0, xlabel="xaxis") 21 | assert plot(x1, y=y1) == Graph(series0) 22 | assert plot(x=x1, y=y1) == Graph(series0) 23 | 24 | 25 | def test_plot_small_dataset(): 26 | for length in range(10): 27 | x = np.linspace(0, 1, length) 28 | y = x**2 29 | series = Series(x, y) 30 | assert plot(x, y) == Graph(series) 31 | 32 | 33 | def test_plot_inconsistent_length(): 34 | x = np.zeros(10) 35 | y = np.zeros(20) 36 | with pytest.raises(exception.IncorrectUsage): 37 | plot(x, y) 38 | 39 | 40 | def test_fatband_inconsistent_length(): 41 | x = np.zeros(10) 42 | y = np.zeros((5, 10)) 43 | weight = np.zeros(20) 44 | with pytest.raises(exception.IncorrectUsage): 45 | plot(x, y, weight=weight) 46 | 47 | 48 | def test_many_colors(not_core): 49 | data = np.random.random((10, 2, 50)) 50 | plots = (plot(x, y) for x, y in data) 51 | graph = sum(plots, start=Graph([])) 52 | figure = graph.to_plotly() 53 | assert len(figure.data) == 10 54 | colors = {series.line.color for series in figure.data} 55 | assert len(colors) > 4 56 | for color1, color2 in itertools.combinations(colors, 2): 57 | assert color_distance(color1, color2) > 30 58 | 59 | 60 | def color_distance(color1, color2): 61 | lab1 = convert.to_lab(color1) 62 | lab2 = convert.to_lab(color2) 63 | return np.linalg.norm(lab1 - lab2) 64 | -------------------------------------------------------------------------------- /tests/third_party/test_interactive.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | from unittest.mock import patch 4 | 5 | import py4vasp._third_party.interactive as interactive 6 | 7 | 8 | def test_no_error_handling_outside_ipython(): 9 | assert interactive.error_handling() == "Minimal" # default 10 | interactive.set_error_handling("Plain") 11 | assert interactive.error_handling() == "Plain" 12 | 13 | 14 | def test_set_error_handling(not_core): 15 | import IPython 16 | 17 | shell = IPython.terminal.interactiveshell.TerminalInteractiveShell() 18 | with patch("IPython.get_ipython", return_value=shell) as mock: 19 | assert shell.InteractiveTB.mode != "Minimal" 20 | interactive.set_error_handling("Minimal") 21 | assert shell.InteractiveTB.mode == "Minimal" 22 | -------------------------------------------------------------------------------- /tests/third_party/view/test_view_mixin.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | from unittest.mock import MagicMock 4 | 5 | import pytest 6 | 7 | from py4vasp._third_party import view 8 | 9 | VIEW = MagicMock(spec=view.View) 10 | 11 | 12 | class ExampleView(view.Mixin): 13 | def to_view(self): 14 | VIEW.reset_mock() 15 | return VIEW 16 | 17 | 18 | def test_is_abstract_class(): 19 | with pytest.raises(TypeError): 20 | view.Mixin() 21 | 22 | 23 | def test_plot_wraps_to_view(): 24 | example = ExampleView() 25 | assert example.plot() == example.to_view() 26 | 27 | 28 | def test_converting_view_to_ngl(): 29 | example = ExampleView() 30 | widget = example.to_ngl() 31 | VIEW.to_ngl.assert_called_once_with() 32 | assert widget == VIEW.to_ngl.return_value 33 | 34 | 35 | class WithArguments(view.Mixin): 36 | def to_view(self, mandatory, optional=None): 37 | VIEW.reset_mock() 38 | VIEW.arguments = {"mandatory": mandatory, "optional": optional} 39 | return VIEW 40 | 41 | 42 | def test_arguments_passed_by_plot(): 43 | example = WithArguments() 44 | view = example.plot("only mandatory") 45 | assert view.arguments == {"mandatory": "only mandatory", "optional": None} 46 | view = example.plot("first", "second") 47 | assert view.arguments == {"mandatory": "first", "optional": "second"} 48 | view = example.plot(optional="foo", mandatory="bar") 49 | assert view.arguments == {"mandatory": "bar", "optional": "foo"} 50 | 51 | 52 | def test_arguments_passed_by_to_ngl(): 53 | example = WithArguments() 54 | example.to_ngl("only mandatory") 55 | assert VIEW.arguments == {"mandatory": "only mandatory", "optional": None} 56 | example.to_ngl("first", "second") 57 | assert VIEW.arguments == {"mandatory": "first", "optional": "second"} 58 | example.to_ngl(optional="foo", mandatory="bar") 59 | assert VIEW.arguments == {"mandatory": "bar", "optional": "foo"} 60 | -------------------------------------------------------------------------------- /tests/util/test_check.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import numpy as np 4 | import pytest 5 | 6 | from py4vasp import exception, raw 7 | from py4vasp._util import check 8 | 9 | 10 | @pytest.mark.parametrize("is_none", (None, raw.VaspData(None))) 11 | def test_check_is_none(is_none): 12 | assert check.is_none(is_none) 13 | 14 | 15 | @pytest.mark.parametrize("is_not_none", (np.zeros(3), [1, 2], (2, 3), "text", 2)) 16 | def test_check_is_not_none(is_not_none): 17 | assert not check.is_none(is_not_none) 18 | 19 | 20 | def test_error_if_not_string(): 21 | check.raise_error_if_not_string("string does not raise exception", "message") 22 | with pytest.raises(exception.IncorrectUsage): 23 | check.raise_error_if_not_string(1, "message") 24 | 25 | 26 | def test_error_if_not_number(): 27 | check.raise_error_if_not_number(1, "number does not raise exception") 28 | with pytest.raises(exception.IncorrectUsage): 29 | check.raise_error_if_not_number("should be number", "message") 30 | 31 | 32 | def test_error_if_not_callable(): 33 | def func(x, y=1): 34 | pass 35 | 36 | # valid calls do not raise error 37 | check.raise_error_if_not_callable(func, 0) 38 | check.raise_error_if_not_callable(func, 1, 2) 39 | check.raise_error_if_not_callable(func, x=3, y=4) 40 | 41 | # invalid calls raise error 42 | with pytest.raises(exception.IncorrectUsage): 43 | check.raise_error_if_not_callable(func, y=5) 44 | with pytest.raises(exception.IncorrectUsage): 45 | check.raise_error_if_not_callable(func, 6, 7, 8) 46 | with pytest.raises(exception.IncorrectUsage): 47 | check.raise_error_if_not_callable(func, 9, z=10) 48 | -------------------------------------------------------------------------------- /tests/util/test_convert.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import numpy as np 4 | import pytest 5 | 6 | from py4vasp._config import VASP_COLORS 7 | from py4vasp._util.convert import ( 8 | text_to_string, 9 | to_camelcase, 10 | to_complex, 11 | to_lab, 12 | to_rgb, 13 | ) 14 | 15 | 16 | @pytest.fixture 17 | def reference_colors(): 18 | return ("#4C265F", "#2FB5AB", "#2C68FC", "#A82C35", "#808080", "#212529") 19 | 20 | 21 | def test_text_to_string(): 22 | assert text_to_string(b"foo") == "foo" 23 | assert text_to_string("bar") == "bar" 24 | 25 | 26 | def test_scalar_to_complex(Assert): 27 | scalar = np.array((0.0, 1.0)) 28 | converted = to_complex(scalar) 29 | assert converted.shape == () 30 | Assert.allclose(converted.real, scalar[0]) 31 | Assert.allclose(converted.imag, scalar[1]) 32 | 33 | 34 | def test_vector_to_complex(Assert): 35 | vector = np.linspace(0, 9, 10).reshape(5, 2) 36 | converted = to_complex(vector) 37 | assert converted.shape == (5,) 38 | Assert.allclose(converted.real, vector[:, 0]) 39 | Assert.allclose(converted.imag, vector[:, 1]) 40 | 41 | 42 | def test_matrix_to_complex(Assert): 43 | matrix = np.linspace(0, 29, 30).reshape(3, 5, 2) 44 | converted = to_complex(matrix) 45 | assert converted.shape == (3, 5) 46 | Assert.allclose(converted.real, matrix[:, :, 0]) 47 | Assert.allclose(converted.imag, matrix[:, :, 1]) 48 | 49 | 50 | def test_hex_to_rgb(reference_colors, Assert): 51 | converted = [ 52 | [76, 38, 95], 53 | [47, 181, 171], 54 | [44, 104, 252], 55 | [168, 44, 53], 56 | [128, 128, 128], 57 | [33, 37, 41], 58 | ] 59 | expected = np.array(converted) / 255 60 | actual = np.array([to_rgb(color) for color in reference_colors]) 61 | Assert.allclose(expected, actual) 62 | 63 | 64 | def test_hex_to_lab(reference_colors, Assert): 65 | expected = [ 66 | (22.824, 28.808, -26.898), 67 | (66.968, -37.078, -5.109), 68 | (48.836, 34.588, -78.789), 69 | (38.526, 50.465, 25.169), 70 | (53.585, -0.002, -0.000), 71 | (14.437, -0.722, -3.262), 72 | ] 73 | actual = np.array([np.round(to_lab(color), 3) for color in reference_colors]) 74 | Assert.allclose(expected, actual) 75 | 76 | 77 | def test_camelcase(): 78 | assert to_camelcase("foo") == "Foo" 79 | assert to_camelcase("foo_bar") == "FooBar" 80 | assert to_camelcase("foo_bar_baz") == "FooBarBaz" 81 | assert to_camelcase("_foo") == "Foo" 82 | assert to_camelcase("_foo_bar") == "FooBar" 83 | assert to_camelcase("foo_bar", uppercase_first_letter=False) == "fooBar" 84 | -------------------------------------------------------------------------------- /tests/util/test_documentation.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import inspect 4 | 5 | import py4vasp._util.documentation as _documentation 6 | 7 | 8 | def test_format_documentation(): 9 | inner_text = """\ 10 | First line 11 | Second line 12 | """ 13 | 14 | @_documentation.format(inner_text=inner_text) 15 | def func(): 16 | """Multiple line string 17 | 18 | {inner_text} 19 | 20 | continued afterwards""" 21 | 22 | expected = """\ 23 | Multiple line string 24 | 25 | First line 26 | Second line 27 | 28 | continued afterwards""" 29 | assert inspect.getdoc(func) == expected 30 | -------------------------------------------------------------------------------- /tests/util/test_import.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import pytest 4 | 5 | from py4vasp import exception 6 | from py4vasp._util import import_ 7 | 8 | 9 | def test_import_not_available(): 10 | module = import_.optional("_name_which_does_not_exist_") 11 | assert not import_.is_imported(module) 12 | with pytest.raises(exception.ModuleNotInstalled): 13 | module.attribute 14 | 15 | 16 | def test_import_for_existing_module(): 17 | module = import_.optional("py4vasp") 18 | assert import_.is_imported(module) 19 | -------------------------------------------------------------------------------- /tests/util/test_reader.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | import numpy as np 4 | import pytest 5 | 6 | from py4vasp import exception 7 | from py4vasp._util.reader import Reader 8 | 9 | 10 | def test_reader(): 11 | array = np.zeros(20) 12 | reader = Reader(array) 13 | with pytest.raises(exception.IncorrectUsage): 14 | reader[len(array) + 1] 15 | -------------------------------------------------------------------------------- /tests/util/test_suggest.py: -------------------------------------------------------------------------------- 1 | # Copyright © VASP Software GmbH, 2 | # Licensed under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 3 | from py4vasp._util import suggest 4 | 5 | 6 | def test_similar_selection(): 7 | assert suggest.did_you_mean("bar", ["foo", "baz"]) == 'Did you mean "baz"? ' 8 | 9 | 10 | def test_no_similar_selection(): 11 | assert suggest.did_you_mean("foo", ["bar", "baz"]) == "" 12 | 13 | 14 | def test_key_is_converted_to_string(): 15 | assert suggest.did_you_mean(120, ["99", "121", "700"]) == 'Did you mean "121"? ' 16 | 17 | 18 | def test_possibilities_are_converted_to_string(): 19 | assert suggest.did_you_mean("320", range(291, 330, 10)) == 'Did you mean "321"? ' 20 | --------------------------------------------------------------------------------