├── .codacy.yml ├── .gitattributes ├── .github └── workflows │ ├── ci.yml │ └── coverage.yml ├── .gitignore ├── .readthedocs.yaml ├── LICENSE ├── README.md ├── docs ├── Makefile ├── citation.rst ├── compatibility.rst ├── conf.py ├── index.rst ├── installation.rst ├── make.bat ├── modules.rst ├── nonrad.ccd.rst ├── nonrad.constants.rst ├── nonrad.elphon.rst ├── nonrad.nonrad.rst ├── nonrad.rst ├── nonrad.scaling.rst ├── nonrad.tests.rst ├── nonrad.tests.test_ccd.rst ├── nonrad.tests.test_elphon.rst ├── nonrad.tests.test_nonrad.rst ├── nonrad.tests.test_scaling.rst ├── rtd.txt ├── tips.rst ├── tutorial.rst └── tutorial_files │ ├── tutorial_12_0.png │ ├── tutorial_16_0.png │ ├── tutorial_22_0.png │ ├── tutorial_24_0.png │ ├── tutorial_27_0.png │ ├── tutorial_29_0.png │ ├── tutorial_2_0.png │ └── tutorial_8_0.png ├── nonrad ├── __init__.py ├── ccd.py ├── constants.py ├── elphon.py ├── nonrad.py ├── scaling.py └── tests │ ├── __init__.py │ ├── test_ccd.py │ ├── test_elphon.py │ ├── test_nonrad.py │ └── test_scaling.py ├── notebooks ├── benchmarking.ipynb └── tutorial.ipynb ├── pdm.lock ├── pyproject.toml └── test_files ├── POSCAR.C-.gz ├── POSCAR.C0.gz ├── UNK.0 ├── UNK.1 ├── lower └── 10 │ ├── WSWQ.gz │ └── vasprun.xml.gz └── vasprun.xml.0.gz /.codacy.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | pylint: 3 | enabled: true 4 | python_version: 3 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # ignore jupyter notebooks in the language bar on github 2 | **/*.ipynb linguist-vendored 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: nonrad continuous integration 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: ['3.9', '3.10', '3.11'] 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | - name: Set up PDM 15 | uses: pdm-project/setup-pdm@v3 16 | with: 17 | python-version: ${{ matrix.python-version }} 18 | cache: True 19 | - name: Install dependencies 20 | run: | 21 | pdm install -dG lint,test 22 | - name: Lint with ruff 23 | run: | 24 | pdm run ruff check nonrad 25 | - name: Lint with mypy 26 | run: | 27 | pdm run mypy nonrad 28 | - name: Test with pytest 29 | run: | 30 | pdm run pytest nonrad 31 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: nonrad codecov 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - name: Set up PDM 11 | uses: pdm-project/setup-pdm@v3 12 | with: 13 | python-version: '3.11' 14 | cache: True 15 | - name: Install dependencies 16 | run: | 17 | pdm install -dG test 18 | - name: Generate coverage report 19 | run: | 20 | pdm remove -L pdm.new.lock numba 21 | pdm run pytest --cov-report=xml --cov=nonrad nonrad 22 | bash <(curl -s https://codecov.io/bash) -t ${{ secrets.CODECOV_TOKEN }} 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.egg-info/ 3 | dist/ 4 | build/ 5 | .coverage 6 | .ipynb_checkpoints/ 7 | WAVECAR* 8 | _build/ 9 | _static/ 10 | .pdm-python 11 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | sphinx: 4 | configuration: docs/conf.py 5 | 6 | build: 7 | os: ubuntu-22.04 8 | tools: 9 | python: "3.11" 10 | jobs: 11 | post_install: 12 | - pip install --upgrade pdm 13 | - VIRTUAL_ENV=$(dirname $(dirname $(which python))) pdm install -dG docs 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Chris G. Van de Walle 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![build badge](https://img.shields.io/github/actions/workflow/status/mturiansky/nonrad/ci.yml) [![docs badge](https://readthedocs.org/projects/nonrad/badge/?version=latest)](https://nonrad.readthedocs.io/en/latest/?badge=latest) [![codacy](https://app.codacy.com/project/badge/Grade/97df4e822c2349ff858a756b033c6041)](https://www.codacy.com?utm_source=github.com&utm_medium=referral&utm_content=mturiansky/nonrad&utm_campaign=Badge_Grade) [![codecov](https://codecov.io/gh/mturiansky/nonrad/branch/master/graph/badge.svg?token=N1IXIQK333)](https://codecov.io/gh/mturiansky/nonrad) [![license](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.4274317.svg)](https://doi.org/10.5281/zenodo.4274317) 2 | 3 | # NONRAD 4 | 5 | An implementation of the methodology pioneered by [Alkauskas *et al.*](https://doi.org/10.1103/PhysRevB.90.075202) for computing nonradiative recombination rates from first principles. 6 | The code includes various utilities for processing first principles calculations and preparing the input for computing capture coefficients. 7 | More details on the implementation of the code can be found in [our recent paper](). 8 | Documentation for the code is hosted on [Read the Docs](https://nonrad.readthedocs.io/en/latest). 9 | 10 | ## Installation 11 | NONRAD is implemented in python and can be installed through `pip`. 12 | Dependencies are kept to a minimum and include standard packages such as `numpy`, `scipy`, and `pymatgen`. 13 | 14 | #### With pip 15 | As always with python, it is highly recommended to use a virtual environment. 16 | To install NONRAD, issue the following command, 17 | ``` 18 | $ pip install nonrad 19 | ``` 20 | or to install directly from github, 21 | ``` 22 | $ pip install git+https://github.com/mturiansky/nonrad 23 | ``` 24 | 25 | #### Going Fast (*Recommended*) 26 | NONRAD can use `numba` to accelerate certain calculations. 27 | If `numba` is already installed, it will be used; 28 | otherwise, it can be installed by specifying `[fast]` during installation with pip, e.g. 29 | ``` 30 | $ pip install nonrad[fast] 31 | ``` 32 | 33 | #### For Development 34 | To install NONRAD for development purposes, clone the repository 35 | ``` 36 | $ git clone https://github.com/mturiansky/nonrad && cd nonrad 37 | ``` 38 | then install the package in editable mode with development dependencies 39 | ``` 40 | $ pip install -e .[dev] 41 | ``` 42 | `pytest` is used for unittesting. 43 | To run the unittests, issue the command `pytest nonrad` from the base directory. 44 | Unittests should run correctly with and without `numba` installed. 45 | 46 | ## Usage 47 | A tutorial notebook that describes the various steps is available [here](https://github.com/mturiansky/nonrad/blob/master/notebooks/tutorial.ipynb). 48 | The basic steps are summarized below: 49 | 50 | 0. Perform a first-principles calculation of the target defect system. A good explanation of the methodology can be found in this [Review of Modern Physics](http://dx.doi.org/10.1103/RevModPhys.86.253). A high quality calculation is necessary as input for the nonradiative capture rate as the resulting values can differ by orders of magnitude depending on the input values. 51 | 1. Calculate the potential energy surfaces for the configuration coordinate diagram. This is facilitated using the `get_cc_structures` function. Extract the relevant parameters from the configuration coordinate diagram, aided by `get_dQ`, `get_PES_from_vaspruns`, and `get_omega_from_PES`. 52 | 2. Calculate the electron-phonon coupling matrix elements, using the method of your choice (see [our paper]() for details on this calculation with `VASP`). Extraction of the matrix elements are facilitated by the `get_Wif_from_wavecars` or the `get_Wif_from_WSWQ` function. 53 | 3. Calculate scaling coefficients using `sommerfeld_parameter` and/or `charged_supercell_scaling`. 54 | 4. Perform the calculation of the nonradiative capture coefficient using `get_C`. 55 | 56 | ## Contributing 57 | To contribute, see the above section on installing [for development](#for-development). 58 | Contributions are welcome and any potential change or improvement should be submitted as a pull request on [Github](https://github.com/mturiansky/nonrad/pulls). 59 | Potential contribution areas are: 60 | - [ ] implement a command line interface 61 | - [ ] add more robust tests for various functions 62 | 63 | ## How to Cite 64 | If you use our code to calculate nonradiative capture rates, please consider citing 65 | ``` 66 | @article{alkauskas_first-principles_2014, 67 | title = {First-principles theory of nonradiative carrier capture via multiphonon emission}, 68 | volume = {90}, 69 | doi = {10.1103/PhysRevB.90.075202}, 70 | number = {7}, 71 | journal = {Phys. Rev. B}, 72 | author = {Alkauskas, Audrius and Yan, Qimin and Van de Walle, Chris G.}, 73 | month = aug, 74 | year = {2014}, 75 | pages = {075202}, 76 | } 77 | ``` 78 | and 79 | ``` 80 | @article{turiansky_nonrad_2021, 81 | title = {Nonrad: {Computing} nonradiative capture coefficients from first principles}, 82 | volume = {267}, 83 | doi = {10.1016/j.cpc.2021.108056}, 84 | journal = {Comput. Phys. Commun.}, 85 | author = {Turiansky, Mark E. and Alkauskas, Audrius and Engel, Manuel and Kresse, Georg and Wickramaratne, Darshana and Shen, Jimmy-Xuan and Dreyer, Cyrus E. and Van de Walle, Chris G.}, 86 | month = oct, 87 | year = {2021}, 88 | pages = {108056}, 89 | } 90 | ``` 91 | If you use the functionality for the Sommerfeld parameter in 2 and 1 dimensions, then please cite 92 | ``` 93 | @article{turiansky_dimensionality_2024, 94 | title = {Dimensionality Effects on Trap-Assisted Recombination: The {{Sommerfeld}} Parameter}, 95 | shorttitle = {Dimensionality Effects on Trap-Assisted Recombination}, 96 | author = {Turiansky, Mark E and Alkauskas, Audrius and Van De Walle, Chris G}, 97 | year = {2024}, 98 | month = may, 99 | journal = {J. Phys.: Condens. Matter}, 100 | volume = {36}, 101 | number = {19}, 102 | pages = {195902}, 103 | doi = {10.1088/1361-648X/ad2588}, 104 | } 105 | ``` 106 | -------------------------------------------------------------------------------- /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/citation.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Citation 3 | ============ 4 | 5 | If you use our code to calculate nonradiative capture rates, please consider citing 6 | 7 | .. code-block:: tex 8 | 9 | @article{alkauskas_first-principles_2014, 10 | title = {First-principles theory of nonradiative carrier capture via multiphonon emission}, 11 | volume = {90}, 12 | doi = {10.1103/PhysRevB.90.075202}, 13 | number = {7}, 14 | journal = {Phys. Rev. B}, 15 | author = {Alkauskas, Audrius and Yan, Qimin and Van de Walle, Chris G.}, 16 | month = aug, 17 | year = {2014}, 18 | pages = {075202}, 19 | } 20 | 21 | and 22 | 23 | .. code-block:: tex 24 | 25 | @article{turiansky_nonrad_2021, 26 | title = {Nonrad: {Computing} nonradiative capture coefficients from first principles}, 27 | volume = {267}, 28 | doi = {10.1016/j.cpc.2021.108056}, 29 | journal = {Comput. Phys. Commun.}, 30 | author = {Turiansky, Mark E. and Alkauskas, Audrius and Engel, Manuel and Kresse, Georg and Wickramaratne, Darshana and Shen, Jimmy-Xuan and Dreyer, Cyrus E. and Van de Walle, Chris G.}, 31 | month = oct, 32 | year = {2021}, 33 | pages = {108056}, 34 | } 35 | 36 | 37 | If you use the functionality for the Sommerfeld parameter in 2 and 1 dimensions, then please cite 38 | 39 | .. code-block:: tex 40 | 41 | @article{turiansky_dimensionality_2024, 42 | title = {Dimensionality Effects on Trap-Assisted Recombination: The {{Sommerfeld}} Parameter}, 43 | shorttitle = {Dimensionality Effects on Trap-Assisted Recombination}, 44 | author = {Turiansky, Mark E and Alkauskas, Audrius and Van De Walle, Chris G}, 45 | year = {2024}, 46 | month = may, 47 | journal = {J. Phys.: Condens. Matter}, 48 | volume = {36}, 49 | number = {19}, 50 | pages = {195902}, 51 | doi = {10.1088/1361-648X/ad2588}, 52 | } 53 | 54 | -------------------------------------------------------------------------------- /docs/compatibility.rst: -------------------------------------------------------------------------------- 1 | ============= 2 | Compatibility 3 | ============= 4 | 5 | `nonrad` was built to be sufficiently general for the study of nonradiative transitions, but invariably, some utilities are tied to a given first-principles DFT code. 6 | Below, we enumerate the various functionalities provided by the `nonrad` code and specify the compatibility of each. 7 | **General** means that the input parameters are general and do not depend on a given first-principles code. 8 | **Pymatgen** means that function supports any of the first-principles codes that are supported by the `pymatgen` code, including VASP or quantum espresso, as well as general file formats, such as `.cif`, `.xyz`, etc. 9 | **Wannier90** means that the function is compatible with any first-principles code that can write to the Wannier90 formats (e.g., UNK files). 10 | **VASP** means that the function is specific to the VASP code. 11 | 12 | .. list-table:: Compatibility 13 | :widths: 25 10 10 10 10 14 | :header-rows: 1 15 | 16 | * - Function 17 | - General 18 | - Pymatgen 19 | - Wannier90 20 | - VASP 21 | * - `get_C `_ 22 | - X 23 | - 24 | - 25 | - 26 | * - `charged_supercell_scaling `_ 27 | - X 28 | - 29 | - 30 | - 31 | * - `charged_supercell_scaling_VASP `_ 32 | - 33 | - 34 | - 35 | - X 36 | * - `sommerfeld_parameter `_ 37 | - X 38 | - 39 | - 40 | - 41 | * - `thermal_velocity `_ 42 | - X 43 | - 44 | - 45 | - 46 | * - `get_PES_from_vaspruns `_ 47 | - 48 | - 49 | - 50 | - X 51 | * - `get_Q_from_struct `_ 52 | - 53 | - X 54 | - 55 | - 56 | * - `get_Wif_from_WSWQ `_ 57 | - 58 | - 59 | - 60 | - X 61 | * - `get_Wif_from_wavecars `_ 62 | - 63 | - 64 | - 65 | - X 66 | * - `get_Wif_from_UNK `_ 67 | - 68 | - 69 | - X 70 | - 71 | * - `get_cc_structures `_ 72 | - 73 | - X 74 | - 75 | - 76 | * - `get_dQ `_ 77 | - 78 | - X 79 | - 80 | - 81 | * - `get_omega_from_PES `_ 82 | - X 83 | - 84 | - 85 | - 86 | -------------------------------------------------------------------------------- /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 | try: 16 | import nonrad 17 | except ImportError: 18 | sys.path.insert(0, os.path.abspath('..')) 19 | import nonrad 20 | 21 | 22 | # -- Project information ----------------------------------------------------- 23 | 24 | project = 'nonrad' 25 | copyright = '2020, Mark E. Turiansky' 26 | author = 'Mark E. Turiansky' 27 | 28 | release = nonrad.__version__ 29 | version = nonrad.__version__ 30 | 31 | 32 | # -- General configuration --------------------------------------------------- 33 | 34 | # Add any Sphinx extension module names here, as strings. They can be 35 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 36 | # ones. 37 | extensions = [ 38 | 'sphinx_rtd_theme', 39 | 'sphinx.ext.autodoc', 40 | 'sphinx.ext.viewcode', 41 | 'sphinx.ext.napoleon', 42 | 'sphinx.ext.todo' 43 | ] 44 | 45 | # Add any paths that contain templates here, relative to this directory. 46 | templates_path = ['_templates'] 47 | 48 | # List of patterns, relative to source directory, that match files and 49 | # directories to ignore when looking for source files. 50 | # This pattern also affects html_static_path and html_extra_path. 51 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 52 | 53 | 54 | # -- Options for HTML output ------------------------------------------------- 55 | 56 | # The theme to use for HTML and HTML Help pages. See the documentation for 57 | # a list of builtin themes. 58 | # 59 | html_theme = 'sphinx_rtd_theme' 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'] -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. toctree:: 2 | :hidden: 3 | 4 | installation 5 | compatibility 6 | citation 7 | Tutorial 8 | tips 9 | API Docs 10 | 11 | Nonrad 12 | ====== 13 | 14 | .. image:: https://img.shields.io/github/actions/workflow/status/mturiansky/nonrad/ci.yml 15 | :alt: Build Badge 16 | .. image:: https://readthedocs.org/projects/nonrad/badge/?version=latest 17 | :alt: Docs Badge 18 | .. image:: https://app.codacy.com/project/badge/Grade/97df4e822c2349ff858a756b033c6041 19 | :alt: Codacy 20 | .. image:: https://codecov.io/gh/mturiansky/nonrad/branch/master/graph/badge.svg?token=N1IXIQK333 21 | :alt: Codecov 22 | .. image:: https://img.shields.io/badge/License-MIT-yellow.svg 23 | :alt: License 24 | .. image:: https://zenodo.org/badge/DOI/10.5281/zenodo.4274318.svg 25 | :alt: DOI 26 | 27 | An implementation of the methodology pioneered by `Alkauskas et al. `_ for computing nonradiative recombination rates from first principles. 28 | The code includes various utilities for processing first principles calculations and preparing the input for computing capture coefficients. 29 | More details on the implementation of the code can be found in `our recent paper `_. 30 | If you use our code, please consider citing it (see :doc:`citation`). 31 | 32 | The code is hosted on `github `_. 33 | 34 | 35 | Indices and tables 36 | ================== 37 | 38 | * :ref:`genindex` 39 | * :ref:`modindex` 40 | * :ref:`search` 41 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Installation 3 | ============ 4 | 5 | `nonrad` may be installed through pip from PyPI, 6 | 7 | .. code-block:: sh 8 | 9 | pip install nonrad 10 | 11 | or directly through github, 12 | 13 | .. code-block:: sh 14 | 15 | pip install git+https://github.com/mturiansky/nonrad 16 | 17 | 18 | Going faster 19 | ============ 20 | 21 | This code utilizes `numba` to speed up calculations, and there are various ways to improve the performance of `numba`. 22 | 23 | SVML 24 | ---- 25 | 26 | On Intel processors, the short vector math library (SVML) can be enabled to speed up certain operations. 27 | The runtime libraries from Intel are required for this. 28 | On a `conda` installation, they should already be installed in the package `icc_rt`. 29 | The `icc_rt` package is also available through pip 30 | 31 | .. code-block:: sh 32 | 33 | pip install icc_rt 34 | 35 | However, you will likely need to add your virtual environment to the library path: 36 | 37 | .. code-block:: sh 38 | 39 | export LD_LIBRARY_PATH=/path/to/.virtualenvs/env_name/lib/:$LD_LIBRARY_PATH 40 | 41 | This can be added to your `activate` script in your virtual environment (``/path/to/.virtualenvs/env_name/bin/activate``) to make the change persistent. 42 | To check if the installation worked, run ``numba -s``; the output should include 43 | 44 | .. code-block:: 45 | 46 | ... 47 | 48 | __SVML Information__ 49 | SVML State, config.USING_SVML : True 50 | SVML Library Loaded : True 51 | llvmlite Using SVML Patched LLVM : True 52 | SVML Operational : True 53 | 54 | ... 55 | 56 | Numba Enviornment Variables 57 | --------------------------- 58 | 59 | There are several environment variables for `numba` that can be enabled and may improve performance. 60 | If your machine has AVX instructions, we recommend enabling it with: 61 | 62 | .. code-block:: sh 63 | 64 | export NUMBA_ENABLE_AVX=1 65 | 66 | The full list of `numba` environment variables is available `here `_. 67 | -------------------------------------------------------------------------------- /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/modules.rst: -------------------------------------------------------------------------------- 1 | nonrad 2 | ====== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | nonrad 8 | -------------------------------------------------------------------------------- /docs/nonrad.ccd.rst: -------------------------------------------------------------------------------- 1 | nonrad.ccd module 2 | ================= 3 | 4 | .. automodule:: nonrad.ccd 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/nonrad.constants.rst: -------------------------------------------------------------------------------- 1 | nonrad.constants module 2 | ======================= 3 | 4 | .. automodule:: nonrad.constants 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/nonrad.elphon.rst: -------------------------------------------------------------------------------- 1 | nonrad.elphon module 2 | ==================== 3 | 4 | .. automodule:: nonrad.elphon 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/nonrad.nonrad.rst: -------------------------------------------------------------------------------- 1 | nonrad.nonrad module 2 | ==================== 3 | 4 | .. automodule:: nonrad.nonrad 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/nonrad.rst: -------------------------------------------------------------------------------- 1 | nonrad package 2 | ============== 3 | 4 | .. automodule:: nonrad 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Subpackages 10 | ----------- 11 | 12 | .. toctree:: 13 | :maxdepth: 4 14 | 15 | nonrad.tests 16 | 17 | Submodules 18 | ---------- 19 | 20 | .. toctree:: 21 | :maxdepth: 4 22 | 23 | nonrad.ccd 24 | nonrad.constants 25 | nonrad.elphon 26 | nonrad.nonrad 27 | nonrad.scaling 28 | -------------------------------------------------------------------------------- /docs/nonrad.scaling.rst: -------------------------------------------------------------------------------- 1 | nonrad.scaling module 2 | ===================== 3 | 4 | .. automodule:: nonrad.scaling 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/nonrad.tests.rst: -------------------------------------------------------------------------------- 1 | nonrad.tests package 2 | ==================== 3 | 4 | .. automodule:: nonrad.tests 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | .. toctree:: 13 | :maxdepth: 4 14 | 15 | nonrad.tests.test_ccd 16 | nonrad.tests.test_elphon 17 | nonrad.tests.test_nonrad 18 | nonrad.tests.test_scaling 19 | -------------------------------------------------------------------------------- /docs/nonrad.tests.test_ccd.rst: -------------------------------------------------------------------------------- 1 | nonrad.tests.test\_ccd module 2 | ============================= 3 | 4 | .. automodule:: nonrad.tests.test_ccd 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/nonrad.tests.test_elphon.rst: -------------------------------------------------------------------------------- 1 | nonrad.tests.test\_elphon module 2 | ================================ 3 | 4 | .. automodule:: nonrad.tests.test_elphon 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/nonrad.tests.test_nonrad.rst: -------------------------------------------------------------------------------- 1 | nonrad.tests.test\_nonrad module 2 | ================================ 3 | 4 | .. automodule:: nonrad.tests.test_nonrad 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/nonrad.tests.test_scaling.rst: -------------------------------------------------------------------------------- 1 | nonrad.tests.test\_scaling module 2 | ================================= 3 | 4 | .. automodule:: nonrad.tests.test_scaling 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/rtd.txt: -------------------------------------------------------------------------------- 1 | jinja2==3.0.3 2 | Sphinx==3.1.2 3 | sphinx-rtd-theme==0.5.0 4 | sphinxcontrib-applehelp==1.0.2 5 | sphinxcontrib-devhelp==1.0.2 6 | sphinxcontrib-htmlhelp==1.0.3 7 | sphinxcontrib-jsmath==1.0.1 8 | sphinxcontrib-qthelp==1.0.3 9 | sphinxcontrib-serializinghtml==1.1.4 10 | -------------------------------------------------------------------------------- /docs/tips.rst: -------------------------------------------------------------------------------- 1 | ============= 2 | Tips & Tricks 3 | ============= 4 | 5 | Below we compile several useful tips and tricks related to running `nonrad`. 6 | 7 | 8 | 1. Known bug in VASP 5.4.4 9 | -------------------------- 10 | 11 | In version 5.4.4 of `VASP`, a bug is present in the code, which results in potential errors in the computed electron-phonon coupling. 12 | The easy solution is to use version :math:`\ge` 6.0.0 of `VASP`, for which the bug is already fixed. 13 | Otherwise, the `VASP` source code can be modified to fix the bug, as described at `here `_. 14 | 15 | 2. Systems with large barriers 16 | ------------------------------ 17 | 18 | Many of the default parameters and choices in the design of `nonrad` are based around conventional recombination or trapping centers (generally, :math:`C > 10^{-13}`). 19 | If you apply the code to more unusual cases, care needs to be taken to ensure convergence. 20 | For example, one may try to calculate the capture coefficient for a system with a large semiclassical barrier to recombination (the crossing point between the potential energy surfaces). 21 | If the barrier energy is too large, then the calculation may be sensitive to the ``occ_tol`` parameter. 22 | ``occ_tol`` is used to determine, along with the temperature and phonon frequency, the highest phonon quantum number of the initial state included in the calculation. 23 | When the barrier is large and the temperature is high, higher quantum numbers may be important (i.e., :math:`m_{\rm max} \hbar\Omega_i > E_b`, where :math:`E_b` is the barrier energy, is desired). 24 | Of course, the conventional wisdom would tell you that a system with such a large barrier can be neglected, so one does not even need to explicitly perform the calculation. 25 | -------------------------------------------------------------------------------- /docs/tutorial.rst: -------------------------------------------------------------------------------- 1 | NONRAD Tutorial 2 | =============== 3 | 4 | This notebook serves as a tutorial for how to use the NONRAD code to 5 | compute the nonradiative capture coefficient for a given defect. In this 6 | tutorial, we will examine the capture of a hole by the negatively charge 7 | C substiution on the N site in wurtzite GaN. 8 | 9 | **Recommendation**: For every function provided by NONRAD, read the 10 | docstring to understand how the function behaves. This can be done using 11 | ``function?`` in a notebook or ``print(function.__doc__)``. 12 | 13 | 0. First-Principles Defect Calculation 14 | -------------------------------------- 15 | 16 | Before we begin using the code provided by NONRAD, we must perform a 17 | first-principles calculation to obtain the equilibrium structures and 18 | thermodynamic level for our defect. This results in a formation energy 19 | plot such as the following. 20 | 21 | .. code:: python3 22 | 23 | %matplotlib inline 24 | import matplotlib.pyplot as plt 25 | import numpy as np 26 | 27 | Efermi = np.linspace(0., 3.5, 10) 28 | fig, ax = plt.subplots(figsize=(5, 5)) 29 | ax.plot(Efermi, - Efermi + 1.058) 30 | ax.plot(Efermi, np.zeros(10)) 31 | ax.scatter(1.058, 0., color='r', marker='|', zorder=10) 32 | ax.text(1.058, 0., '$\epsilon (0/-)$ = 1.058 eV', color='r', va='bottom') 33 | ax.set_xlabel(r'$E_{\rm{Fermi}}$ [eV]') 34 | ax.set_ylabel(r'$E_f - E_f(q=0)$ [eV]') 35 | plt.show() 36 | 37 | 38 | 39 | .. image:: tutorial_files/tutorial_2_0.png 40 | 41 | 42 | The formation energy plot tells us the most stable charge state as a 43 | function of the Fermi level. The blue line corresponds to the C 44 | substitution being in the negative charge state, and the orange line 45 | corresponds to the neutral charge state. The thermodynamic transition 46 | level is the crossing between these two lines and for this defect, we 47 | find a value of 1.058 eV. This will be one input parameter to the 48 | calculation of the nonradiative capture coefficient, ``dE``. Let’s save 49 | this value for later: 50 | 51 | .. code:: python3 52 | 53 | dE = 1.058 # eV 54 | 55 | 1. Compute Configuration Coordinate Diagram 56 | ------------------------------------------- 57 | 58 | Preparing the CCD Calculations 59 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 60 | 61 | We now are ready to prepare our configuration coordinate diagram. The 62 | configuration coordinate diagram gives us a practical method to depict 63 | the coupling between electron and phonon degrees of freedom. The 64 | potential energy surface in each charge state is plotted as a function 65 | of displacement. The displacement is generated by a linear interpolation 66 | between the ground and excited configurations and also corresponds to 67 | the special phonon mode used in our calculation of the nonradiative 68 | recombination rates. 69 | 70 | The following code can be used to prepare the input files for the ab 71 | initio calculation of the configuration coordinate diagram (example for 72 | ``VASP`` is shown below). 73 | 74 | .. code:: python3 75 | 76 | import os 77 | from pathlib import Path 78 | from shutil import copyfile 79 | from pymatgen import Structure 80 | from nonrad.ccd import get_cc_structures 81 | 82 | # equilibrium structures from your first-principles calculation 83 | ground_files = Path('/path/to/C0/relax/') 84 | ground_struct = Structure.from_file(str(ground_files / 'CONTCAR')) 85 | excited_files = Path('/path/to/C-/relax/') 86 | excited_struct = Structure.from_file(str(excited_files / 'CONTCAR')) 87 | 88 | # output directory that will contain the input files for the CC diagram 89 | cc_dir = Path('/path/to/cc_dir') 90 | os.mkdir(str(cc_dir)) 91 | os.mkdir(str(cc_dir / 'ground')) 92 | os.mkdir(str(cc_dir / 'excited')) 93 | 94 | # displacements as a percentage, this will generate the displacements 95 | # -50%, -37.5%, -25%, -12.5%, 0%, 12.5%, 25%, 37.5%, 50% 96 | displacements = np.linspace(-0.5, 0.5, 9) 97 | 98 | # note: the returned structures won't include the 0% displacement, this is intended 99 | # it can be included by specifying remove_zero=False 100 | ground, excited = get_cc_structures(ground_struct, excited_struct, displacements) 101 | 102 | for i, struct in enumerate(ground): 103 | working_dir = cc_dir / 'ground' / str(i) 104 | os.mkdir(str(working_dir)) 105 | 106 | # write structure and copy necessary input files 107 | struct.to(filename=str(working_dir / 'POSCAR'), fmt='poscar') 108 | for f in ['KPOINTS', 'POTCAR', 'INCAR', 'submit.job']: 109 | copyfile(str(ground_files / f), str(working_dir / f)) 110 | 111 | for i, struct in enumerate(excited): 112 | working_dir = cc_dir / 'excited' / str(i) 113 | os.mkdir(str(working_dir)) 114 | 115 | # write structure and copy necessary input files 116 | struct.to(filename=str(working_dir / 'POSCAR'), fmt='poscar') 117 | for f in ['KPOINTS', 'POTCAR', 'INCAR', 'submit.job']: 118 | copyfile(str(excited_files / f), str(working_dir / f)) 119 | 120 | Before submitting the calculations prepared above, the INCAR files 121 | should be modified to remove the ``NSW`` flag (no relaxation should be 122 | performed). 123 | 124 | One of the nice features provided by the NONRAD code is the 125 | ``get_Q_from_struct`` function, which can determine the Q value from the 126 | interpolated structure and the endpoints. Therefore, we don’t need any 127 | fancy naming schemes or tricks to prepare our potential energy surfaces. 128 | 129 | Extracting the Potential Energy Surface and Relevant Parameters 130 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 131 | 132 | Once the calculations have completed, we can extract the potential 133 | energy surface using the functions provided by NONRAD. The below code 134 | extracts the potential energy surfaces and plots them. Furthermore, it 135 | will extract the dQ value and the phonon frequencies of the potential 136 | energy surfaces. These are 3 input parameters for the calculation of the 137 | nonradiative capture coefficient. 138 | 139 | .. code:: python3 140 | 141 | from glob import glob 142 | from nonrad.ccd import get_dQ, get_PES_from_vaspruns, get_omega_from_PES 143 | 144 | # calculate dQ 145 | dQ = get_dQ(ground_struct, excited_struct) # amu^{1/2} Angstrom 146 | 147 | # this prepares a list of all vasprun.xml's from the CCD calculations 148 | ground_vaspruns = glob(str(cc_dir / 'ground' / '*' / 'vasprun.xml')) 149 | excited_vaspruns = glob(str(cc_dir / 'excited' / '*' / 'vasprun.xml')) 150 | 151 | # remember that the 0% displacement was removed before? we need to add that back in here 152 | ground_vaspruns = ground_vaspruns + [str(ground_files / 'vasprun.xml')] 153 | excited_vaspruns = excited_vaspruns + [str(excited_files / 'vasprun.xml')] 154 | 155 | # extract the potential energy surface 156 | Q_ground, E_ground = get_PES_from_vaspruns(ground_struct, excited_struct, ground_vaspruns) 157 | Q_excited, E_excited = get_PES_from_vaspruns(ground_struct, excited_struct, excited_vaspruns) 158 | 159 | # the energy surfaces are referenced to the minimums, so we need to add dE (defined before) to E_excited 160 | E_excited = dE + E_excited 161 | 162 | fig, ax = plt.subplots(figsize=(5, 5)) 163 | ax.scatter(Q_ground, E_ground, s=10) 164 | ax.scatter(Q_excited, E_excited, s=10) 165 | 166 | # by passing in the axis object, it also plots the fitted curve 167 | q = np.linspace(-1.0, 3.5, 100) 168 | ground_omega = get_omega_from_PES(Q_ground, E_ground, ax=ax, q=q) 169 | excited_omega = get_omega_from_PES(Q_excited, E_excited, ax=ax, q=q) 170 | 171 | ax.set_xlabel('$Q$ [amu$^{1/2}$ $\AA$]') 172 | ax.set_ylabel('$E$ [eV]') 173 | plt.show() 174 | 175 | 176 | 177 | .. image:: tutorial_files/tutorial_8_0.png 178 | 179 | 180 | The resulting input parameters that we have extracted for our 181 | calculation of the nonradiative recombination coefficient are below. 182 | 183 | .. code:: python3 184 | 185 | print(f'dQ = {dQ:7.05f} amu^(1/2) Angstrom, ground_omega = {ground_omega:7.05f} eV, excited_omega = {excited_omega:7.05f} eV') 186 | 187 | 188 | .. parsed-literal:: 189 | 190 | dQ = 1.68588 amu^(1/2) Angstrom, ground_omega = 0.03358 eV, excited_omega = 0.03754 eV 191 | 192 | 193 | 2. Calculate the Electron-Phonon Coupling Matrix Element 194 | -------------------------------------------------------- 195 | 196 | Before computing the el-ph matrix elements, it is highly suggested that 197 | you re-read the `original methodology 198 | paper `__ and the `code 199 | implementation paper `__ to 200 | make sure you understand the details. 201 | 202 | The most important criteria for selecting the geometry in which the 203 | el-ph matrix elements are calculated is the presence of a Kohn-Sham 204 | level associated with the defect in the gap. For the C substitution we 205 | are considering, when the geometry of the defect (:math:`\{Q_0\}`) 206 | corresponds to the neutral charge state, a well-defined Kohn-Sham state 207 | associated with the defect is clear and sits in the gap. Therefore, we 208 | compute the el-ph matrix elements by expanding around this 209 | configuration. 210 | 211 | To perform this calculation with ``VASP``, access to version 5.4.4 or 212 | greater is necessary. The calculation amounts to calculating the overlap 213 | :math:`\langle \psi_i (0) \vert \psi_f (Q) \rangle` (where :math:`Q = 0` 214 | corresponds to the geometry :math:`\{Q_0\}` described above) as a 215 | function of :math:`Q` and computing the slope with respect to :math:`Q`. 216 | The el-ph matrix element is then 217 | :math:`W_{if} = (\epsilon_f - \epsilon_i) \langle \psi_i (0) \vert \delta \psi_f (Q) \rangle`. 218 | For each :math:`Q`, one sets up the calculation by copying the 219 | ``INCAR``, ``POSCAR``, ``POTCAR``, ``KPOINTS``, and ``WAVECAR`` from :math:`Q = 0` to a new 220 | directory and sets ``LWSWQ = True`` in the ``INCAR`` file. The 221 | ``WAVECAR`` from the :math:`Q` configuration is copied to 222 | ``WAVECAR.qqq``. This calculation produces the file ``WSWQ``, which 223 | includes the overlap information for all bands and kpoints. These files 224 | can then be parsed to obtain the matrix element using NONRAD as below. 225 | 226 | .. code:: python3 227 | 228 | from nonrad.ccd import get_Q_from_struct 229 | from nonrad.elphon import get_Wif_from_WSWQ 230 | 231 | # this generates a list of tuples where the first value of the tuple is a Q value 232 | # and the second is the path to the WSWQ file that corresponds to that tuple 233 | WSWQs = [] 234 | for d in glob(str(cc_dir / 'ground' / '*')): 235 | pd = Path(d) 236 | Q = get_Q_from_struct(ground_struct, excited_struct, str(pd / 'CONTCAR')) 237 | path_wswq = str(pd / 'WSWQ') 238 | WSWQs.append((Q, path_wswq)) 239 | 240 | # by passing a figure object, we can inspect the resulting plots 241 | fig = plt.figure(figsize=(12, 5)) 242 | Wifs = get_Wif_from_WSWQ(WSWQs, str(ground_files / 'vasprun.xml'), 192, [189, 190, 191], spin=1, fig=fig) 243 | plt.tight_layout() 244 | plt.show() 245 | 246 | 247 | 248 | .. image:: tutorial_files/tutorial_12_0.png 249 | 250 | 251 | We pass as input, the indices of the 3 valence bands. What we find is 252 | that the valence band that is pushed down in energy has the greatest 253 | el-ph matrix element. This makes sense because it is pushed down by the 254 | interaction with the defect state. 255 | 256 | **NOTE**: We highly recommend passing a figure object to view the 257 | resulting plot. This ensures that the value obtained is reasonable. 258 | 259 | The resulting values of the matrix elements are shown below. They are in 260 | units of eV amu\ :math:`^{-1/2}` :math:`\unicode{xC5}^{-1}`. The VBM of 261 | wz-GaN has three (nearly degenerate) bands, so we must average over the 262 | matrix elements. The resulting value can then be directly input into the 263 | nonradiative capture calculation. 264 | 265 | .. code:: python3 266 | 267 | Wif = np.sqrt(np.mean([x[1]**2 for x in Wifs])) 268 | print(Wifs, Wif) 269 | 270 | 271 | .. parsed-literal:: 272 | 273 | [(189, 0.08081487879834824), (190, 0.020450559002109615), (191, 0.0259145184003146)] 0.05040116487612406 274 | 275 | 276 | Alternative Method (Note: not publication quality) 277 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 278 | 279 | Another method for obtaining the Wif value would be to use the 280 | pseudo-wavefunctions from the ``WAVECAR`` files. This will neglect the 281 | core information. For some defect systems, this is not a bad 282 | approximation. The quality of the result can generally be judged by the 283 | overlap at :math:`Q = 0`. If the overlap is almost zero (maybe < 0.05), 284 | then the result should be reasonably reliable. Please only use this to 285 | get a rough idea, the above method is preferred. This is facilitated by 286 | the ``get_Wif_from_wavecars`` function. 287 | 288 | .. code:: python3 289 | 290 | from nonrad.elphon import get_Wif_from_wavecars 291 | 292 | # this generates a list of tuples where the first value of the tuple is a Q value 293 | # and the second is the path to the WAVECAR file that corresponds to that tuple 294 | wavecars = [] 295 | for d in glob(str(cc_dir / 'ground' / '*')): 296 | pd = Path(d) 297 | Q = get_Q_from_struct(ground_struct, excited_struct, str(pd / 'CONTCAR')) 298 | path_wavecar = str(pd / 'WAVECAR') 299 | wavecars.append((Q, path_wavecar)) 300 | 301 | # by passing a figure object, we can inspect the resulting plots 302 | fig = plt.figure(figsize=(12, 5)) 303 | Wifs = get_Wif_from_wavecars(wavecars, str(ground_files / 'WAVECAR'), 192, [189, 190, 191], spin=1, fig=fig) 304 | plt.tight_layout() 305 | plt.show() 306 | 307 | 308 | 309 | .. image:: tutorial_files/tutorial_16_0.png 310 | 311 | 312 | As we can see, the results are reasonably close because the Q = 0 value 313 | is somewhat low. 314 | 315 | .. code:: python3 316 | 317 | print(Wifs, np.sqrt(np.mean([x[1]**2 for x in Wifs]))) 318 | 319 | 320 | .. parsed-literal:: 321 | 322 | [(189, 0.08609599795923484), (190, 0.030574033957316595), (191, 0.019013362685731866)] 0.05387887767217285 323 | 324 | 325 | 3. Compute Scaling Parameters 326 | ----------------------------- 327 | 328 | When calculating the capture coefficient, we need to take into account 329 | two effects. First is the coulombic interaction between the carrier and 330 | defect. This occurs when the carrier is captured into a defect with a 331 | non-zero charge state. Second, there is the effect on the el-ph matrix 332 | element as a result of using a finite-size charged supercell. This leads 333 | to a suppression or enhancement of the charge density near the defect 334 | and would not occur in an infinitely large supercell. 335 | 336 | Sommerfeld Parameter 337 | ^^^^^^^^^^^^^^^^^^^^ 338 | 339 | The Sommerfeld parameter captures the long-range coulombic interaction 340 | that can affect the capture rates. The interaction can be attractive or 341 | repulsive and may enhance or suppress the resulting rate. 342 | 343 | For our system, we have the C substitution capturing a hole in the 344 | negative charge state, so there will be a long-range coulombic 345 | attraction that enhances the capture rates. One input parameter for the 346 | Sommerfeld parameter is the Z value. We define it as 347 | :math:`Z = q_d / q_c`, where :math:`q_d` is the charge of the defect and 348 | :math:`q_c` is the charge of the carrier. For a negatively charge defect 349 | (:math:`q_d = -1`) interacting with a hole (:math:`q_c = +1`), we have 350 | :math:`Z = -1`. :math:`Z < 0` is an attractive center, while 351 | :math:`Z > 0` is a repulsive center. 352 | 353 | Below, we calculate the scaling coefficient. Note, we use the hole 354 | effective mass (because we are capturing a hole) and the static 355 | dielectric constant. 356 | 357 | .. code:: python3 358 | 359 | from nonrad.scaling import sommerfeld_parameter 360 | 361 | Z = -1 362 | m_eff = 0.18 # hole effective mass of GaN 363 | eps_0 = 8.9 # static dielectric constant of GaN 364 | 365 | # We can compute the Sommerfeld parameter at a single temperature 366 | print(f'Sommerfeld Parameter @ 300K: {sommerfeld_parameter(300, Z, m_eff, eps_0):7.05f}') 367 | 368 | # or we can compute it at a range of temperatures 369 | T = np.linspace(25, 800, 1000) 370 | f = sommerfeld_parameter(T, Z, m_eff, eps_0) 371 | 372 | 373 | .. parsed-literal:: 374 | 375 | Sommerfeld Parameter @ 300K: 7.77969 376 | 377 | 378 | Charged Supercell Effects 379 | ^^^^^^^^^^^^^^^^^^^^^^^^^ 380 | 381 | Ideally, one could always calculate the el-ph matrix elements in the 382 | neutral charge state, and for many defects, this is possible. However, 383 | sometimes it is unavoidable to use a charged defect cell for computing 384 | the matrix elements. As a result of the charge on the supercell, an 385 | interaction between the defect and the delocalized band edges occurs. 386 | This leads to an enhancement or suppression of the charge density near 387 | the defect that would not exist in an infinite-size supercell, and 388 | therefore, a scaling of the el-ph matrix element. 389 | 390 | For the C substitution that we are considering, the el-ph matrix element 391 | is computed in the neutral charge state, so *no correction is 392 | necessary*. For illustration purposes, we shall examine how we would 393 | compute this scaling coefficient *if it were necessary* by studying the 394 | wavefunctions in the negative charge state. Here, we have a spurious 395 | interaction that suppresses or enhances the charge density of the bulk 396 | wavefunctions near the charged defect. The scaling coefficient is 397 | calculated by comparing the radial distribution of the charge density to 398 | a purely homogenous distribution. The function 399 | ``charged_supercell_scaling`` computes the scaling factor. 400 | 401 | Below is an example of the interaction with the valence band: 402 | 403 | .. code:: python3 404 | 405 | from nonrad.scaling import charged_supercell_scaling 406 | 407 | wavecar_path = str(excited_files / 'WAVECAR') 408 | 409 | fig = plt.figure(figsize=(12, 5)) 410 | factor = charged_supercell_scaling(wavecar_path, 189, def_index=192, fig=fig) 411 | plt.tight_layout() 412 | plt.show() 413 | 414 | print('scaling =', 1 / factor) 415 | 416 | 417 | 418 | .. image:: tutorial_files/tutorial_22_0.png 419 | 420 | 421 | .. parsed-literal:: 422 | 423 | scaling = 0.9259259259259259 424 | 425 | 426 | The left-most plot is of the cumulative charge density (blue) against a 427 | homogenous distribution (red). The scaling parameter that brings the two 428 | into agreement is shown in the second plot. A plateau is found around 429 | ~2-3 :math:`\unicode{xC5}`. This is the value that we use for the 430 | scaling. If we had calculated the el-ph matrix elements in the negative 431 | charge state, we would scale the capture coefficient by 1 over this 432 | value squared (printed above). For completeness, the right-most plot is 433 | the derivative of the scaling coefficient, which provides an algorithmic 434 | way to find the plateau. 435 | 436 | Below we show the process for the interaction with the conduction band. 437 | 438 | .. code:: python3 439 | 440 | fig = plt.figure(figsize=(12, 5)) 441 | factor = charged_supercell_scaling(wavecar_path, 193, def_index=192, fig=fig) 442 | plt.tight_layout() 443 | plt.show() 444 | 445 | print('scaling =', 1 / factor) 446 | 447 | 448 | 449 | .. image:: tutorial_files/tutorial_24_0.png 450 | 451 | 452 | .. parsed-literal:: 453 | 454 | scaling = 1.4492753623188408 455 | 456 | 457 | Here we see that the distribution is suppressed near the defect. 458 | 459 | 4. Compute the Nonradiative Capture Coefficient 460 | ----------------------------------------------- 461 | 462 | We are now ready to compute the capture coefficient. The last input 463 | parameter we need to think about is the configurational degeneracy. For 464 | a C substitution, there are 4 identical defect configurations (one along 465 | each bond) that the hole can be captured into. 466 | 467 | .. code:: python3 468 | 469 | from nonrad import get_C 470 | 471 | g = 4 # configurational degeneracy 472 | volume = ground_struct.volume # Angstrom^3 473 | 474 | # we pass in T, which is a numpy array 475 | # we will get the capture coefficient at each of these temperatures 476 | Ctilde = get_C(dQ, dE, excited_omega, ground_omega, Wif, volume, g=g, T=T) 477 | 478 | # apply Sommerfeld parameter, evaluated at the same temperatures 479 | C = f * Ctilde 480 | 481 | fig, ax = plt.subplots(1, 2, figsize=(10, 5)) 482 | ax[0].semilogy(T, C) 483 | ax[0].set_xlabel('$T$ [K]') 484 | ax[0].set_ylabel('$C_p$ [cm$^{3}$ s$^{-1}$]') 485 | ax[1].semilogy(1000 / T[200:], C[200:]) 486 | ax[1].set_xlabel('$1000 / T$ [K$^{-1}$]') 487 | ax[1].set_ylabel('$C_p$ [cm$^{3}$ s$^{-1}$]') 488 | plt.tight_layout() 489 | plt.show() 490 | 491 | 492 | 493 | .. image:: tutorial_files/tutorial_27_0.png 494 | 495 | 496 | We may also want to calculate the capture cross section, 497 | :math:`\sigma = C / \langle v \rangle`. We can do this using the 498 | ``thermal_velocity`` function. 499 | 500 | .. code:: python3 501 | 502 | from nonrad.scaling import thermal_velocity 503 | 504 | sigma = C / thermal_velocity(T, m_eff) # cm^2 505 | sigma *= (1e8)**2 # (cm to Angstrom)^2 506 | 507 | fig, ax = plt.subplots(1, 2, figsize=(10, 5)) 508 | ax[0].semilogy(T, sigma) 509 | ax[0].set_xlabel('$T$ [K]') 510 | ax[0].set_ylabel('$\sigma$ [$\AA^{2}$]') 511 | ax[1].semilogy(1000 / T[200:], sigma[200:]) 512 | ax[1].set_xlabel('$1000 / T$ [K$^{-1}$]') 513 | ax[1].set_ylabel('$\sigma$ [$\AA^{2}$]') 514 | plt.tight_layout() 515 | plt.show() 516 | 517 | 518 | 519 | .. image:: tutorial_files/tutorial_29_0.png 520 | 521 | -------------------------------------------------------------------------------- /docs/tutorial_files/tutorial_12_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mturiansky/nonrad/d5bd3308c84f1052c9f30fe4e1f1bbe41f9fc3d2/docs/tutorial_files/tutorial_12_0.png -------------------------------------------------------------------------------- /docs/tutorial_files/tutorial_16_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mturiansky/nonrad/d5bd3308c84f1052c9f30fe4e1f1bbe41f9fc3d2/docs/tutorial_files/tutorial_16_0.png -------------------------------------------------------------------------------- /docs/tutorial_files/tutorial_22_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mturiansky/nonrad/d5bd3308c84f1052c9f30fe4e1f1bbe41f9fc3d2/docs/tutorial_files/tutorial_22_0.png -------------------------------------------------------------------------------- /docs/tutorial_files/tutorial_24_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mturiansky/nonrad/d5bd3308c84f1052c9f30fe4e1f1bbe41f9fc3d2/docs/tutorial_files/tutorial_24_0.png -------------------------------------------------------------------------------- /docs/tutorial_files/tutorial_27_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mturiansky/nonrad/d5bd3308c84f1052c9f30fe4e1f1bbe41f9fc3d2/docs/tutorial_files/tutorial_27_0.png -------------------------------------------------------------------------------- /docs/tutorial_files/tutorial_29_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mturiansky/nonrad/d5bd3308c84f1052c9f30fe4e1f1bbe41f9fc3d2/docs/tutorial_files/tutorial_29_0.png -------------------------------------------------------------------------------- /docs/tutorial_files/tutorial_2_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mturiansky/nonrad/d5bd3308c84f1052c9f30fe4e1f1bbe41f9fc3d2/docs/tutorial_files/tutorial_2_0.png -------------------------------------------------------------------------------- /docs/tutorial_files/tutorial_8_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mturiansky/nonrad/d5bd3308c84f1052c9f30fe4e1f1bbe41f9fc3d2/docs/tutorial_files/tutorial_8_0.png -------------------------------------------------------------------------------- /nonrad/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Chris G. Van de Walle 2 | # Distributed under the terms of the MIT License. 3 | 4 | """Init module for nonrad. 5 | 6 | This module provides the main implementation to evaluate the nonradiative 7 | capture coefficient from first-principles. 8 | """ 9 | 10 | import importlib.metadata 11 | 12 | from nonrad.nonrad import get_C 13 | 14 | __all__ = ['get_C'] 15 | __author__ = 'Mark E. Turiansky' 16 | __email__ = 'mturiansky@physics.ucsb.edu' 17 | __version__ = importlib.metadata.version('nonrad') 18 | -------------------------------------------------------------------------------- /nonrad/ccd.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Chris G. Van de Walle 2 | # Distributed under the terms of the MIT License. 3 | 4 | """Convenience utilities for nonrad. 5 | 6 | This module contains various convenience utilities for working with and 7 | preparing input for nonrad. 8 | """ 9 | 10 | from typing import Union 11 | 12 | import numpy as np 13 | from pymatgen.core import Structure 14 | from pymatgen.io.vasp.outputs import Vasprun 15 | from scipy.optimize import curve_fit 16 | 17 | from nonrad.constants import AMU2KG, ANGS2M, EV2J, HBAR 18 | 19 | 20 | def get_cc_structures( 21 | ground: Structure, 22 | excited: Structure, 23 | displacements: np.ndarray, 24 | remove_zero: bool = True 25 | ) -> tuple[list, list]: 26 | """Generate the structures for a CC diagram. 27 | 28 | Parameters 29 | ---------- 30 | ground : pymatgen.core.structure.Structure 31 | pymatgen structure corresponding to the ground (final) state 32 | excited : pymatgen.core.structure.Structure 33 | pymatgen structure corresponding to the excited (initial) state 34 | displacements : list(float) 35 | list of displacements to compute the perturbed structures. Note: the 36 | displacements are for only one potential energy surface and will be 37 | applied to both (e.g. displacements=np.linspace(-0.1, 0.1, 5)) will 38 | return 10 structures 5 of the ground state displaced at +-10%, +-5%, 39 | and 0% and 5 of the excited state displaced similarly) 40 | remove_zero : bool 41 | remove 0% displacement from list (default is True) 42 | 43 | Returns 44 | ------- 45 | ground_structs = list(pymatgen.core.structure.Struture) 46 | a list of structures corresponding to the displaced ground state 47 | excited_structs = list(pymatgen.core.structure.Structure) 48 | a list of structures corresponding to the displaced excited state 49 | """ 50 | displacements = np.array(displacements) 51 | if remove_zero: 52 | displacements = displacements[displacements != 0.] 53 | ground_structs = ground.interpolate(excited, nimages=displacements) 54 | excited_structs = ground.interpolate(excited, nimages=(displacements + 1.)) 55 | return ground_structs, excited_structs 56 | 57 | 58 | def get_dQ(ground: Structure, excited: Structure) -> float: 59 | """Calculate dQ from the initial and final structures. 60 | 61 | Parameters 62 | ---------- 63 | ground : pymatgen.core.structure.Structure 64 | pymatgen structure corresponding to the ground (final) state 65 | excited : pymatgen.core.structure.Structure 66 | pymatgen structure corresponding to the excited (initial) state 67 | 68 | Returns 69 | ------- 70 | float 71 | the dQ value (amu^{1/2} Angstrom) 72 | """ 73 | return np.sqrt(np.sum(list(map( 74 | lambda x: x[0].distance(x[1])**2 * x[0].specie.atomic_mass, 75 | zip(ground, excited) 76 | )))) 77 | 78 | 79 | def get_Q_from_struct( 80 | ground: Structure, 81 | excited: Structure, 82 | struct: Union[Structure, str], 83 | tol: float = 1e-4, 84 | nround: int = 5, 85 | ) -> float: 86 | """Calculate the Q value for a given structure. 87 | 88 | This function calculates the Q value for a given structure, knowing the 89 | endpoints and assuming linear interpolation. 90 | 91 | Parameters 92 | ---------- 93 | ground : pymatgen.core.structure.Structure 94 | pymatgen structure corresponding to the ground (final) state 95 | excited : pymatgen.core.structure.Structure 96 | pymatgen structure corresponding to the excited (initial) state 97 | struct : pymatgen.core.structure.Structure or str 98 | pymatgen structure corresponding to the structure we want to calculate 99 | the Q value for (may also be a path to a file containing a structure) 100 | tol : float 101 | distance cutoff to throw away coordinates for determining Q (sites that 102 | don't move very far could introduce numerical noise) 103 | nround : int 104 | number of decimal places to round to in determining Q value 105 | 106 | Returns 107 | ------- 108 | float 109 | the Q value (amu^{1/2} Angstrom) of the structure 110 | """ 111 | if isinstance(struct, str): 112 | tstruct = Structure.from_file(struct) 113 | else: 114 | tstruct = struct 115 | 116 | dQ = get_dQ(ground, excited) 117 | 118 | excited_coords = excited.cart_coords 119 | ground_coords = ground.cart_coords 120 | struct_coords = tstruct.cart_coords 121 | 122 | dx = excited_coords - ground_coords 123 | ind = np.abs(dx) > tol 124 | 125 | poss_x = np.round((struct_coords - ground_coords)[ind] / dx[ind], nround) 126 | val, count = np.unique(poss_x, return_counts=True) 127 | 128 | return dQ * val[np.argmax(count)] 129 | 130 | 131 | def get_PES_from_vaspruns( 132 | ground: Structure, 133 | excited: Structure, 134 | vasprun_paths: list[str], 135 | tol: float = 0.001 136 | ) -> tuple[np.ndarray, np.ndarray]: 137 | """Extract the potential energy surface (PES) from vasprun.xml files. 138 | 139 | This function reads in vasprun.xml files to extract the energy and Q value 140 | of each calculation and then returns it as a list. 141 | 142 | Parameters 143 | ---------- 144 | ground : pymatgen.core.structure.Structure 145 | pymatgen structure corresponding to the ground (final) state 146 | excited : pymatgen.core.structure.Structure 147 | pymatgen structure corresponding to the excited (initial) state 148 | vasprun_paths : list(strings) 149 | a list of paths to each of the vasprun.xml files that make up the PES. 150 | Note that the minimum (0% displacement) should be included in the list, 151 | and each path should end in 'vasprun.xml' (e.g. /path/to/vasprun.xml) 152 | tol : float 153 | tolerance to pass to get_Q_from_struct 154 | 155 | Returns 156 | ------- 157 | Q : np.array(float) 158 | array of Q values (amu^{1/2} Angstrom) corresponding to each vasprun 159 | energy : np.array(float) 160 | array of energies (eV) corresponding to each vasprun 161 | """ 162 | num = len(vasprun_paths) 163 | Q, energy = (np.zeros(num), np.zeros(num)) 164 | for i, vr_fname in enumerate(vasprun_paths): 165 | vr = Vasprun(vr_fname, parse_dos=False, parse_eigen=False) 166 | Q[i] = get_Q_from_struct(ground, excited, vr.structures[-1], tol=tol) 167 | energy[i] = vr.final_energy 168 | return Q, (energy - np.min(energy)) 169 | 170 | 171 | def get_omega_from_PES( 172 | Q: np.ndarray, 173 | energy: np.ndarray, 174 | Q0: Union[float, None] = None, 175 | ax=None, 176 | q: Union[np.ndarray, None] = None 177 | ) -> float: 178 | """Calculate the harmonic phonon frequency for the given PES. 179 | 180 | Parameters 181 | ---------- 182 | Q : np.array(float) 183 | array of Q values (amu^{1/2} Angstrom) corresponding to each vasprun 184 | energy : np.array(float) 185 | array of energies (eV) corresponding to each vasprun 186 | Q0 : float 187 | fix the minimum of the parabola (default is None) 188 | ax : matplotlib.axes.Axes 189 | optional axis object to plot the resulting fit (default is None) 190 | q : np.array(float) 191 | array of Q values to evaluate the fitting function at 192 | 193 | Returns 194 | ------- 195 | float 196 | harmonic phonon frequency from the PES in eV 197 | """ 198 | def f(Q, omega, Q0, dE): 199 | return 0.5 * omega**2 * (Q - Q0)**2 + dE 200 | 201 | # set bounds to restrict Q0 to the given Q0 value 202 | bounds = (-np.inf, np.inf) if Q0 is None else \ 203 | ([-np.inf, Q0 - 1e-10, -np.inf], [np.inf, Q0, np.inf]) 204 | popt, _ = curve_fit(f, Q, energy, bounds=bounds) # pylint: disable=W0632 205 | 206 | # optional plotting to check fit 207 | if ax is not None: 208 | q_L = np.max(Q) - np.min(Q) 209 | if q is None: 210 | q = np.linspace(np.min(Q) - 0.1 * q_L, np.max(Q) + 0.1 * q_L, 1000) 211 | ax.plot(q, f(q, *popt)) 212 | 213 | return HBAR * popt[0] * np.sqrt(EV2J / (ANGS2M**2 * AMU2KG)) 214 | 215 | 216 | def get_barrier_harmonic( 217 | dQ: float, 218 | dE: float, 219 | wi: float, 220 | wf: float 221 | ) -> Union[float, None]: 222 | """Calculate the barrier height within the Harmonic approximation. 223 | 224 | Parameters 225 | ---------- 226 | dQ : float 227 | displacement between harmonic oscillators in amu^{1/2} Angstrom 228 | dE : float 229 | energy offset between the two harmonic oscillators 230 | wi, wf : float 231 | frequencies of the harmonic oscillators in eV 232 | 233 | Returns 234 | ------- 235 | float, None 236 | barrier energy in eV or None if no crossing found 237 | """ 238 | swi = wi / HBAR / np.sqrt(EV2J / (ANGS2M**2 * AMU2KG)) 239 | swf = wf / HBAR / np.sqrt(EV2J / (ANGS2M**2 * AMU2KG)) 240 | r = np.roots([ 241 | 0.5 * (swi**2 - swf**2), 242 | -swi**2 * dQ, 243 | dE + 0.5 * swi**2 * dQ**2, 244 | ]) 245 | 246 | # no crossing point was found 247 | if not np.any(np.isreal(r)): 248 | return None 249 | 250 | return np.min(0.5 * swf**2 * r[np.isreal(r)]**2) - dE 251 | -------------------------------------------------------------------------------- /nonrad/constants.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Chris G. Van de Walle 2 | # Distributed under the terms of the MIT License. 3 | 4 | """Constants used by various parts of the code.""" 5 | 6 | import scipy.constants as const 7 | 8 | HBAR = const.hbar / const.e # in units of eV.s 9 | EV2J = const.e # 1 eV in Joules 10 | AMU2KG = const.physical_constants['atomic mass constant'][0] 11 | ANGS2M = 1e-10 # angstrom in meters 12 | -------------------------------------------------------------------------------- /nonrad/elphon.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Chris G. Van de Walle 2 | # Distributed under the terms of the MIT License. 3 | 4 | """Utilities to compute electron-phonon coupling. 5 | 6 | This module provides various utilities to evaluate electron-phonon coupling 7 | strength using different first-principles codes. 8 | """ 9 | 10 | import re 11 | from collections.abc import Sequence 12 | from typing import Union 13 | 14 | import numpy as np 15 | from monty.io import zopen 16 | from pymatgen.electronic_structure.core import Spin 17 | from pymatgen.io.vasp.outputs import BSVasprun, Wavecar 18 | from pymatgen.io.wannier90 import Unk 19 | 20 | 21 | def _compute_matel(psi0: np.ndarray, psi1: np.ndarray) -> float: 22 | """Compute the inner product of the two wavefunctions. 23 | 24 | Parameters 25 | ---------- 26 | psi0 : np.array 27 | first wavefunction 28 | psi1 : np.array 29 | second wavefunction 30 | 31 | Returns 32 | ------- 33 | float 34 | inner product np.abs() 35 | """ 36 | npsi0 = psi0 / np.sqrt(np.abs(np.vdot(psi0, psi0))) 37 | npsi1 = psi1 / np.sqrt(np.abs(np.vdot(psi1, psi1))) 38 | return np.abs(np.vdot(npsi0, npsi1)) 39 | 40 | 41 | def get_Wif_from_wavecars( 42 | wavecars: list, 43 | init_wavecar_path: str, 44 | def_index: int, 45 | bulk_index: Union[np.ndarray, Sequence[int]], 46 | spin: int = 0, 47 | kpoint: int = 1, 48 | fig=None 49 | ) -> list: 50 | """Compute the electron-phonon matrix element using the WAVECARs. 51 | 52 | This function reads in the pseudo-wavefunctions from the WAVECAR files and 53 | computes the overlaps necessary. 54 | 55 | *** WARNING: USE AT YOUR OWN RISK *** 56 | Because these are pseudo-wavefunctions, the core information from the PAWs 57 | is missing. As a result, the resulting Wif value may be unreliable. A good 58 | test of this is how close the Q=0 overlap is to 0. (it would be exactly 0. 59 | if you include the corrections from the PAWs). This should only be used 60 | to get a preliminary idea of the Wif value. 61 | *************** 62 | 63 | Parameters 64 | ---------- 65 | wavecars : list((Q, wavecar_path)) 66 | a list of tuples where the first value is the Q and the second is the 67 | path to the WAVECAR file 68 | init_wavecar_path : string 69 | path to the initial wavecar for computing overlaps 70 | def_index : int 71 | index corresponding to the defect wavefunction (1-based indexing) 72 | bulk_index : int, list(int) 73 | index or list of indices corresponding to the bulk wavefunction 74 | (1-based indexing) 75 | spin : int 76 | spin channel to read from (0 - up, 1 - down) 77 | kpoint : int 78 | kpoint to read from (defaults to the first kpoint) 79 | fig : matplotlib.figure.Figure 80 | optional figure object to plot diagnostic information 81 | 82 | Returns 83 | ------- 84 | list((bulk_index, Wif)) 85 | electron-phonon matrix element Wif in units of 86 | eV amu^{-1/2} Angstrom^{-1} for each bulk_index 87 | """ 88 | bulk_index = np.array(bulk_index, ndmin=1) 89 | initial_wavecar = Wavecar(init_wavecar_path) 90 | if initial_wavecar.spin == 2: 91 | psi_i = initial_wavecar.coeffs[spin][kpoint-1][def_index-1] 92 | else: 93 | psi_i = initial_wavecar.coeffs[kpoint-1][def_index-1] 94 | 95 | Nw, Nbi = (len(wavecars), len(bulk_index)) 96 | Q, matels, deig = (np.zeros(Nw+1), np.zeros((Nbi, Nw+1)), np.zeros(Nbi)) 97 | 98 | # first compute the Q = 0 values and eigenvalue differences 99 | for i, bi in enumerate(bulk_index): 100 | if initial_wavecar.spin == 2: 101 | psi_f = initial_wavecar.coeffs[spin][kpoint-1][bi-1] 102 | deig[i] = initial_wavecar.band_energy[spin][kpoint-1][bi-1][0] - \ 103 | initial_wavecar.band_energy[spin][kpoint-1][def_index-1][0] 104 | else: 105 | psi_f = initial_wavecar.coeffs[kpoint-1][bi-1] 106 | deig[i] = initial_wavecar.band_energy[kpoint-1][bi-1][0] - \ 107 | initial_wavecar.band_energy[kpoint-1][def_index-1][0] 108 | matels[i, Nw] = _compute_matel(psi_i, psi_f) 109 | deig = np.abs(deig) 110 | 111 | # now compute for each Q 112 | for i, (q, fname) in enumerate(wavecars): 113 | Q[i] = q 114 | final_wavecar = Wavecar(fname) 115 | for j, bi in enumerate(bulk_index): 116 | if final_wavecar.spin == 2: 117 | psi_f = final_wavecar.coeffs[spin][kpoint-1][bi-1] 118 | else: 119 | psi_f = final_wavecar.coeffs[kpoint-1][bi-1] 120 | matels[j, i] = _compute_matel(psi_i, psi_f) 121 | 122 | if fig is not None: 123 | ax = fig.subplots(1, Nbi) 124 | ax = np.array(ax, ndmin=1) 125 | for a, i in zip(ax, range(Nbi)): 126 | a.scatter(Q, matels[i, :]) 127 | a.set_title(f'{bulk_index[i]}') 128 | 129 | return [(bi, deig[i] * np.mean(np.abs(np.gradient(matels[i, :], Q)))) 130 | for i, bi in enumerate(bulk_index)] 131 | 132 | 133 | def get_Wif_from_UNK( 134 | unks: list, 135 | init_unk_path: str, 136 | def_index: int, 137 | bulk_index: Union[np.ndarray, Sequence[int]], 138 | eigs: Sequence[float], 139 | fig=None 140 | ) -> list: 141 | """Compute the electron-phonon matrix element using UNK files. 142 | 143 | Evaluate the electron-phonon coupling matrix element using the information 144 | stored in the given UNK files. This is compatible with any first-principles 145 | code that write to the wannier90 UNK file format. The onus is on the user 146 | to ensure the wavefunctions are valid (i.e., norm-conserving). 147 | 148 | Parameters 149 | ---------- 150 | unks: list((Q, unk_path)) 151 | a list of tuples where the first value is the Q and the second is the 152 | path to the UNK file 153 | init_unk_path : string 154 | path to the initial unk file for computing overlaps 155 | def_index : int 156 | index corresponding to the defect wavefunction (1-based indexing) 157 | bulk_index : int, list(int) 158 | index or list of indices corresponding to the bulk wavefunction 159 | (1-based indexing) 160 | eigs : np.ndarray 161 | array of eigenvalues in eV where the indices correspond to those given 162 | by def_index and bulk_index 163 | fig : matplotlib.figure.Figure 164 | optional figure object to plot diagnostic information 165 | 166 | Returns 167 | ------- 168 | list((bulk_index, Wif)) 169 | electron-phonon matrix element Wif in units of 170 | eV amu^{-1/2} Angstrom^{-1} for each bulk_index 171 | """ 172 | bulk_index = np.array(bulk_index, ndmin=1) 173 | initial_unk = Unk.from_file(init_unk_path) 174 | psi_i = initial_unk.data[def_index-1].flatten() 175 | 176 | Nu, Nbi = (len(unks), len(bulk_index)) 177 | Q, matels, deig = (np.zeros(Nu+1), np.zeros((Nbi, Nu+1)), np.zeros(Nbi)) 178 | 179 | # first compute the Q = 0 values and eigenvalue differences 180 | for i, bi in enumerate(bulk_index): 181 | psi_f = initial_unk.data[bi-1].flatten() 182 | deig[i] = eigs[bi-1] - eigs[def_index-1] 183 | matels[i, Nu] = _compute_matel(psi_i, psi_f) 184 | deig = np.abs(deig) 185 | 186 | # now compute for each Q 187 | for i, (q, fname) in enumerate(unks): 188 | Q[i] = q 189 | final_unk = Unk.from_file(fname) 190 | for j, bi in enumerate(bulk_index): 191 | psi_f = final_unk.data[bi-1].flatten() 192 | matels[j, i] = _compute_matel(psi_i, psi_f) 193 | 194 | print(matels) 195 | 196 | if fig is not None: 197 | ax = fig.subplots(1, Nbi) 198 | ax = np.array(ax, ndmin=1) 199 | for a, i in zip(ax, range(Nbi)): 200 | a.scatter(Q, matels[i, :]) 201 | a.set_title(f'{bulk_index[i]}') 202 | 203 | return [(bi, deig[i] * np.mean(np.abs(np.gradient(matels[i, :], Q)))) 204 | for i, bi in enumerate(bulk_index)] 205 | 206 | 207 | def _read_WSWQ(fname: str) -> dict: 208 | """Read the WSWQ file from VASP. 209 | 210 | Parameters 211 | ---------- 212 | fname : string 213 | path to the WSWQ file to read 214 | 215 | Returns 216 | ------- 217 | dict(dict) 218 | a dict of dicts that takes keys (spin, kpoint) and (initial, final) as 219 | indices and maps it to a complex number 220 | """ 221 | # whoa, this is horrific 222 | wswq: dict[Union[tuple[int, int], None], dict[tuple[int, int], complex]] = {} 223 | current = None 224 | with zopen(fname, 'r') as f: 225 | for line in f: 226 | spin_kpoint = \ 227 | re.search(r'\s*spin=(\d+), kpoint=\s*(\d+)', str(line)) 228 | data = \ 229 | re.search(r'i=\s*(\d+), ' 230 | r'j=\s*(\d+)\s*:\s*([0-9\-.]+)\s+([0-9\-.]+)', 231 | str(line)) 232 | if spin_kpoint: 233 | current = \ 234 | (int(spin_kpoint.group(1)), int(spin_kpoint.group(2))) 235 | wswq[current] = {} 236 | elif data: 237 | wswq[current][(int(data.group(1)), int(data.group(2)))] = \ 238 | complex(float(data.group(3)), float(data.group(4))) 239 | return wswq 240 | 241 | 242 | def get_Wif_from_WSWQ( 243 | wswqs: list, 244 | initial_vasprun: str, 245 | def_index: int, 246 | bulk_index: Union[np.ndarray, Sequence[int]], 247 | spin: int = 0, 248 | kpoint: int = 1, 249 | fig=None 250 | ) -> list: 251 | """Compute the electron-phonon matrix element using the WSWQ files. 252 | 253 | Read in the WSWQ files to obtain the overlaps. Then compute the electron- 254 | phonon matrix elements from the overlaps as a function of Q. 255 | 256 | Parameters 257 | ---------- 258 | wswqs : list((Q, wswq_path)) 259 | a list of tuples where the first value is the Q and the second is the 260 | path to the directory that contains the WSWQ file 261 | initial_vasprun : string 262 | path to the initial vasprun.xml to extract the eigenvalue difference 263 | def_index : int 264 | index corresponding to the defect wavefunction (1-based indexing) 265 | bulk_index : int, list(int) 266 | index or list of indices corresponding to the bulk wavefunction 267 | (1-based indexing) 268 | spin : int 269 | spin channel to read from (0 - up, 1 - down) 270 | kpoint : int 271 | kpoint to read from (defaults to the first kpoint) 272 | fig : matplotlib.figure.Figure 273 | optional figure object to plot diagnostic information 274 | 275 | Returns 276 | ------- 277 | list((bulk_index, Wif)) 278 | electron-phonon matrix element Wif in units of 279 | eV amu^{-1/2} Angstrom^{-1} for each bulk_index 280 | """ 281 | bulk_index = np.array(bulk_index, ndmin=1) 282 | 283 | Nw, Nbi = (len(wswqs), len(bulk_index)) 284 | Q, matels, deig = (np.zeros(Nw+1), np.zeros((Nbi, Nw+1)), np.zeros(Nbi)) 285 | 286 | # first compute the eigenvalue differences 287 | bvr = BSVasprun(initial_vasprun) 288 | sp = Spin.up if spin == 0 else Spin.down 289 | def_eig = bvr.eigenvalues[sp][kpoint-1][def_index-1][0] 290 | for i, bi in enumerate(bulk_index): 291 | deig[i] = bvr.eigenvalues[sp][kpoint-1][bi-1][0] - def_eig 292 | deig = np.abs(deig) 293 | 294 | # now compute for each Q 295 | for i, (q, fname) in enumerate(wswqs): 296 | Q[i] = q 297 | wswq = _read_WSWQ(fname) 298 | for j, bi in enumerate(bulk_index): 299 | matels[j, i] = np.sign(q) * \ 300 | np.abs(wswq[(spin+1, kpoint)][(bi, def_index)]) 301 | 302 | if fig is not None: 303 | ax = fig.subplots(1, Nbi) 304 | ax = np.array(ax, ndmin=1) 305 | for a, i in zip(ax, range(Nbi)): 306 | tq = np.linspace(np.min(Q), np.max(Q), 100) 307 | a.scatter(Q, matels[i, :]) 308 | a.plot(tq, np.polyval(np.polyfit(Q, matels[i, :], 1), tq)) 309 | a.set_title(f'{bulk_index[i]}') 310 | 311 | return [(bi, deig[i] * np.polyfit(Q, matels[i, :], 1)[0]) 312 | for i, bi in enumerate(bulk_index)] 313 | -------------------------------------------------------------------------------- /nonrad/nonrad.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Chris G. Van de Walle 2 | # Distributed under the terms of the MIT License. 3 | 4 | """Core module for nonrad. 5 | 6 | This module provides the main implementation to evaluate the nonradiative 7 | capture coefficient from first-principles. 8 | """ 9 | 10 | import warnings 11 | from typing import Union 12 | 13 | import numpy as np 14 | from scipy import constants as const 15 | from scipy.interpolate import PchipInterpolator, interp1d 16 | 17 | from nonrad.ccd import get_barrier_harmonic 18 | from nonrad.constants import AMU2KG, ANGS2M, EV2J, HBAR 19 | 20 | try: 21 | from numba import njit, vectorize 22 | 23 | @vectorize 24 | def herm_vec(x: float, n: int) -> float: 25 | """Wrap herm function.""" 26 | return herm(x, n) 27 | except ModuleNotFoundError: 28 | from numpy.polynomial.hermite import hermval 29 | 30 | def njit(*args, **kwargs): # pylint: disable=W0613 31 | """Fake njit when numba can't be found.""" 32 | def _njit(func): 33 | return func 34 | return _njit 35 | 36 | def herm_vec(x: float, n: int) -> float: 37 | """Wrap hermval function.""" 38 | return hermval(x, [0.]*n + [1.]) 39 | 40 | 41 | factor = ANGS2M**2 * AMU2KG / HBAR / HBAR / EV2J 42 | Factor2 = const.hbar / ANGS2M**2 / AMU2KG 43 | Factor3 = 1 / HBAR 44 | 45 | # for fast factorial calculations 46 | LOOKUP_TABLE = np.array([ 47 | 1, 1, 2, 6, 24, 120, 720, 5040, 40320, 48 | 362880, 3628800, 39916800, 479001600, 49 | 6227020800, 87178291200, 1307674368000, 50 | 20922789888000, 355687428096000, 6402373705728000, 51 | 121645100408832000, 2432902008176640000], dtype=np.double) 52 | 53 | # range for computing overlaps in overlap_NM, precomputing once saves time 54 | # note: -30 to 30 is an arbitrary region. It should be sufficient, but 55 | # we should probably check this to be safe. 5000 is arbitrary also. 56 | QQ = np.linspace(-30., 30., 5000) 57 | 58 | # grid and weights for Hermite-Gauss quadrature in fast_overlap_NM 59 | hgx, hgw = np.polynomial.hermite.hermgauss(256) 60 | 61 | 62 | @njit(cache=True) 63 | def fact(n: int) -> float: 64 | """Compute the factorial of n.""" 65 | if n > 20: 66 | return LOOKUP_TABLE[-1] * \ 67 | np.prod(np.array(list(range(21, n+1)), dtype=np.double)) 68 | return LOOKUP_TABLE[n] 69 | 70 | 71 | @njit(cache=True) 72 | def herm(x: float, n: int) -> float: 73 | """Recursive definition of hermite polynomial.""" 74 | if n == 0: 75 | return 1. 76 | if n == 1: 77 | return 2. * x 78 | 79 | y1 = 2. * x 80 | dy1 = 2. 81 | for i in range(2, n+1): 82 | yn = 2. * x * y1 - dy1 83 | dyn = 2. * i * y1 84 | y1 = yn 85 | dy1 = dyn 86 | return yn 87 | 88 | 89 | @njit(cache=True) 90 | def overlap_NM( 91 | DQ: float, 92 | w1: float, 93 | w2: float, 94 | n1: int, 95 | n2: int 96 | ) -> float: 97 | """Compute the overlap between two displaced harmonic oscillators. 98 | 99 | This function computes the overlap integral between two harmonic 100 | oscillators with frequencies w1, w2 that are displaced by DQ for the 101 | quantum numbers n1, n2. The integral is computed using the trapezoid 102 | method and the analytic form for the wavefunctions. 103 | 104 | Parameters 105 | ---------- 106 | DQ : float 107 | displacement between harmonic oscillators in amu^{1/2} Angstrom 108 | w1, w2 : float 109 | frequencies of the harmonic oscillators in eV 110 | n1, n2 : integer 111 | quantum number of the overlap integral to calculate 112 | 113 | Returns 114 | ------- 115 | np.longdouble 116 | overlap of the two harmonic oscillator wavefunctions 117 | """ 118 | Hn1Q = herm_vec(np.sqrt(factor*w1)*(QQ-DQ), n1) 119 | Hn2Q = herm_vec(np.sqrt(factor*w2)*(QQ), n2) 120 | 121 | wfn1 = (factor*w1/np.pi)**(0.25)*(1./np.sqrt(2.**n1*fact(n1))) * \ 122 | Hn1Q*np.exp(-(factor*w1)*(QQ-DQ)**2/2.) 123 | wfn2 = (factor*w2/np.pi)**(0.25)*(1./np.sqrt(2.**n2*fact(n2))) * \ 124 | Hn2Q*np.exp(-(factor*w2)*QQ**2/2.) 125 | 126 | return np.trapz(wfn2*wfn1, x=QQ) 127 | 128 | 129 | @njit(cache=True) 130 | def fast_overlap_NM( 131 | dQ: float, 132 | w1: float, 133 | w2: float, 134 | n1: int, 135 | n2: int 136 | ) -> float: 137 | """Compute the overlap between two displaced harmonic oscillators. 138 | 139 | This function computes the overlap integral between two harmonic 140 | oscillators with frequencies w1, w2 that are displaced by dQ for the 141 | quantum numbers n1, n2. The integral is computed using Hermite-Gauss 142 | quadrature; this method requires an order of magnitude less grid points 143 | than the trapezoid method. 144 | 145 | Parameters 146 | ---------- 147 | dQ : float 148 | displacement between harmonic oscillators in amu^{1/2} Angstrom 149 | w1, w2 : float 150 | frequencies of the harmonic oscillators in eV 151 | n1, n2 : integer 152 | quantum number of the overlap integral to calculate 153 | 154 | Returns 155 | ------- 156 | np.longdouble 157 | overlap of the two harmonic oscillator wavefunctions 158 | """ 159 | fw1, fw2 = (factor * w1, factor * w2) 160 | x2Q = np.sqrt(2. / (fw1 + fw2)) 161 | 162 | def _g(Q): 163 | h1 = herm_vec(np.sqrt(fw1)*(Q-dQ), n1) \ 164 | / np.sqrt(2.**n1 * fact(n1)) * (fw1/np.pi)**(0.25) 165 | h2 = herm_vec(np.sqrt(fw2)*Q, n2) \ 166 | / np.sqrt(2.**n2 * fact(n2)) * (fw2/np.pi)**(0.25) 167 | return np.exp(fw1*dQ*(Q-dQ/2.)) * h1 * h2 168 | 169 | return x2Q * np.sum(hgw * _g(x2Q*hgx)) 170 | 171 | 172 | @njit(cache=True) 173 | def analytic_overlap_NM( 174 | DQ: float, 175 | w1: float, 176 | w2: float, 177 | n1: int, 178 | n2: int 179 | ) -> float: 180 | """Compute the overlap between two displaced harmonic oscillators. 181 | 182 | This function computes the overlap integral between two harmonic 183 | oscillators with frequencies w1, w2 that are displaced by DQ for the 184 | quantum numbers n1, n2. The integral is computed using an analytic formula 185 | for the overlap of two displaced harmonic oscillators. The method comes 186 | from B.P. Zapol, Chem. Phys. Lett. 93, 549 (1982). 187 | 188 | Parameters 189 | ---------- 190 | DQ : float 191 | displacement between harmonic oscillators in amu^{1/2} Angstrom 192 | w1, w2 : float 193 | frequencies of the harmonic oscillators in eV 194 | n1, n2 : integer 195 | quantum number of the overlap integral to calculate 196 | 197 | Returns 198 | ------- 199 | np.longdouble 200 | overlap of the two harmonic oscillator wavefunctions 201 | """ 202 | w = np.double(w1 * w2 / (w1 + w2)) 203 | rho = np.sqrt(factor) * np.sqrt(w / 2) * DQ 204 | sinfi = np.sqrt(w1) / np.sqrt(w1 + w2) 205 | cosfi = np.sqrt(w2) / np.sqrt(w1 + w2) 206 | 207 | Pr1 = (-1)**n1 * np.sqrt(2 * cosfi * sinfi) * np.exp(-rho**2) 208 | Ix = 0. 209 | k1 = n2 // 2 210 | k2 = n2 % 2 211 | l1 = n1 // 2 212 | l2 = n1 % 2 213 | for kx in range(k1+1): 214 | for lx in range(l1+1): 215 | k = 2 * kx + k2 216 | l = 2 * lx + l2 # noqa: E741 217 | Pr2 = (fact(n1) * fact(n2))**0.5 / \ 218 | (fact(k)*fact(l)*fact(k1-kx)*fact(l1-lx)) * \ 219 | 2**((k + l - n2 - n1) / 2) 220 | Pr3 = (sinfi**k)*(cosfi**l) 221 | # f = hermval(rho, [0.]*(k+l) + [1.]) 222 | f = herm(np.float64(rho), k+l) 223 | Ix = Ix + Pr1*Pr2*Pr3*f 224 | return Ix 225 | 226 | 227 | def get_C( 228 | dQ: float, 229 | dE: float, 230 | wi: float, 231 | wf: float, 232 | Wif: float, 233 | volume: float, 234 | g: int = 1, 235 | T: Union[float, np.ndarray] = 300., 236 | sigma: Union[str, float] = 'pchip', 237 | occ_tol: float = 1e-5, 238 | overlap_method: str = 'HermiteGauss' 239 | ) -> Union[float, np.ndarray]: 240 | """Compute the nonradiative capture coefficient. 241 | 242 | This function computes the nonradiative capture coefficient following the 243 | methodology of A. Alkauskas et al., Phys. Rev. B 90, 075202 (2014). The 244 | resulting capture coefficient is unscaled [See Eq. (22) of the above 245 | reference]. Our code assumes harmonic potential energy surfaces. 246 | 247 | Parameters 248 | ---------- 249 | dQ : float 250 | displacement between harmonic oscillators in amu^{1/2} Angstrom 251 | dE : float 252 | energy offset between the two harmonic oscillators 253 | wi, wf : float 254 | frequencies of the harmonic oscillators in eV 255 | Wif : float 256 | electron-phonon coupling matrix element in eV amu^{-1/2} Angstrom^{-1} 257 | volume : float 258 | volume of the supercell in Å^3 259 | g : int 260 | degeneracy factor of the final state 261 | T : float, np.array(dtype=float) 262 | temperature or a np.array of temperatures in K 263 | sigma : 'pchip', 'cubic', or float 264 | smearing parameter in eV for replacement of the delta functions with 265 | gaussians. A value of 'pchip' or 'cubic' corresponds to interpolation 266 | instead of gaussian smearing, utilizing PCHIP or cubic spline 267 | interpolaton. PCHIP is preferred to cubic spline as cubic spline can 268 | result in negative values when small rates are found. The default is 269 | 'pchip' and is recommended for improved accuracy. 270 | occ_tol : float 271 | criteria to determine the maximum quantum number for overlaps based on 272 | the Bose weights 273 | overlap_method : str 274 | method for evaluating the overlaps (only the first letter is checked) 275 | allowed values => ['Analytic', 'Integral', 'HermiteGauss'] 276 | 277 | Returns 278 | ------- 279 | float, np.array(dtype=float) 280 | resulting capture coefficient (unscaled) in cm^3 s^{-1} 281 | """ 282 | volume *= (1e-8)**3 283 | kT = (const.k / const.e) * T # [(J / K) * (eV / J)] * K = eV 284 | Z = 1. / (1 - np.exp(-wi / kT)) 285 | 286 | Ni, Nf = (17, 50) # default values 287 | tNi = np.ceil(-np.max(kT) * np.log(occ_tol) / wi).astype(int) 288 | if tNi > Ni: 289 | Ni = tNi 290 | tNf = np.ceil((dE + Ni*wi) / wf).astype(int) 291 | if tNf > Nf: 292 | Nf = tNf 293 | 294 | if (Eb := get_barrier_harmonic(dQ, dE, wi, wf)) is not None \ 295 | and (N_barrier := np.ceil(Eb / wi).astype(int)) > Ni: 296 | warnings.warn('Number of initial phonon states included does not ' 297 | f'reach the barrier height ({N_barrier} > {Ni}). ' 298 | 'You may want to test the sensitivity to occ_tol.', 299 | RuntimeWarning, stacklevel=2) 300 | 301 | # warn if there are large values, can be ignored if you're confident 302 | if Ni > 150 or Nf > 150: 303 | warnings.warn(f'Large value for Ni, Nf encountered: ({Ni}, {Nf})', 304 | RuntimeWarning, stacklevel=2) 305 | 306 | # precompute values of the overlap 307 | ovl = np.zeros((Ni, Nf), dtype=np.longdouble) 308 | for m in np.arange(Ni): 309 | for n in np.arange(Nf): 310 | if overlap_method.lower()[0] == 'a': 311 | ovl[m, n] = analytic_overlap_NM(dQ, wi, wf, m, n) 312 | elif overlap_method.lower()[0] == 'i': 313 | ovl[m, n] = overlap_NM(dQ, wi, wf, m, n) 314 | elif overlap_method.lower()[0] == 'h': 315 | ovl[m, n] = fast_overlap_NM(dQ, wi, wf, m, n) 316 | else: 317 | raise ValueError(f'Invalid overlap method: {overlap_method}') 318 | 319 | t = np.linspace(-Ni*wi, Nf*wf, 5000) 320 | R = 0. 321 | for m in np.arange(Ni-1): 322 | weight_m = np.exp(-m * wi / kT) / Z 323 | if isinstance(sigma, str): 324 | # interpolation to replace delta functions 325 | E, matels = (np.zeros(Nf), np.zeros(Nf)) 326 | for n in np.arange(Nf): 327 | if m == 0: 328 | matel = np.sqrt(Factor2 / 2 / wi) * ovl[1, n] + \ 329 | np.sqrt(Factor3) * dQ * ovl[0, n] 330 | else: 331 | matel = np.sqrt((m+1) * Factor2 / 2 / wi) * ovl[m+1, n] + \ 332 | np.sqrt(m * Factor2 / 2 / wi) * ovl[m-1, n] + \ 333 | np.sqrt(Factor3) * dQ * ovl[m, n] 334 | E[n] = n*wf - m*wi 335 | matels[n] = np.abs(np.conj(matel) * matel) 336 | if sigma[0].lower() == 'c': 337 | f = interp1d(E, matels, kind='cubic', bounds_error=False, 338 | fill_value=0.) 339 | else: 340 | f = PchipInterpolator(E, matels, extrapolate=False) 341 | R = R + weight_m * (f(dE) * np.sum(matels) 342 | / np.trapz(np.nan_to_num(f(t)), x=t)) 343 | else: 344 | # gaussian smearing with given sigma to replace delta functions 345 | for n in np.arange(Nf): 346 | # energy conservation delta function 347 | delta = np.exp(-(dE+m*wi-n*wf)**2/(2.0*sigma**2)) / \ 348 | (sigma*np.sqrt(2.0*np.pi)) 349 | if m == 0: 350 | matel = np.sqrt(Factor2 / 2 / wi) * ovl[1, n] + \ 351 | np.sqrt(Factor3) * dQ * ovl[0, n] 352 | else: 353 | matel = np.sqrt((m+1) * Factor2 / 2 / wi) * ovl[m+1, n] + \ 354 | np.sqrt(m * Factor2 / 2 / wi) * ovl[m-1, n] + \ 355 | np.sqrt(Factor3) * dQ * ovl[m, n] 356 | R = R + weight_m * delta * np.abs(np.conj(matel) * matel) 357 | return 2 * np.pi * g * Wif**2 * volume * R 358 | -------------------------------------------------------------------------------- /nonrad/scaling.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Chris G. Van de Walle 2 | # Distributed under the terms of the MIT License. 3 | 4 | """Utilities for scaling the capture coefficient. 5 | 6 | This module provides various utilities that are necessary to scale the 7 | capture coefficient to the final value. 8 | """ 9 | from itertools import groupby 10 | from typing import Any, Optional, Union 11 | 12 | import numpy as np 13 | from mpmath import whitw 14 | from numpy.polynomial.laguerre import laggauss 15 | from pymatgen.io.vasp.outputs import Wavecar 16 | from scipy import constants as const 17 | from scipy.optimize import curve_fit 18 | 19 | try: 20 | from numba import njit 21 | except ModuleNotFoundError: 22 | def njit(*args, **kwargs): # pylint: disable=W0613 23 | """Fake njit when numba can't be found.""" 24 | def _njit(func): 25 | return func 26 | return _njit 27 | 28 | 29 | def _s_k( 30 | k: Union[float, np.ndarray], 31 | Z: int, 32 | m_eff: float, 33 | eps0: float, 34 | dim: int = 3, 35 | x0: float = 1e-3, 36 | ) -> Union[float, np.ndarray]: 37 | """Compute the Sommerfeld parameter as a function of momentum. 38 | 39 | Parameters 40 | ---------- 41 | k : float, np.array(dtype=float) 42 | wavevector 43 | Z : int 44 | Z = Q / q where Q is the charge of the defect and q is the charge of 45 | the carrier. Z < 0 corresponds to attractive centers and Z > 0 46 | corresponds to repulsive centers 47 | m_eff : float 48 | effective mass of the carrier in units of m_e (electron mass) 49 | eps0 : float 50 | static dielectric constant 51 | dim : int 52 | dimension of the system (3, 2, or 1) 53 | x0 : float 54 | cutoff length for 1D evaluation 55 | method : str 56 | specify method for evaluating sommerfeld parameter ('Integrate' or 57 | 'Analytic'). The default is recommended as the analytic equation may 58 | introduce significant errors for the repulsive case at high T. 59 | 60 | Returns 61 | ------- 62 | float, np.array(dtype=float) 63 | sommerfeld factor evaluated at the given temperature 64 | """ 65 | if Z == 0: 66 | return 0. 67 | 68 | a = (eps0 / m_eff) * const.value('Bohr radius') 69 | nu = Z / a / k 70 | if dim == 1: 71 | def _phi(Z, k, a, x, x0): 72 | nu = np.abs(Z) / a / k 73 | z0 = 2*1j*k*x0 74 | z = 2*1j*k*np.abs(x) + z0 75 | 76 | W1 = lambda nu, z: np.exp(z0/2) * whitw(-1j*nu, 1/2, z) # noqa: E731 77 | W2 = lambda nu, z: np.exp(-z0/2) * whitw(1j*nu, 1/2, -z) # noqa: E731 78 | 79 | D10 = (W1(nu, 1.01*z0) - W1(nu, 0.99*z0))/(2*0.01*z0) 80 | D20 = (W2(nu, 1.01*z0) - W2(nu, 0.99*z0))/(2*0.01*z0) 81 | 82 | N = np.sqrt(np.exp(-np.pi * nu) / 2 / 83 | (np.abs(D10)**2 + np.abs(D20)**2)) 84 | 85 | return N*(D20*W1(nu, z) - D10*W2(nu, z)) 86 | 87 | return np.vectorize(lambda q: np.abs(_phi(Z, q, a, 0., x0*a))**2)(k) 88 | elif dim == 2: 89 | return 2 / (1 + np.exp(2 * np.pi * nu)) 90 | else: 91 | return 2 * np.pi * nu / (np.exp(2 * np.pi * nu) - 1) 92 | 93 | 94 | def sommerfeld_parameter( 95 | T: Union[float, np.ndarray], 96 | Z: int, 97 | m_eff: float, 98 | eps0: float, 99 | dim: int = 3, 100 | x0: float = 1e-3, 101 | method: str = 'Integrate' 102 | ) -> Union[float, np.ndarray]: 103 | """Compute the T-dependent Sommerfeld parameter. 104 | 105 | Computes the sommerfeld parameter at a given temperature using the 106 | definitions in R. Pässler et al., phys. stat. sol. (b) 78, 625 (1976). We 107 | assume that theta_{b,i}(T) ~ T. 108 | 109 | Parameters 110 | ---------- 111 | T : float, np.array(dtype=float) 112 | temperature in K 113 | Z : int 114 | Z = Q / q where Q is the charge of the defect and q is the charge of 115 | the carrier. Z < 0 corresponds to attractive centers and Z > 0 116 | corresponds to repulsive centers 117 | m_eff : float 118 | effective mass of the carrier in units of m_e (electron mass) 119 | eps0 : float 120 | static dielectric constant 121 | dim : int 122 | dimension of the system (3, 2, or 1) 123 | x0 : float 124 | cutoff length for 1D evaluation 125 | method : str 126 | specify method for evaluating sommerfeld parameter ('Integrate' or 127 | 'Analytic'). The default is recommended as the analytic equation may 128 | introduce significant errors for the repulsive case at high T. 129 | 130 | Returns 131 | ------- 132 | float, np.array(dtype=float) 133 | sommerfeld factor evaluated at the given temperature 134 | """ 135 | if Z == 0: 136 | return 1. 137 | 138 | if method.lower()[0] == 'i': 139 | mkT = m_eff * const.m_e * const.k * T 140 | x_to_k = np.sqrt(2*mkT)/const.hbar 141 | 142 | def _f(k, Z, m_eff, eps0, dim, x0): 143 | if dim == 1: 144 | return _s_k(k, Z, m_eff, eps0, dim=dim, x0=x0) / k 145 | elif dim == 2: 146 | return _s_k(k, Z, m_eff, eps0, dim=dim) 147 | else: 148 | return k * _s_k(k, Z, m_eff, eps0, dim=dim) 149 | 150 | def _norm(mkT, dim): 151 | if dim == 1: 152 | return np.sqrt(np.pi*mkT/2) / const.hbar 153 | elif dim == 2: 154 | return mkT / const.hbar**2 155 | else: 156 | return np.sqrt(np.pi/2) * (mkT)**(3/2) / const.hbar**3 157 | 158 | t = 0. 159 | x, w = laggauss(64) 160 | for ix, iw in zip(x, w): 161 | t += iw * _f(x_to_k*np.sqrt(ix), Z, m_eff, eps0, dim, x0) 162 | t *= (mkT/const.hbar**2) 163 | return t / _norm(mkT, dim) 164 | 165 | theta_b = np.pi**2 * (m_eff * const.m_e) * const.e**4 / \ 166 | (2 * const.k * const.hbar**2 * (eps0 * 4*np.pi*const.epsilon_0)**2) 167 | zthetaT = Z**2 * theta_b / T 168 | if dim == 1: 169 | raise ValueError('Cannot analytically evaluate Sommerfeld parameter in 1D') 170 | elif dim == 2: 171 | if Z < 0: 172 | return 2. 173 | return np.sqrt(8*np.pi/3) * \ 174 | (8*zthetaT)**(1/6) * np.exp(-3 * zthetaT**(1/3)) 175 | else: 176 | if Z < 0: 177 | return 4 * np.sqrt(zthetaT / np.pi) 178 | return (8 / np.sqrt(3)) * \ 179 | zthetaT**(2/3) * np.exp(-3 * zthetaT**(1/3)) 180 | 181 | 182 | @njit(cache=True) 183 | def find_charge_center(density: np.ndarray, lattice: np.ndarray) -> np.ndarray: 184 | """Compute the center of the charge density. 185 | 186 | Parameters 187 | ---------- 188 | density : np.array 189 | density of the wavecar returned by wavecar.fft_mesh 190 | lattice : np.array 191 | lattice to use to compute PBC 192 | 193 | Returns 194 | ------- 195 | np.array 196 | position of the center in cartesian coordinates 197 | """ 198 | avg = np.zeros(3) 199 | for i in range(density.shape[0]): 200 | for j in range(density.shape[1]): 201 | for k in range(density.shape[2]): 202 | avg += np.dot(np.array((i, j, k)) / np.array(density.shape), 203 | lattice) * density[(i, j, k)] 204 | return avg / np.sum(density) 205 | 206 | 207 | @njit(cache=True) 208 | def distance_PBC(a: np.ndarray, b: np.ndarray, lattice: np.ndarray) -> float: 209 | """Compute the distance between a and b on the lattice with periodic BCs. 210 | 211 | Parameters 212 | ---------- 213 | a, b : np.array 214 | points in cartesian coordinates 215 | lattice : np.array 216 | lattice to use to compute PBC 217 | 218 | Returns 219 | ------- 220 | float 221 | distance 222 | """ 223 | min_dist: Any = np.inf 224 | for i in [-1, 0, 1]: 225 | for j in [-1, 0, 1]: 226 | for k in [-1, 0, 1]: 227 | R = np.dot(np.array((i, j, k), dtype=np.float64), lattice) 228 | dist = np.linalg.norm(a - b + R) 229 | min_dist = dist if dist < min_dist else min_dist 230 | return min_dist 231 | 232 | 233 | @njit(cache=True) 234 | def radial_distribution( 235 | density: np.ndarray, 236 | point: np.ndarray, 237 | lattice: np.ndarray 238 | ) -> tuple[np.ndarray, np.ndarray]: 239 | """Compute the radial distribution. 240 | 241 | Computes the radial distribution of the density around the given point with 242 | the defined lattice. 243 | 244 | Parameters 245 | ---------- 246 | density : np.array 247 | density of the wavecar returned by wavecar.fft_mesh 248 | point : np.array 249 | position of the defect in cartesian coordinates 250 | lattice : np.array 251 | lattice to use to compute PBC 252 | 253 | Returns 254 | ------- 255 | r : np.array 256 | array of radii that the density corresponds to 257 | n : np.array 258 | array of densities at the corresponding radii 259 | """ 260 | N = density.shape[0] * density.shape[1] * density.shape[2] 261 | r, n = (np.zeros(N), np.zeros(N)) 262 | m = 0 263 | for i in range(density.shape[0]): 264 | for j in range(density.shape[1]): 265 | for k in range(density.shape[2]): 266 | r[m] = distance_PBC(np.dot( 267 | np.array((i, j, k), dtype=np.float64) / 268 | np.array(density.shape), 269 | lattice 270 | ), point, lattice) 271 | n[m] = density[(i, j, k)] 272 | m += 1 273 | return r, n 274 | 275 | 276 | def charged_supercell_scaling_VASP( 277 | wavecar_path: str, 278 | bulk_index: int, 279 | def_index: int = -1, 280 | def_coord: Optional[np.ndarray] = None, 281 | cutoff: float = 0.02, 282 | limit: float = 5., 283 | spin: int = 0, 284 | kpoint: int = 1, 285 | fig=None, 286 | full_range=False 287 | ) -> float: 288 | """ 289 | Estimate the interaction between the defect and bulk wavefunction. 290 | 291 | This function estimates the interaction between the defect and bulk 292 | wavefunction due to spurious effects as a result of using a charged 293 | supercell. The radial distribution of the bulk wavefunction is compared to 294 | a perfectly homogenous wavefunction to estimate the scaling. 295 | 296 | Either def_index or def_coord must be specified. 297 | 298 | If you get wonky results with def_index, try using def_coord as there may 299 | be a problem with finding the defect position if the defect charge is at 300 | the boundary of the cell. 301 | 302 | Parameters 303 | ---------- 304 | wavecar_path : str 305 | path to the WAVECAR file that contains the relevant wavefunctions 306 | def_index, bulk_index : int 307 | index of the defect and bulk wavefunctions in the WAVECAR file 308 | def_coord : np.array(dim=(3,)) 309 | cartesian coordinates of defect position 310 | cutoff : float 311 | cutoff for determining zero slope regions 312 | limit : float 313 | upper limit for windowing procedure 314 | spin : int 315 | spin channel to read from (0 - up, 1 - down) 316 | kpoint : int 317 | kpoint to read from (defaults to the first kpoint) 318 | fig : matplotlib.figure.Figure 319 | optional figure object to plot diagnostic information (recommended) 320 | full_range : bool 321 | determines if full range of first plot is shown 322 | 323 | Returns 324 | ------- 325 | float 326 | estimated scaling value to apply to the capture coefficient 327 | """ 328 | if def_index == -1 and def_coord is None: 329 | raise ValueError('either def_index or def_coord must be specified') 330 | 331 | wavecar = Wavecar(wavecar_path) 332 | 333 | # compute relevant things 334 | if def_coord is None: 335 | psi_def = wavecar.fft_mesh(spin=spin, kpoint=kpoint-1, 336 | band=def_index-1) 337 | fft_psi_def = np.fft.ifftn(psi_def) 338 | den_def = np.abs(np.conj(fft_psi_def) * fft_psi_def) / \ 339 | np.abs(np.vdot(fft_psi_def, fft_psi_def)) 340 | def_coord = find_charge_center(den_def, wavecar.a) 341 | 342 | psi_bulk = wavecar.fft_mesh(spin=spin, kpoint=kpoint-1, band=bulk_index-1) 343 | fft_psi_bulk = np.fft.ifftn(psi_bulk) 344 | return charged_supercell_scaling(fft_psi_bulk, wavecar.a, def_coord, 345 | cutoff=cutoff, limit=limit, fig=fig, 346 | full_range=full_range) 347 | 348 | 349 | def charged_supercell_scaling( 350 | wavefunc: np.ndarray, 351 | lattice: np.ndarray, 352 | def_coord: np.ndarray, 353 | cutoff: float = 0.02, 354 | limit: float = 5., 355 | fig=None, 356 | full_range=False 357 | ) -> float: 358 | """ 359 | Estimate the interaction between the defect and bulk wavefunction. 360 | 361 | This function estimates the interaction between the defect and bulk 362 | wavefunction due to spurious effects as a result of using a charged 363 | supercell. The radial distribution of the bulk wavefunction is compared to 364 | a perfectly homogenous wavefunction to estimate the scaling. 365 | 366 | Parameters 367 | ---------- 368 | wavefunc : np.array(dim=(NX, NY, NZ)) 369 | bulk wavefunction in real-space on a NX by NY by NZ FFT grid 370 | lattice : np.array(dim=(3, 3)) 371 | real-space lattice vectors for your system 372 | def_coord : np.array(dim=(3,)) 373 | cartesian coordinates of defect position 374 | cutoff : float 375 | cutoff for determining zero slope regions 376 | limit : float 377 | upper limit for windowing procedure 378 | fig : matplotlib.figure.Figure 379 | optional figure object to plot diagnostic information (recommended) 380 | full_range : bool 381 | determines if full range of first plot is shown 382 | 383 | Returns 384 | ------- 385 | float 386 | estimated scaling value to apply to the capture coefficient 387 | """ 388 | volume = np.dot(lattice[0, :], np.cross(lattice[1, :], lattice[2, :])) 389 | den_bulk = np.abs(np.conj(wavefunc) * wavefunc) / \ 390 | np.abs(np.vdot(wavefunc, wavefunc)) 391 | r, density = radial_distribution(den_bulk, def_coord, lattice) 392 | 393 | # fitting procedure 394 | def f(x, alpha): 395 | return alpha * (4 * np.pi / 3 / volume) * x**3 396 | 397 | sind = np.argsort(r) 398 | R, int_den = (r[sind], np.cumsum(density[sind])) 399 | uppers = np.linspace(1., limit, 500) 400 | alphas = np.array([ 401 | curve_fit(f, R[(R >= 0.) & (R <= u)], 402 | int_den[(R >= 0.) & (R <= u)])[0][0] 403 | for u in uppers 404 | ]) 405 | 406 | # find possible plateaus and return earliest one 407 | zeros = np.where(np.abs(np.gradient(alphas, uppers)) < cutoff)[0] 408 | plateaus = list(map( 409 | lambda x: (x[0], len(list(x[1]))), 410 | groupby(np.round(alphas[zeros], 2)) 411 | )) 412 | 413 | if fig: 414 | tr = np.linspace(0., np.max(r), 100) 415 | ax = fig.subplots(1, 3) 416 | ax[0].scatter(R, int_den, s=10) 417 | ax[0].plot(tr, f(tr, 1.), color='r') 418 | ax[0].fill_between(tr, f(tr, 0.9), f(tr, 1.1), 419 | facecolor='r', alpha=0.5) 420 | if not full_range: 421 | ax[0].set_xlim([-0.05 * limit, 1.05 * limit]) 422 | ax[0].set_ylim([-0.05, 0.30]) 423 | else: 424 | ax[0].set_ylim([-0.05, 1.05]) 425 | ax[0].set_xlabel(r'radius [$\AA$]') 426 | ax[0].set_ylabel('cumulative charge density') 427 | ax[1].scatter(uppers, alphas, s=10) 428 | try: 429 | ax[1].axhline(y=plateaus[0][0], color='k', alpha=0.5) 430 | except IndexError: 431 | pass 432 | ax[1].set_xlabel(r'radius [$\AA$]') 433 | ax[1].set_ylabel(r'$\alpha$ [1]') 434 | ax[2].set_yscale('log') 435 | ax[2].scatter(uppers, np.abs(np.gradient(alphas, uppers)), s=10) 436 | ax[2].scatter(uppers[zeros], 437 | np.abs(np.gradient(alphas, uppers))[zeros], 438 | s=10, color='r', marker='.') 439 | ax[2].axhline(y=cutoff, color='k', alpha=0.5) 440 | ax[2].set_xlabel(r'radius [$\AA$]') 441 | ax[2].set_ylabel(r'$d \alpha / d r$ [1]') 442 | 443 | return plateaus[0][0] 444 | 445 | 446 | def thermal_velocity(T: Union[float, np.ndarray], m_eff: float): 447 | """Calculate the thermal velocity at a given temperature. 448 | 449 | Parameters 450 | ---------- 451 | T : float, np.array(dtype=float) 452 | temperature in K 453 | m_eff : float 454 | effective mass in electron masses 455 | 456 | Returns 457 | ------- 458 | float, np.array(dtype=float) 459 | thermal velocity at the given temperature in cm s^{-1} 460 | """ 461 | return np.sqrt(3 * const.k * T / (m_eff * const.m_e)) * 1e2 462 | -------------------------------------------------------------------------------- /nonrad/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=all 2 | 3 | from pathlib import Path 4 | 5 | TEST_FILES = Path(__file__).absolute().parent / '..' / '..' / 'test_files' 6 | 7 | 8 | class FakeAx: 9 | def plot(*args, **kwargs): 10 | pass 11 | 12 | def scatter(*args, **kwargs): 13 | pass 14 | 15 | def set_title(*args, **kwargs): 16 | pass 17 | 18 | def axhline(*args, **kwargs): 19 | pass 20 | 21 | def set_xlim(*args, **kwargs): 22 | pass 23 | 24 | def set_ylim(*args, **kwargs): 25 | pass 26 | 27 | def set_xlabel(*args, **kwargs): 28 | pass 29 | 30 | def set_ylabel(*args, **kwargs): 31 | pass 32 | 33 | def set_yscale(*args, **kwargs): 34 | pass 35 | 36 | def fill_between(*args, **kwargs): 37 | pass 38 | 39 | 40 | class FakeFig: 41 | def subplots(self, x, y, **kwargs): 42 | return [FakeAx() for _ in range(y)] 43 | -------------------------------------------------------------------------------- /nonrad/tests/test_ccd.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=C0114,C0115,C0116 2 | 3 | import glob 4 | import unittest 5 | import warnings 6 | 7 | import numpy as np 8 | from pymatgen.core import Lattice, Structure 9 | 10 | from nonrad.ccd import ( 11 | get_cc_structures, 12 | get_dQ, 13 | get_omega_from_PES, 14 | get_PES_from_vaspruns, 15 | get_Q_from_struct, 16 | ) 17 | from nonrad.nonrad import AMU2KG, ANGS2M, EV2J, HBAR 18 | from nonrad.tests import TEST_FILES, FakeAx 19 | 20 | 21 | class CCDTest(unittest.TestCase): 22 | def setUp(self): 23 | self.gnd_real = Structure.from_file(TEST_FILES / 'POSCAR.C0.gz') 24 | self.exd_real = Structure.from_file(TEST_FILES / 'POSCAR.C-.gz') 25 | self.gnd_test = Structure(Lattice.cubic(1.), ['H'], 26 | [[0., 0., 0.]]) 27 | self.exd_test = Structure(Lattice.cubic(1.), ['H'], 28 | [[0.5, 0.5, 0.5]]) 29 | self.sct_test = Structure(Lattice.cubic(1.), ['H'], 30 | [[0.25, 0.25, 0.25]]) 31 | self.vrs = [TEST_FILES / 'vasprun.xml.0.gz'] + \ 32 | glob.glob(str(TEST_FILES / 'lower' / '*' / 'vasprun.xml.gz')) 33 | 34 | def test_get_cc_structures(self): 35 | gs, es = get_cc_structures(self.gnd_real, self.exd_real, [0.]) 36 | self.assertEqual(gs, []) 37 | self.assertEqual(es, []) 38 | gs, es = get_cc_structures(self.gnd_test, self.exd_test, [0.], 39 | remove_zero=False) 40 | self.assertEqual(self.gnd_test, gs[0]) 41 | self.assertEqual(self.exd_test, es[0]) 42 | gs, es = get_cc_structures(self.gnd_test, self.exd_test, [0.5]) 43 | self.assertTrue(np.allclose(gs[0][0].coords, [0.25, 0.25, 0.25])) 44 | 45 | def test_get_dQ(self): 46 | self.assertEqual(get_dQ(self.gnd_test, self.gnd_test), 0.) 47 | self.assertEqual(get_dQ(self.exd_test, self.exd_test), 0.) 48 | self.assertEqual(get_dQ(self.gnd_real, self.gnd_real), 0.) 49 | self.assertEqual(get_dQ(self.exd_real, self.exd_real), 0.) 50 | self.assertAlmostEqual(get_dQ(self.gnd_test, self.exd_test), 0.86945, 51 | places=4) 52 | self.assertAlmostEqual(get_dQ(self.gnd_real, self.exd_real), 1.68587, 53 | places=4) 54 | 55 | def test_get_Q_from_struct(self): 56 | q = get_Q_from_struct(self.gnd_test, self.exd_test, self.sct_test) 57 | self.assertAlmostEqual(q, 0.5 * 0.86945, places=4) 58 | q = get_Q_from_struct(self.gnd_real, self.exd_real, 59 | str(TEST_FILES / 'POSCAR.C0.gz')) 60 | self.assertAlmostEqual(q, 0., places=4) 61 | gs, es = get_cc_structures(self.gnd_real, self.exd_real, 62 | np.linspace(-0.5, 0.5, 100), 63 | remove_zero=False) 64 | Q = 1.68587 * np.linspace(-0.5, 0.5, 100) 65 | for s, q in zip(gs, Q): 66 | tq = get_Q_from_struct(self.gnd_real, self.exd_real, s) 67 | self.assertAlmostEqual(tq, q, places=4) 68 | for s, q in zip(es, Q + 1.68587): 69 | tq = get_Q_from_struct(self.gnd_real, self.exd_real, s) 70 | self.assertAlmostEqual(tq, q, places=4) 71 | 72 | # test when one of the coordinates stays the same 73 | sg = Structure(np.eye(3), ['H'], [[0.0, 0.0, 0.0]]) 74 | sq = Structure(np.eye(3), ['H'], [[0.1, 0.0, 0.1]]) 75 | se = Structure(np.eye(3), ['H'], [[0.2, 0.0, 0.2]]) 76 | dQ = get_dQ(sg, se) 77 | self.assertAlmostEqual(get_Q_from_struct(sg, se, sq)/dQ, 0.5) 78 | 79 | def test_get_PES_from_vaspruns(self): 80 | with warnings.catch_warnings(): 81 | warnings.simplefilter('ignore') 82 | q, en = get_PES_from_vaspruns(self.gnd_real, self.exd_real, 83 | self.vrs) 84 | self.assertEqual(len(q), 2) 85 | self.assertEqual(len(en), 2) 86 | self.assertEqual(np.min(en), 0.) 87 | self.assertEqual(en[0], 0.) 88 | 89 | def test_get_omega_from_PES(self): 90 | q = np.linspace(-0.5, 0.5, 20) 91 | for om, q0 in zip(np.linspace(0.01, 0.1, 10), 92 | np.linspace(0.1, 3., 10)): 93 | omega = (om / HBAR)**2 * ANGS2M**2 * AMU2KG / EV2J 94 | en = 0.5 * omega * (q - q0)**2 95 | with warnings.catch_warnings(): 96 | warnings.simplefilter('ignore') 97 | self.assertAlmostEqual(get_omega_from_PES(q, en), om) 98 | self.assertAlmostEqual(get_omega_from_PES(q, en, Q0=q0), om) 99 | om, q0 = (0.1, 3.) 100 | self.assertAlmostEqual(get_omega_from_PES(q, en, Q0=q0, ax=FakeAx()), 101 | om) 102 | self.assertAlmostEqual(get_omega_from_PES(q, en, Q0=q0, ax=FakeAx(), 103 | q=np.linspace(-1, 1, 100)), 104 | om) 105 | with warnings.catch_warnings(): 106 | warnings.simplefilter('ignore') 107 | q, en = \ 108 | get_PES_from_vaspruns(self.gnd_real, self.exd_real, self.vrs) 109 | q = np.append(q, [-1*q[-1]]) 110 | en = np.append(en, [en[-1]]) 111 | self.assertAlmostEqual(get_omega_from_PES(q, en), 0.0335, places=3) 112 | -------------------------------------------------------------------------------- /nonrad/tests/test_elphon.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=C0114,C0115,C0116 2 | 3 | import glob 4 | import unittest 5 | import warnings 6 | from itertools import product 7 | 8 | import numpy as np 9 | from pymatgen.core import Lattice, Structure 10 | 11 | from nonrad.ccd import get_Q_from_struct 12 | from nonrad.elphon import ( 13 | _compute_matel, 14 | _read_WSWQ, 15 | get_Wif_from_UNK, 16 | get_Wif_from_wavecars, 17 | get_Wif_from_WSWQ, 18 | ) 19 | from nonrad.tests import TEST_FILES, FakeFig 20 | 21 | 22 | class ElphonTest(unittest.TestCase): 23 | def setUp(self): 24 | self.gnd_real = Structure.from_file(TEST_FILES / 'POSCAR.C0.gz') 25 | self.exd_real = Structure.from_file(TEST_FILES / 'POSCAR.C-.gz') 26 | self.gnd_test = Structure(Lattice.cubic(1.), ['H'], 27 | [[0., 0., 0.]]) 28 | self.exd_test = Structure(Lattice.cubic(1.), ['H'], 29 | [[0.5, 0.5, 0.5]]) 30 | self.sct_test = Structure(Lattice.cubic(1.), ['H'], 31 | [[0.25, 0.25, 0.25]]) 32 | self.vrs = [TEST_FILES / 'vasprun.xml.0.gz'] + \ 33 | glob.glob(str(TEST_FILES / 'lower' / '*' / 'vasprun.xml.gz')) 34 | 35 | def test__compute_matel(self): 36 | N = 10 37 | rng = np.random.default_rng() 38 | H = rng.random(size=(N, N)).astype(complex) + \ 39 | 1j*rng.random(size=(N, N)).astype(complex) 40 | H = H + np.conj(H).T 41 | _, ev = np.linalg.eigh(H) 42 | for i, j in product(range(N), range(N)): 43 | if i == j: 44 | self.assertAlmostEqual(_compute_matel(ev[:, i], ev[:, j]), 1.) 45 | else: 46 | self.assertAlmostEqual(_compute_matel(ev[:, i], ev[:, j]), 0.) 47 | 48 | @unittest.skip('WAVECARs too large to share') 49 | def test_get_Wif_from_wavecars(self): 50 | with warnings.catch_warnings(): 51 | warnings.simplefilter('ignore') 52 | wcrs = [ 53 | (Structure.from_file(d+'/vasprun.xml.gz'), d+'/WAVECAR') 54 | for d in glob.glob(str(TEST_FILES / 'lower' / '*')) 55 | ] 56 | wcrs = list(map( 57 | lambda x: ( 58 | get_Q_from_struct(self.gnd_real, self.exd_real, x[0]), 59 | x[1] 60 | ), wcrs)) 61 | self.assertAlmostEqual( 62 | get_Wif_from_wavecars(wcrs, str(TEST_FILES / 'WAVECAR.C0'), 63 | 192, [189], spin=1)[0][1], 64 | 0.087, places=2 65 | ) 66 | self.assertAlmostEqual( 67 | get_Wif_from_wavecars(wcrs, str(TEST_FILES / 'WAVECAR.C0'), 68 | 192, [189], spin=1, fig=FakeFig())[0][1], 69 | 0.087, places=2 70 | ) 71 | 72 | def test_get_Wif_from_UNK(self): 73 | Wif = get_Wif_from_UNK( 74 | unks=[(1., str(TEST_FILES / 'UNK.1'))], 75 | init_unk_path=str(TEST_FILES / 'UNK.0'), 76 | def_index=2, 77 | bulk_index=[1], 78 | eigs=np.array([0., 1.]) 79 | ) 80 | self.assertEqual(Wif[0][0], 1) 81 | self.assertAlmostEqual(Wif[0][1], 1.) 82 | 83 | def test__read_WSWQ(self): 84 | wswq = _read_WSWQ(str(TEST_FILES / 'lower' / '10' / 'WSWQ.gz')) 85 | self.assertGreater(len(wswq), 0) 86 | self.assertGreater(len(wswq[(1, 1)]), 0) 87 | self.assertGreater(np.abs(wswq[(1, 1)][(1, 1)]), 0) 88 | self.assertEqual(type(wswq), dict) 89 | self.assertEqual(type(wswq[(1, 1)]), dict) 90 | 91 | def test_get_Wif_from_WSWQ(self): 92 | with warnings.catch_warnings(): 93 | warnings.simplefilter('ignore') 94 | wswqs = [ 95 | (Structure.from_file(d+'/vasprun.xml.gz'), d+'/WSWQ.gz') 96 | for d in glob.glob(str(TEST_FILES / 'lower' / '*')) 97 | ] 98 | wswqs = list(map( 99 | lambda x: ( 100 | get_Q_from_struct(self.gnd_real, self.exd_real, x[0]), 101 | x[1] 102 | ), wswqs)) 103 | self.assertAlmostEqual( 104 | get_Wif_from_WSWQ(wswqs, str(TEST_FILES / 'vasprun.xml.0.gz'), 105 | 192, [189], spin=1)[0][1], 106 | 0.094, places=2 107 | ) 108 | self.assertAlmostEqual( 109 | get_Wif_from_WSWQ(wswqs, str(TEST_FILES / 'vasprun.xml.0.gz'), 110 | 192, [189], spin=1, fig=FakeFig())[0][1], 111 | 0.094, places=2 112 | ) 113 | -------------------------------------------------------------------------------- /nonrad/tests/test_nonrad.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=C0114,C0115,C0116 2 | 3 | import unittest 4 | from itertools import product 5 | 6 | import numpy as np 7 | from numpy.polynomial.hermite import hermval 8 | from scipy.special import factorial 9 | 10 | from nonrad.nonrad import ( 11 | analytic_overlap_NM, 12 | fact, 13 | fast_overlap_NM, 14 | get_C, 15 | herm, 16 | overlap_NM, 17 | ) 18 | 19 | 20 | class OverlapTest(unittest.TestCase): 21 | def test_fast_overlap_NM(self): 22 | DQ, w1, w2 = (0.00, 0.03, 0.03) 23 | for m, n in product(range(10), range(10)): 24 | if m == n: 25 | self.assertAlmostEqual(fast_overlap_NM(DQ, w1, w2, m, n), 1.) 26 | else: 27 | self.assertAlmostEqual(fast_overlap_NM(DQ, w1, w2, m, n), 0.) 28 | DQ, w1, w2 = (1.00, 0.03, 0.03) 29 | for m, n in product(range(10), range(10)): 30 | if m == n: 31 | self.assertNotAlmostEqual( 32 | fast_overlap_NM(DQ, w1, w2, m, n), 1. 33 | ) 34 | else: 35 | self.assertNotAlmostEqual( 36 | fast_overlap_NM(DQ, w1, w2, m, n), 0. 37 | ) 38 | DQ, w1, w2 = (1.00, 0.15, 0.03) 39 | for m, n in product(range(10), range(10)): 40 | if m == n: 41 | self.assertNotAlmostEqual( 42 | fast_overlap_NM(DQ, w1, w2, m, n), 1. 43 | ) 44 | else: 45 | self.assertNotAlmostEqual( 46 | fast_overlap_NM(DQ, w1, w2, m, n), 0. 47 | ) 48 | 49 | def test_overlap_NM(self): 50 | DQ, w1, w2 = (0.00, 0.03, 0.03) 51 | for m, n in product(range(10), range(10)): 52 | if m == n: 53 | self.assertAlmostEqual(overlap_NM(DQ, w1, w2, m, n), 1.) 54 | else: 55 | self.assertAlmostEqual(overlap_NM(DQ, w1, w2, m, n), 0.) 56 | DQ, w1, w2 = (1.00, 0.03, 0.03) 57 | for m, n in product(range(10), range(10)): 58 | if m == n: 59 | self.assertNotAlmostEqual(overlap_NM(DQ, w1, w2, m, n), 1.) 60 | else: 61 | self.assertNotAlmostEqual(overlap_NM(DQ, w1, w2, m, n), 0.) 62 | DQ, w1, w2 = (1.00, 0.15, 0.03) 63 | for m, n in product(range(10), range(10)): 64 | if m == n: 65 | self.assertNotAlmostEqual(overlap_NM(DQ, w1, w2, m, n), 1.) 66 | else: 67 | self.assertNotAlmostEqual(overlap_NM(DQ, w1, w2, m, n), 0.) 68 | 69 | def test_analytic_overlap_NM(self): 70 | DQ, w1, w2 = (0.00, 0.03, 0.03) 71 | for m, n in product(range(10), range(10)): 72 | if m == n: 73 | self.assertAlmostEqual( 74 | analytic_overlap_NM(DQ, w1, w2, m, n), 1.) 75 | else: 76 | self.assertAlmostEqual( 77 | analytic_overlap_NM(DQ, w1, w2, m, n), 0.) 78 | DQ, w1, w2 = (1.00, 0.03, 0.03) 79 | for m, n in product(range(10), range(10)): 80 | if m == n: 81 | self.assertNotAlmostEqual( 82 | analytic_overlap_NM(DQ, w1, w2, m, n), 1.) 83 | else: 84 | self.assertNotAlmostEqual( 85 | analytic_overlap_NM(DQ, w1, w2, m, n), 0.) 86 | DQ, w1, w2 = (1.00, 0.15, 0.03) 87 | for m, n in product(range(10), range(10)): 88 | if m == n: 89 | self.assertNotAlmostEqual( 90 | analytic_overlap_NM(DQ, w1, w2, m, n), 1.) 91 | else: 92 | self.assertNotAlmostEqual( 93 | analytic_overlap_NM(DQ, w1, w2, m, n), 0.) 94 | 95 | def test_compare_overlaps(self): 96 | for DQ, w1, w2 in product([0., 0.5, 3.14], [0.03, 0.1], [0.03, 0.5]): 97 | for m, n in product(range(10), range(10)): 98 | self.assertAlmostEqual( 99 | overlap_NM(DQ, w1, w2, m, n), 100 | analytic_overlap_NM(DQ, w1, w2, m, n), places=5) 101 | 102 | 103 | # more robust tests for get_C would be ideal... 104 | # we're a bit limited because there aren't too many "obvious" results to 105 | # compare to 106 | class GetCTest(unittest.TestCase): 107 | def setUp(self): 108 | self.args = { 109 | 'dQ': 2.008, 110 | 'dE': 1.0102, 111 | 'wi': 0.0306775211118, 112 | 'wf': 0.0339920265573, 113 | 'Wif': 0.00669174, 114 | 'volume': 1100, 115 | 'g': 1, 116 | 'T': 300, 117 | 'sigma': 'pchip', 118 | 'occ_tol': 1e-5, 119 | 'overlap_method': 'Integrate' 120 | } 121 | 122 | def test_normal_run(self): 123 | self.assertGreater(get_C(**self.args), 0.) 124 | 125 | def test_same_w(self): 126 | self.args['wf'] = self.args['wi'] 127 | self.assertGreater(get_C(**self.args), 0.) 128 | self.args['dQ'] = 0. 129 | self.assertLess(get_C(**self.args), 1e-20) 130 | 131 | def test_analytic(self): 132 | self.args['overlap_method'] = 'analytic' 133 | self.assertGreater(get_C(**self.args), 0.) 134 | self.args['overlap_method'] = 'Analytic' 135 | self.assertGreater(get_C(**self.args), 0.) 136 | 137 | def test_bad_overlap(self): 138 | self.args['overlap_method'] = 'blah' 139 | with self.assertRaises(ValueError): 140 | get_C(**self.args) 141 | 142 | def test_cubic(self): 143 | self.args['sigma'] = 'cubic' 144 | self.assertGreater(get_C(**self.args), 0.) 145 | 146 | def test_cubic_failure(self): 147 | # should result in a negative C, which doesn't make sense 148 | self.args = { 149 | 'dQ': 0.3, 150 | 'dE': 1.0, 151 | 'wi': 0.065, 152 | 'wf': 0.065, 153 | 'Wif': 0.3, 154 | 'volume': 1.0, 155 | 'g': 1, 156 | 'T': 300, 157 | 'sigma': 'cubic', 158 | 'occ_tol': 1e-5, 159 | 'overlap_method': 'Integrate' 160 | } 161 | self.assertLess(get_C(**self.args), 0.) 162 | 163 | # fixed with pchip 164 | self.args['sigma'] = 'pchip' 165 | self.assertGreater(get_C(**self.args), 0.) 166 | 167 | def test_gaussian(self): 168 | for sigma in np.linspace(0.1, 5, 5): 169 | self.args['sigma'] = sigma 170 | self.assertGreater(get_C(**self.args), 0.) 171 | 172 | def test_T(self): 173 | self.args['T'] = np.linspace(0.01, 1000, 100) 174 | cs = get_C(**self.args) 175 | self.assertEqual(type(cs), np.ndarray) 176 | self.assertEqual(len(cs), 100) 177 | for c in cs: 178 | self.assertGreater(c, 0.) 179 | self.args['T'] = [300] 180 | with self.assertRaises(TypeError): 181 | get_C(**self.args) 182 | 183 | def test_occ_tol(self): 184 | self.args['occ_tol'] = 1e-6 185 | self.assertGreater(get_C(**self.args), 0.) 186 | self.args['occ_tol'] = 1. 187 | self.args['dE'] = 150 * self.args['wf'] 188 | with self.assertRaises(ValueError): 189 | get_C(**self.args) 190 | self.args['sigma'] = 'cubic' 191 | with self.assertWarns(RuntimeWarning): 192 | get_C(**self.args) 193 | 194 | 195 | class MathTest(unittest.TestCase): 196 | def test_fact(self): 197 | for i in range(171): 198 | exact = np.double(factorial(i, exact=True)) 199 | self.assertAlmostEqual(fact(i)/exact - 1, 0.) 200 | 201 | def test_herm(self): 202 | for x in np.linspace(0.1, 1., 50): 203 | for i in range(70): 204 | exact = hermval(x, [0.]*i + [1.]) 205 | self.assertAlmostEqual(herm(x, i)/exact - 1, 0.) 206 | -------------------------------------------------------------------------------- /nonrad/tests/test_scaling.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=C0114,C0115,C0116 2 | 3 | import unittest 4 | from typing import Union 5 | 6 | import numpy as np 7 | from numpy.polynomial.laguerre import laggauss 8 | from scipy import constants as const 9 | 10 | from nonrad.scaling import ( 11 | charged_supercell_scaling, 12 | charged_supercell_scaling_VASP, 13 | distance_PBC, 14 | find_charge_center, 15 | radial_distribution, 16 | sommerfeld_parameter, 17 | thermal_velocity, 18 | ) 19 | from nonrad.tests import TEST_FILES, FakeFig 20 | 21 | 22 | def _old_sommerfeld_parameter( 23 | T: Union[float, np.ndarray], 24 | Z: int, 25 | m_eff: float, 26 | eps0: float, 27 | method: str = 'Integrate' 28 | ) -> Union[float, np.ndarray]: 29 | if Z == 0: 30 | return 1. 31 | 32 | if method.lower()[0] == 'i': 33 | kT = const.k * T 34 | m = m_eff * const.m_e 35 | eps = (4 * np.pi * const.epsilon_0) * eps0 36 | f = -2 * np.pi * Z * m * const.e**2 / const.hbar**2 / eps 37 | 38 | def s_k(k): 39 | return f / k / (1 - np.exp(-f / k)) 40 | 41 | t = 0. 42 | x, w = laggauss(64) 43 | for ix, iw in zip(x, w): 44 | t += iw * np.sqrt(ix) * s_k(np.sqrt(2 * m * kT * ix) / const.hbar) 45 | return t / np.sum(w * np.sqrt(x)) 46 | 47 | # that 4*pi from Gaussian units.... 48 | theta_b = np.pi**2 * (m_eff * const.m_e) * const.e**4 / \ 49 | (2 * const.k * const.hbar**2 * (eps0 * 4*np.pi*const.epsilon_0)**2) 50 | zthetaT = Z**2 * theta_b / T 51 | 52 | if Z < 0: 53 | return 4 * np.sqrt(zthetaT / np.pi) 54 | return (8 / np.sqrt(3)) * \ 55 | zthetaT**(2/3) * np.exp(-3 * zthetaT**(1/3)) 56 | 57 | 58 | class SommerfeldTest(unittest.TestCase): 59 | def setUp(self): 60 | self.args = { 61 | 'T': 300, 62 | 'Z': 0, 63 | 'm_eff': 1., 64 | 'eps0': 1., 65 | 'method': 'Integrate' 66 | } 67 | 68 | def test_neutral(self): 69 | self.assertAlmostEqual(sommerfeld_parameter(**self.args), 1.) 70 | self.args['method'] = 'Analytic' 71 | self.assertAlmostEqual(sommerfeld_parameter(**self.args), 1.) 72 | 73 | def test_attractive(self): 74 | self.args['Z'] = -1 75 | self.assertGreater(sommerfeld_parameter(**self.args), 1.) 76 | self.args['method'] = 'Analytic' 77 | self.assertGreater(sommerfeld_parameter(**self.args), 1.) 78 | 79 | def test_repulsive(self): 80 | self.args['Z'] = 1 81 | self.assertLess(sommerfeld_parameter(**self.args), 1.) 82 | self.args['method'] = 'Analytic' 83 | self.assertLess(sommerfeld_parameter(**self.args), 1.) 84 | 85 | def test_list(self): 86 | self.args['T'] = np.linspace(0.1, 1000, 100) 87 | self.assertEqual(sommerfeld_parameter(**self.args), 1.) 88 | self.args['Z'] = -1 89 | self.assertTrue(np.all(sommerfeld_parameter(**self.args) > 1.)) 90 | self.args['Z'] = 1 91 | self.assertTrue(np.all(sommerfeld_parameter(**self.args) < 1.)) 92 | self.args['Z'] = 0 93 | self.args['method'] = 'Analytic' 94 | self.assertEqual(sommerfeld_parameter(**self.args), 1.) 95 | self.args['Z'] = -1 96 | self.assertTrue(np.all(sommerfeld_parameter(**self.args) > 1.)) 97 | self.args['Z'] = 1 98 | self.assertTrue(np.all(sommerfeld_parameter(**self.args) < 1.)) 99 | 100 | def test_compare_methods(self): 101 | self.args = { 102 | 'T': 100, 103 | 'Z': -1, 104 | 'm_eff': 0.2, 105 | 'eps0': 8.9, 106 | 'method': 'Integrate' 107 | } 108 | 109 | f0 = sommerfeld_parameter(**self.args) 110 | self.args['method'] = 'Analytic' 111 | f1 = sommerfeld_parameter(**self.args) 112 | self.assertAlmostEqual(f0, f1, places=2) 113 | 114 | self.args['Z'] = 1 115 | self.args['T'] = 900 116 | f0 = sommerfeld_parameter(**self.args) 117 | self.args['method'] = 'Integrate' 118 | f1 = sommerfeld_parameter(**self.args) 119 | self.assertGreater(np.abs(f0-f1)/f1, 0.1) 120 | 121 | def test_old_sommerfeld(self): 122 | self.args = {'m_eff': 0.2, 'eps0': 8.9} 123 | for m in ['i', 'a']: 124 | self.args['method'] = m 125 | for t in [100, 300, 700, 900]: 126 | self.args['T'] = t 127 | for z in [0, 1, -1]: 128 | self.args['Z'] = z 129 | f0 = _old_sommerfeld_parameter(**self.args) 130 | f1 = sommerfeld_parameter(**self.args) 131 | self.assertAlmostEqual(f0, f1, places=2) 132 | 133 | def test_sommerfeld_dim(self): 134 | self.args = { 135 | 'T': 200, 136 | 'Z': -1, 137 | 'm_eff': 0.2, 138 | 'eps0': 8.9, 139 | 'dim': 2, 140 | 'method': 'Integrate' 141 | } 142 | 143 | self.assertAlmostEqual(sommerfeld_parameter(**self.args), 2., places=2) 144 | self.args['method'] = 'a' 145 | self.assertAlmostEqual(sommerfeld_parameter(**self.args), 2., places=5) 146 | self.args['method'] = 'i' 147 | self.args['Z'] = 1 148 | self.assertLess(sommerfeld_parameter(**self.args), 1.) 149 | self.args['method'] = 'a' 150 | self.assertLess(sommerfeld_parameter(**self.args), 1.) 151 | 152 | self.args['dim'] = 1 153 | with self.assertRaises(ValueError): 154 | self.assertLess(sommerfeld_parameter(**self.args), 1.) 155 | self.args['method'] = 'i' 156 | self.assertLess(sommerfeld_parameter(**self.args), 1.) 157 | self.args['Z'] = -1 158 | self.assertLess(sommerfeld_parameter(**self.args), 1.) 159 | 160 | self.args['Z'] = 0 161 | self.assertEqual(sommerfeld_parameter(**self.args), 1.) 162 | 163 | 164 | class ChargedSupercellScalingTest(unittest.TestCase): 165 | def test_find_charge_center(self): 166 | lattice = np.eye(3) 167 | density = np.ones((50, 50, 50)) 168 | self.assertTrue( 169 | np.allclose(find_charge_center(density, lattice), [0.49]*3) 170 | ) 171 | density = np.zeros((50, 50, 50)) 172 | density[0, 0, 0] = 1. 173 | self.assertTrue( 174 | np.allclose(find_charge_center(density, lattice), [0.]*3) 175 | ) 176 | 177 | def test_distance_PBC(self): 178 | a = np.array([0.25]*3) 179 | b = np.array([0.5]*3) 180 | lattice = np.eye(3) 181 | self.assertEqual(distance_PBC(a, b, lattice), np.sqrt(3)*0.25) 182 | b = np.array([0.9]*3) 183 | self.assertEqual(distance_PBC(a, b, lattice), np.sqrt(3)*0.35) 184 | 185 | def test_radial_distribution(self): 186 | lattice = np.eye(3) 187 | density = np.zeros((50, 50, 50)) 188 | density[0, 0, 0] = 1. 189 | point = np.array([0.]*3) 190 | dist = distance_PBC(np.zeros(3), point, lattice) 191 | r, n = radial_distribution(density, point, lattice) 192 | self.assertAlmostEqual(r[np.where(n == 1.)[0][0]], dist) 193 | point = np.array([0.25]*3) 194 | dist = distance_PBC(np.zeros(3), point, lattice) 195 | r, n = radial_distribution(density, point, lattice) 196 | self.assertAlmostEqual(r[np.where(n == 1.)[0][0]], dist) 197 | point = np.array([0.29, 0.73, 0.44]) 198 | dist = distance_PBC(np.zeros(3), point, lattice) 199 | r, n = radial_distribution(density, point, lattice) 200 | self.assertAlmostEqual(r[np.where(n == 1.)[0][0]], dist) 201 | 202 | @unittest.skip('WAVECARs too large to share') 203 | def test_charged_supercell_scaling_VASP(self): 204 | f = charged_supercell_scaling_VASP( 205 | str(TEST_FILES / 'WAVECAR.C-'), 206 | 189, 207 | def_index=192 208 | ) 209 | self.assertAlmostEqual(f, 1.08) 210 | 211 | def test_charged_supercell_scaling(self): 212 | # test that numbers work out for homogeneous case 213 | wf = np.ones((20, 20, 20)) 214 | f = charged_supercell_scaling(wf, 10*np.eye(3), np.array([0.]*3)) 215 | self.assertAlmostEqual(f, 1.00) 216 | 217 | # test the plotting stuff 218 | wf = np.ones((1, 1, 1)) 219 | f = charged_supercell_scaling(wf, 10*np.eye(3), np.array([0.]*3), 220 | fig=FakeFig()) 221 | self.assertAlmostEqual(f, 1.00) 222 | f = charged_supercell_scaling(wf, 10*np.eye(3), np.array([0.]*3), 223 | fig=FakeFig(), full_range=True) 224 | self.assertAlmostEqual(f, 1.00) 225 | 226 | 227 | class ThermalVelocityTest(unittest.TestCase): 228 | def test_thermal_velocity(self): 229 | f = thermal_velocity(1., 1.) 230 | self.assertAlmostEqual(f, np.sqrt(3 * const.k / const.m_e) * 1e2) 231 | f = thermal_velocity(np.array([1.]), 1.) 232 | self.assertEqual(type(f), np.ndarray) 233 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.pdm] 2 | 3 | [tool.pdm.dev-dependencies] 4 | test = [ 5 | "pytest>=7.3.1", 6 | "pytest-cov>=4.0.0", 7 | "coverage>=7.2.5", 8 | ] 9 | docs = [ 10 | "sphinx>=6.2.1", 11 | "sphinx-rtd-theme>=1.2.0", 12 | ] 13 | lint = [ 14 | "ruff>=0.0.264", 15 | "mypy>=1.2.0", 16 | ] 17 | 18 | [tool.pdm.scripts] 19 | lint = {composite = ["ruff check nonrad", "mypy nonrad"]} 20 | test = "pytest -v nonrad" 21 | all = {composite = ["lint", "test"]} 22 | 23 | [project] 24 | name = "nonrad" 25 | version = "1.2.0" 26 | description = "Implementation for computing nonradiative recombination rates in semiconductors" 27 | authors = [ 28 | {name = "Mark E. Turiansky", email = "mturiansky@ucsb.edu"}, 29 | ] 30 | requires-python = ">=3.9" 31 | readme = "README.md" 32 | license = {text = "MIT"} 33 | keywords = [ 34 | "physics", 35 | "materials", 36 | "science", 37 | "VASP", 38 | "recombination", 39 | "Shockley-Read-Hall", 40 | ] 41 | classifiers=[ 42 | "Programming Language :: Python :: 3", 43 | "Programming Language :: Python :: 3.9", 44 | "Programming Language :: Python :: 3.10", 45 | "Programming Language :: Python :: 3.11", 46 | "License :: OSI Approved :: MIT License", 47 | "Operating System :: OS Independent", 48 | ] 49 | dependencies = [ 50 | "numpy>=1.24.3", 51 | "scipy>=1.9.3", 52 | "pymatgen>=2023.3.23", 53 | "monty>=2023.4.10", 54 | "numba>=0.57.0", 55 | "mpmath>=1.3.0", 56 | ] 57 | 58 | [project.urls] 59 | documentation = "https://nonrad.readthedocs.io" 60 | repository = "https://github.com/mturiansky/nonrad" 61 | 62 | [build-system] 63 | requires = ["pdm-backend"] 64 | build-backend = "pdm.backend" 65 | 66 | [tool.ruff] 67 | lint.select = [ 68 | "F", 69 | "E", 70 | "W", 71 | "I", 72 | "UP", 73 | "B", 74 | "A", 75 | "NPY", 76 | "PL", 77 | ] 78 | lint.ignore = [ 79 | "PLR", 80 | ] 81 | extend-exclude = ["docs"] 82 | 83 | [tool.mypy] 84 | ignore_missing_imports = "True" 85 | plugins = "numpy.typing.mypy_plugin" 86 | -------------------------------------------------------------------------------- /test_files/POSCAR.C-.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mturiansky/nonrad/d5bd3308c84f1052c9f30fe4e1f1bbe41f9fc3d2/test_files/POSCAR.C-.gz -------------------------------------------------------------------------------- /test_files/POSCAR.C0.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mturiansky/nonrad/d5bd3308c84f1052c9f30fe4e1f1bbe41f9fc3d2/test_files/POSCAR.C0.gz -------------------------------------------------------------------------------- /test_files/UNK.0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mturiansky/nonrad/d5bd3308c84f1052c9f30fe4e1f1bbe41f9fc3d2/test_files/UNK.0 -------------------------------------------------------------------------------- /test_files/UNK.1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mturiansky/nonrad/d5bd3308c84f1052c9f30fe4e1f1bbe41f9fc3d2/test_files/UNK.1 -------------------------------------------------------------------------------- /test_files/lower/10/WSWQ.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mturiansky/nonrad/d5bd3308c84f1052c9f30fe4e1f1bbe41f9fc3d2/test_files/lower/10/WSWQ.gz -------------------------------------------------------------------------------- /test_files/lower/10/vasprun.xml.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mturiansky/nonrad/d5bd3308c84f1052c9f30fe4e1f1bbe41f9fc3d2/test_files/lower/10/vasprun.xml.gz -------------------------------------------------------------------------------- /test_files/vasprun.xml.0.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mturiansky/nonrad/d5bd3308c84f1052c9f30fe4e1f1bbe41f9fc3d2/test_files/vasprun.xml.0.gz --------------------------------------------------------------------------------