From bee7f9bdd23c973f0919b5ef08be18e4218c18de Mon Sep 17 00:00:00 2001 From: Luke Shingles Date: Sat, 17 Feb 2024 13:15:43 +0000 Subject: [PATCH] Squashed commit of the following: commit e36a8d95839635d7df114f6ae9d595df2962883d Author: Luke Shingles Date: Sat Feb 17 13:14:18 2024 +0000 Update pytest.yml commit f2c76ebdf770b0b9dd63c5e7d5f3176f9a082fc7 Author: Luke Shingles Date: Sat Feb 17 13:08:52 2024 +0000 Update pytest.yml commit 17b1736d26d779fee0424f62ddf8b24387686842 Author: Luke Shingles Date: Sat Feb 17 13:06:25 2024 +0000 Update requirements.txt commit 87500b91d96ef84484d7dd0658a0c0646e16198b Author: Luke Shingles Date: Sat Feb 17 13:05:35 2024 +0000 Attempt to fix long_description commit 3b633d9fca67e0a490a0784989e0376c61c6176b Author: Luke Shingles Date: Sat Feb 17 12:59:49 2024 +0000 Delete _version.py commit ce512cda9cf2624f717d8ef5f9d6edad0bedd347 Author: Luke Shingles Date: Sat Feb 17 12:59:28 2024 +0000 Update build process commit ec03c7ee3417bf75ff4367bae9087d1d32e9e10a Author: Luke Shingles Date: Sat Feb 17 12:49:16 2024 +0000 remove setuptools_scm commit fc080be907a1e58024dc7bde6f29e7b91d01fa95 Author: Luke Shingles Date: Sat Feb 17 12:47:21 2024 +0000 Remove pylint commit bf8eded790d6b1b1b39f64528e8e9478185dc68f Author: Luke Shingles Date: Sat Feb 17 12:46:34 2024 +0000 Update linter.yml commit c286340a0cbf6934f6ca705340ec03ac26cb7726 Author: Luke Shingles Date: Sat Feb 17 12:43:07 2024 +0000 Update .pre-commit-config.yaml commit 9158243f78e7eb72c332e9b1ff4800ddf458b8b5 Author: Luke Shingles Date: Sat Feb 17 12:42:33 2024 +0000 Update README.md commit 5f6ee39b9c60c5e308f281832a58217967a75cfb Author: Luke Shingles Date: Sat Feb 17 12:42:08 2024 +0000 Update pytest.yml commit a5d41aecb4b1e1dda51631e485a83850d312f034 Author: Luke Shingles Date: Sat Feb 17 12:40:01 2024 +0000 add .python-version commit 6009deaae4039639df892aa8a33656a88c68b3dd Author: Luke Shingles Date: Sat Feb 17 12:34:21 2024 +0000 Modernize packaging --- .github/workflows/black.yml | 12 - .github/workflows/deploypypi.yml | 36 +++ .github/workflows/deploytestpypi.yml | 41 ++++ .github/workflows/linter.yml | 63 ++++++ .github/workflows/pytest.yml | 62 +++++ .github/workflows/pythonapp.yml | 85 ------- .gitignore | 5 +- .pre-commit-config.yaml | 59 +++++ .python-version | 1 + LICENSE | 42 ++-- MANIFEST.in | 3 +- README.md | 106 +++++---- pynonthermal/__init__.py | 14 +- pynonthermal/axelrod.py | 49 ++-- pynonthermal/base.py | 29 ++- pynonthermal/collion.py | 35 +-- pynonthermal/excitation.py | 59 ++--- pynonthermal/spencerfano.py | 323 ++++++++------------------- pynonthermal/tests/__init__.py | 0 pynonthermal/tests/test_sfsolve.py | 14 +- pyproject.toml | 177 +++++++++++++++ quickstart.ipynb | 20 +- requirements.txt | 1 + setup.cfg | 6 - setup.py | 53 ----- 25 files changed, 693 insertions(+), 602 deletions(-) delete mode 100644 .github/workflows/black.yml create mode 100644 .github/workflows/deploypypi.yml create mode 100644 .github/workflows/deploytestpypi.yml create mode 100644 .github/workflows/linter.yml create mode 100644 .github/workflows/pytest.yml delete mode 100644 .github/workflows/pythonapp.yml create mode 100644 .pre-commit-config.yaml create mode 100644 .python-version create mode 100644 pynonthermal/tests/__init__.py create mode 100644 pyproject.toml delete mode 100644 setup.cfg delete mode 100755 setup.py diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml deleted file mode 100644 index 58c7509..0000000 --- a/.github/workflows/black.yml +++ /dev/null @@ -1,12 +0,0 @@ -name: Lint - -on: [push, pull_request] - -jobs: - lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: psf/black@stable - with: - options: "--preview --check --verbose" \ No newline at end of file diff --git a/.github/workflows/deploypypi.yml b/.github/workflows/deploypypi.yml new file mode 100644 index 0000000..9e38664 --- /dev/null +++ b/.github/workflows/deploypypi.yml @@ -0,0 +1,36 @@ +--- +name: Upload Package to PyPI + +on: + release: + types: [published, edited] + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-python@v5 + with: + cache: pip + python-version-file: .python-version + + - name: Install dependencies + run: | + python3 -m pip install --upgrade pip + python3 -m pip install -r requirements.txt + python3 -m pip install --upgrade setuptools setuptools_scm[toml] wheel twine build + + - name: Build + run: | + python3 -m setuptools_scm + python3 -m build --sdist --wheel --outdir dist/ . + python3 -m twine check dist/* + + - name: Publish to PyPI + run: | + python3 -m twine upload -r pypi -u __token__ -p ${{ secrets.PYPI_TOKEN }} --non-interactive dist/* diff --git a/.github/workflows/deploytestpypi.yml b/.github/workflows/deploytestpypi.yml new file mode 100644 index 0000000..06e8bd6 --- /dev/null +++ b/.github/workflows/deploytestpypi.yml @@ -0,0 +1,41 @@ +--- +name: Upload Package to TestPyPI + +on: + push: + branches: + - '*' + tags: + - '*' + repository_dispatch: + types: [trigger_checks] + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-python@v5 + with: + cache: pip + python-version-file: .python-version + + - name: Install dependencies + run: | + python3 -m pip install --upgrade pip + python3 -m pip install -r requirements.txt + python3 -m pip install --upgrade setuptools setuptools_scm[toml] wheel twine build + + - name: Build + run: | + python3 -m setuptools_scm + python3 -m build --sdist --wheel --outdir dist/ . + python3 -m twine check dist/* + + - name: Publish to Test PyPI + run: | + python3 -m twine upload --skip-existing --verbose -r testpypi -u __token__ -p ${{ secrets.TESTPYPI_TOKEN }} --non-interactive dist/* diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml new file mode 100644 index 0000000..9c6bec7 --- /dev/null +++ b/.github/workflows/linter.yml @@ -0,0 +1,63 @@ +--- +name: Linter + +on: + push: + merge_group: + schedule: + - cron: 0 13 * * 1 + #pull_request: + workflow_dispatch: + repository_dispatch: + types: [trigger_checks] + +jobs: + pre-commit: + runs-on: macos-14 + env: + RUFF_OUTPUT_FORMAT: github + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + cache: pip + python-version-file: .python-version + + - name: Install dependencies + run: | + python3 -m pip install --upgrade pip wheel mypy + python3 -m pip install -r requirements.txt + + - name: Run pre-commit + run: | + pre-commit run --all-files + + lint: + runs-on: macos-14 + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + cache: pip + python-version-file: .python-version + + - name: Install dependencies + run: | + python3 -m pip install --upgrade pip wheel pylint mypy ruff + python3 -m pip install -r requirements.txt + + - name: Run Ruff + run: ruff check --output-format=github . + + - name: Run Ruff Format + run: ruff format --diff . + + - name: Run mypy + run: | + mypy --install-types --non-interactive diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml new file mode 100644 index 0000000..741f328 --- /dev/null +++ b/.github/workflows/pytest.yml @@ -0,0 +1,62 @@ +--- +name: Installation and pytest + +on: + push: + merge_group: + schedule: + - cron: 0 13 * * 1 + #pull_request: + workflow_dispatch: + repository_dispatch: + types: [trigger_checks] + +jobs: + pytest: + timeout-minutes: 10 + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + # os: [macos-14] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + env: + OS: ${{ matrix.os }} + PYTHON: ${{ matrix.python-version }} + + name: pytest python${{ matrix.python-version }} + steps: + - name: Checkout Code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + cache: pip + python-version: ${{ matrix.python-version }} + + - name: Install artistools + run: | + python3 -m pip install --upgrade pip + python3 -m pip install . + + - name: Test with pytest + run: pytest --cov=./ --cov-report=xml + + - name: Report coverage + run: coverage report + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./coverage.xml + # directory: ./coverage/reports/ + flags: unittests + env_vars: OS,PYTHON + name: codecov-umbrella + fail_ci_if_error: false + verbose: true diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml deleted file mode 100644 index 0037799..0000000 --- a/.github/workflows/pythonapp.yml +++ /dev/null @@ -1,85 +0,0 @@ -name: Build and test - -on: [push] - -jobs: - build: - runs-on: ${{ matrix.os }} - strategy: - matrix: - # os: [ubuntu-latest, macos-latest] - os: [ubuntu-latest] - python-version: ['3.8', '3.9', '3.10'] # 3.9 is unusually slow when pip installing requirements (>3 mins) - env: - OS: ${{ matrix.os }} - PYTHON: ${{ matrix.python-version }} - - steps: - - - name: Checkout Code - uses: actions/checkout@v3 - with: - # this is needed to the get the automatic version correct - fetch-depth: 0 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - - name: Get pip cache dir - id: pip-cache - run: | - pip list --format=freeze > piplist.txt - echo "::set-output name=dir::$(pip cache dir)" - - - name: Cache pip - uses: actions/cache@v2.1.4 - with: - path: ${{ steps.pip-cache.outputs.dir }} - key: ${{ runner.os }}-pip-${{ hashFiles('piplist.txt') }} - restore-keys: | - ${{ runner.os }}-pip- - - - name: Install dependencies - run: | - python3 -m pip install --upgrade pip - python3 -m pip install -r requirements.txt - - # upgrade all installed packages to their latest versions - python3 -m pip list --format=freeze --outdated | cut -d '=' -f1 | xargs -n1 python3 -m pip install --upgrade - - - name: Install this package - run: | - python -m pip install -e . - - - name: Lint with flake8 - working-directory: pynonthermal/ - run: | - pip install flake8 - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - # - name: Test with pytest - # run: | - # pytest -s --durations=0 - - - name: Test with pytest - run: | - pytest --cov=./ --cov-report=xml --durations=0 - - - name: Upload coverage to Codecov - # if: matrix.python-version == 3.9 - uses: codecov/codecov-action@v1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: ./coverage.xml - # directory: ./coverage/reports/ - flags: unittests - env_vars: OS,PYTHON - name: codecov-umbrella - fail_ci_if_error: true - path_to_write_report: ./coverage/codecov_report.txt - verbose: true diff --git a/.gitignore b/.gitignore index b6e4761..8eb6071 100644 --- a/.gitignore +++ b/.gitignore @@ -81,9 +81,6 @@ target/ profile_default/ ipython_config.py -# pyenv -.python-version - # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies @@ -127,3 +124,5 @@ dmypy.json # Pyre type checker .pyre/ + +_version.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..d67487d --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,59 @@ +--- +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: check-added-large-files + args: [--maxkb=800] + - id: check-ast + - id: check-case-conflict + - id: check-builtin-literals + - id: check-docstring-first + - id: check-executables-have-shebangs + - id: check-json + - id: check-merge-conflict + - id: check-symlinks + - id: check-toml + - id: check-yaml + - id: detect-private-key + - id: destroyed-symlinks + - id: fix-byte-order-marker + - id: forbid-new-submodules + - id: mixed-line-ending + args: [--fix=lf] + - id: name-tests-test + args: [--pytest-test-first] + - id: requirements-txt-fixer + # - id: trailing-whitespace + + - repo: https://github.com/jumanjihouse/pre-commit-hook-yamlfmt + rev: 0.2.3 + hooks: + - id: yamlfmt + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.2.1 + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.2.1 + hooks: + - id: ruff-format + + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.8.0 + hooks: + - id: mypy + additional_dependencies: [numpy, types-PyYAML, pandas>=2.0.3] + types: [python] + require_serial: true + + # - repo: local + # hooks: + # - id: mypy + # name: mypy + # entry: mypy + # types: [python] + # language: python diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..fdcfcfd --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12 \ No newline at end of file diff --git a/LICENSE b/LICENSE index f00485e..2998ae9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ -MIT License - -Copyright (c) 2021 Luke Shingles - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +MIT License + +Copyright (c) 2021-2024 Luke Shingles + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in index 1ee1cd5..c57b566 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,3 @@ recursive-include pynonthermal *.* -include requirements.txt \ No newline at end of file +include requirements.txt +include LICENCE \ No newline at end of file diff --git a/README.md b/README.md index 99cdc2a..084bd6e 100644 --- a/README.md +++ b/README.md @@ -1,54 +1,52 @@ -# pynonthermal -[![Build and test](https://github.com/lukeshingles/pynonthermal/actions/workflows/pythonapp.yml/badge.svg)](https://github.com/lukeshingles/pynonthermal/actions/workflows/pythonapp.yml) -[![codecov](https://codecov.io/gh/lukeshingles/pynonthermal/branch/main/graph/badge.svg?token=574XDCYFIi)](https://codecov.io/gh/lukeshingles/pynonthermal) - -pynonthermal is a non-thermal energy deposition (Spencer-Fano equation) solver. - -When high-energy leptons (electron and positrons) are injected into a plasma, they slow down by ionising and exciting atoms and ions, and Coulomb scattering with free (thermal) electrons. Keeping track of the rates of the processes is important for example, when modelling Type Ia supernovae at late times (>100 days). At late times, ionisation by high-energy non-thermal leptons (seeded by radioactive decay products) generally overtakes photoionisation, and the non-thermal contribution to ionisation is needed to obtain reasonable agreement with observed emission lines of singly- and doubly-ionised species. - -The numerical details of the solver are similar to the Spencer-Fano solver in the [ARTIS](https://github.com/artis-mcrt/artis) radiative transfer code ([Shingles et al. 2020](https://ui.adsabs.harvard.edu/abs/2020MNRAS.492.2029S/abstract)), which itself is an independent implementation of the [Kozma & Fransson (1992)](https://ui.adsabs.harvard.edu/abs/1992ApJ...390..602K/abstract) solution to the [Spencer & Fano (1945)](https://ui.adsabs.harvard.edu/abs/1954PhRv...93.1172S/abstract) equation. A similar solver is also applied in the [CMFGEN code](https://kookaburra.phyast.pitt.edu/hillier/web/CMFGEN.htm). - -The impact ionisation cross sections are formula fits from [Arnaud & Rothenflug (1985)](https://ui.adsabs.harvard.edu/abs/1985A%26AS...60..425A/abstract) and [Arnaud & Raymond (1992)](https://ui.adsabs.harvard.edu/abs/1992ApJ...398..394A/abstract). - -If the internal set of levels and transitions are applied (e.g., using ```add_ion_ltepopexcitation()```), these are imported from the [CMFGEN](https://kookaburra.phyast.pitt.edu/hillier/web/CMFGEN.htm) atomic data compilation. See the individual source files for atomic data references. - -## Example output -The following plot shows the energy distribution of contributions to ionisation, excitation, and heating for a pure oxygen plasma (electron fraction 1e-2), reproducing figure 2 of KF92. The area under each curve gives the fraction of deposited energy going into that particular channel. - -![Emission plot](https://raw.githubusercontent.com/lukeshingles/pynonthermal/main/docs/oxygen_channels.svg) - -## Installation -For the latest experimental version, pynonthermal can be installed with: -```sh -python3 -m pip install git+git://github.com/lukeshingles/pynonthermal.git -``` - -If this version crashes or causes problems, you can try dropping back to a released version. -```sh -python3 -m pip install pynonthermal -``` - -## Usage -See the [quickstart notebook](https://github.com/lukeshingles/pynonthermal/blob/main/quickstart.ipynb) [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/lukeshingles/pynonthermal/HEAD?filepath=quickstart.ipynb) for an example of how to set up the composition and use the solver to determine ionisation and heating rates. - -## Advanced Usage -Advanced users will likely want to control the particular excitation transitions that are included in the solver. Individual excitation transitions can be added with: - -```python -SpencerFanoSolver.add_excitation( - Z, ion_stage, n_level, xs_vec, epsilon_trans_ev, transitionkey=(lower, upper) -) -``` -Z is the atomic number. ion_stage is the one more than the ion charge (e.g., Fe I or ion stage 1 has charge zero). The argument xs_vec is a numpy array of cross sections [cm2] defined at every energy in the sf.engrid array [eV]. The transition key can be almost anything that is unique within the ion and is used to refer back to the level pair when requesting the excitation rate coefficient. - -```python -nt_exc = SpencerFanoSolver.get_excitation_ratecoeff(Z, ion_stage, transitionkey) -``` - -## Meta - -Distributed under the MIT license. See ``LICENSE`` for more information. - -https://github.com/lukeshingles/pynonthermal - - +# pynonthermal +[![Build and test](https://github.com/lukeshingles/pynonthermal/actions/workflows/pythonapp.yml/badge.svg)](https://github.com/lukeshingles/pynonthermal/actions/workflows/pythonapp.yml) +[![codecov](https://codecov.io/gh/lukeshingles/pynonthermal/branch/main/graph/badge.svg?token=574XDCYFIi)](https://codecov.io/gh/lukeshingles/pynonthermal) + +pynonthermal is a non-thermal energy deposition (Spencer-Fano equation) solver. + +When high-energy leptons (electron and positrons) are injected into a plasma, they slow down by ionising and exciting atoms and ions, and Coulomb scattering with free (thermal) electrons. Keeping track of the rates of the processes is important for example, when modelling Type Ia supernovae at late times (>100 days). At late times, ionisation by high-energy non-thermal leptons (seeded by radioactive decay products) generally overtakes photoionisation, and the non-thermal contribution to ionisation is needed to obtain reasonable agreement with observed emission lines of singly- and doubly-ionised species. + +The numerical details of the solver are similar to the Spencer-Fano solver in the [ARTIS](https://github.com/artis-mcrt/artis) radiative transfer code ([Shingles et al. 2020](https://ui.adsabs.harvard.edu/abs/2020MNRAS.492.2029S/abstract)), which itself is an independent implementation of the [Kozma & Fransson (1992)](https://ui.adsabs.harvard.edu/abs/1992ApJ...390..602K/abstract) solution to the [Spencer & Fano (1945)](https://ui.adsabs.harvard.edu/abs/1954PhRv...93.1172S/abstract) equation. A similar solver is also applied in the [CMFGEN code](https://kookaburra.phyast.pitt.edu/hillier/web/CMFGEN.htm). + +The impact ionisation cross sections are formula fits from [Arnaud & Rothenflug (1985)](https://ui.adsabs.harvard.edu/abs/1985A%26AS...60..425A/abstract) and [Arnaud & Raymond (1992)](https://ui.adsabs.harvard.edu/abs/1992ApJ...398..394A/abstract). + +If the internal set of levels and transitions are applied (e.g., using ```add_ion_ltepopexcitation()```), these are imported from the [CMFGEN](https://kookaburra.phyast.pitt.edu/hillier/web/CMFGEN.htm) atomic data compilation. See the individual source files for atomic data references. + +## Example output +The following plot shows the energy distribution of contributions to ionisation, excitation, and heating for a pure oxygen plasma (electron fraction 1e-2), reproducing figure 2 of KF92. The area under each curve gives the fraction of deposited energy going into that particular channel. + +![Emission plot](https://raw.githubusercontent.com/lukeshingles/pynonthermal/main/docs/oxygen_channels.svg) + +## Installation +For the latest experimental version, pynonthermal can be installed with: +```sh +python3 -m pip install git+git://github.com/lukeshingles/pynonthermal.git +``` + +If this version crashes or causes problems, you can try dropping back to a released version. +```sh +python3 -m pip install pynonthermal +``` + +## Usage +See the [quickstart notebook](https://github.com/lukeshingles/pynonthermal/blob/main/quickstart.ipynb) [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/lukeshingles/pynonthermal/HEAD?filepath=quickstart.ipynb) for an example of how to set up the composition and use the solver to determine ionisation and heating rates. + +## Advanced Usage +Advanced users will likely want to control the particular excitation transitions that are included in the solver. Individual excitation transitions can be added with: + +```python +SpencerFanoSolver.add_excitation(Z, ion_stage, n_level, xs_vec, epsilon_trans_ev, transitionkey=(lower, upper)) +``` +Z is the atomic number. ion_stage is the one more than the ion charge (e.g., Fe I or ion stage 1 has charge zero). The argument xs_vec is a numpy array of cross sections [cm2] defined at every energy in the sf.engrid array [eV]. The transition key can be almost anything that is unique within the ion and is used to refer back to the level pair when requesting the excitation rate coefficient. + +```python +nt_exc = SpencerFanoSolver.get_excitation_ratecoeff(Z, ion_stage, transitionkey) +``` + +## Meta + +Distributed under the MIT license. See ``LICENSE`` for more information. + +https://github.com/lukeshingles/pynonthermal + + diff --git a/pynonthermal/__init__.py b/pynonthermal/__init__.py index d248a23..047e672 100644 --- a/pynonthermal/__init__.py +++ b/pynonthermal/__init__.py @@ -1,9 +1,5 @@ -#!/usr/bin/env python3 - -from .spencerfano import SpencerFanoSolver -from .base import ( - DATADIR, - get_energyindex_lteq, - get_energyindex_gteq, - electronlossfunction, -) +from pynonthermal.base import DATADIR +from pynonthermal.base import electronlossfunction +from pynonthermal.base import get_energyindex_gteq +from pynonthermal.base import get_energyindex_lteq +from pynonthermal.spencerfano import SpencerFanoSolver diff --git a/pynonthermal/axelrod.py b/pynonthermal/axelrod.py index 8863fa7..018d07f 100644 --- a/pynonthermal/axelrod.py +++ b/pynonthermal/axelrod.py @@ -1,25 +1,27 @@ # functions related to Axelrod 1980 non-thermal treatment import math -import numpy as np from pathlib import Path -import pynonthermal +import numpy as np -from pynonthermal.constants import CLIGHT, EV, H, ME, QE +import pynonthermal +from pynonthermal.constants import CLIGHT +from pynonthermal.constants import EV +from pynonthermal.constants import H +from pynonthermal.constants import ME +from pynonthermal.constants import QE def read_binding_energies(modelpath=None): collionfilename = Path(pynonthermal.DATADIR, "binding_energies.txt") - with open(collionfilename, "r") as f: - nt_shells, n_z_binding = [int(x) for x in f.readline().split()] + with collionfilename.open() as f: + nt_shells, n_z_binding = (int(x) for x in f.readline().split()) electron_binding = np.zeros((n_z_binding, nt_shells)) for i in range(n_z_binding): - electron_binding[i] = ( - np.array([float(x) for x in f.readline().split()]) * EV - ) + electron_binding[i] = np.array([float(x) for x in f.readline().split()]) * EV return electron_binding @@ -31,7 +33,7 @@ def get_electronoccupancy(atomic_number, ion_stage, nt_shells): ioncharge = ion_stage - 1 nbound = atomic_number - ioncharge # number of bound electrons - for electron_loop in range(nbound): + for _ in range(nbound): if q[0] < 2: # K 1s q[0] += 1 elif q[1] < 2: # L1 2s @@ -143,9 +145,7 @@ def get_mean_binding_energy_alt(atomic_number, ion_stage, electron_binding, ionp return total / ecount -def get_lotz_xs_ionisation( - atomic_number, ion_stage, electron_binding, ionpot_ev, en_ev -): +def get_lotz_xs_ionisation(atomic_number, ion_stage, electron_binding, ionpot_ev, en_ev): # Axelrod 1980 Eq 3.38 en_erg = en_ev * EV @@ -177,22 +177,13 @@ def get_lotz_xs_ionisation( assert electron_loop == 8 # print("Z = %d, ion_stage = %d\n", get_element(element), get_ion_stage(element, ion)); - if use2 < use3: - p = use3 - else: - p = use2 + p = use3 if use2 < use3 else use2 if 0.5 * beta**2 * ME * CLIGHT**2 > p: part_sigma += ( electronsinshell / p - * ( - ( - math.log(beta**2 * ME * CLIGHT**2 / 2.0 / p) - - math.log10(1 - beta**2) - - beta**2 - ) - ) + * (math.log(beta**2 * ME * CLIGHT**2 / 2.0 / p) - math.log10(1 - beta**2) - beta**2) ) Aconst = 1.33e-14 * EV * EV @@ -234,11 +225,7 @@ def get_Latom_axelrod(Zboundbar, en_ev): * QE**4 / (ME * vel**2) * Zboundbar - * ( - math.log(2 * ME * vel**2 / I) - + math.log(1.0 / (1.0 - beta**2)) - - beta**2 - ) + * (math.log(2 * ME * vel**2 / I) + math.log(1.0 / (1.0 - beta**2)) - beta**2) ) @@ -265,11 +252,7 @@ def get_Lelec_axelrod(en_ev, n_e, n_e_tot, n_tot): / (ME * vel**2) * n_e / n_tot - * ( - math.log(2 * ME * vel**2 / (HBAR * omegap)) - + 0.5 * math.log(1.0 / (1.0 - beta**2)) - - 0.5 * beta**2 - ) + * (math.log(2 * ME * vel**2 / (HBAR * omegap)) + 0.5 * math.log(1.0 / (1.0 - beta**2)) - 0.5 * beta**2) ) diff --git a/pynonthermal/base.py b/pynonthermal/base.py index 828e8d2..0266ef3 100755 --- a/pynonthermal/base.py +++ b/pynonthermal/base.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 import math - from pathlib import Path -from .constants import EV, H, ME, QE +from pynonthermal.constants import EV +from pynonthermal.constants import H +from pynonthermal.constants import ME +from pynonthermal.constants import QE DATADIR = Path(__file__).absolute().parent / "data" @@ -29,14 +31,7 @@ def electronlossfunction(energy_ev, n_e_cgs): else: v = math.sqrt(2 * energy / ME) # velocity in m/s eulergamma = 0.577215664901532 - lossfunc = ( - n_e - * 2 - * math.pi - * QE**4 - / energy - * math.log(ME * pow(v, 3) / (eulergamma * pow(QE, 2) * omegap)) - ) + lossfunc = n_e * 2 * math.pi * QE**4 / energy * math.log(ME * pow(v, 3) / (eulergamma * pow(QE, 2) * omegap)) # lossfunc is now [erg / cm] return lossfunc / EV # return as [eV / cm] @@ -83,10 +78,11 @@ def get_energyindex_lteq(en_ev, engrid): if index < 0: return 0 - elif index > len(engrid) - 1: + + if index > len(engrid) - 1: return len(engrid) - 1 - else: - return index + + return index def get_energyindex_gteq(en_ev, engrid): @@ -97,7 +93,8 @@ def get_energyindex_gteq(en_ev, engrid): if index < 0: return 0 - elif index > len(engrid) - 1: + + if index > len(engrid) - 1: return len(engrid) - 1 - else: - return index + + return index diff --git a/pynonthermal/collion.py b/pynonthermal/collion.py index dae9387..22af8fc 100644 --- a/pynonthermal/collion.py +++ b/pynonthermal/collion.py @@ -1,23 +1,24 @@ import math -import pandas as pd -import numpy as np -from pathlib import Path from math import atan +from pathlib import Path + +import numpy as np +import pandas as pd import pynonthermal def read_colliondata(collionfilename="collion.txt"): - with open(Path(pynonthermal.DATADIR, collionfilename), "r") as collionfile: - expectedrowcount = int(collionfile.readline().strip()) # can ignore this line + with open(Path(pynonthermal.DATADIR, collionfilename)) as collionfile: # noqa: PTH123 + _expectedrowcount = int(collionfile.readline().strip()) # can ignore this line dfcollion = pd.read_csv( collionfile, - delim_whitespace=True, + sep=r"\s+", header=None, names=["Z", "nelec", "n", "l", "ionpot_ev", "A", "B", "C", "D"], ) - dfcollion.eval("ion_stage = Z - nelec + 1", inplace=True) + dfcollion["ion_stage"] = dfcollion["Z"] - dfcollion["nelec"] + 1 return dfcollion @@ -58,9 +59,9 @@ def get_J(Z, ion_stage, ionpot_ev): if ion_stage == 1: if Z == 2: # He I return 15.8 - elif Z == 10: # Ne I + if Z == 10: # Ne I return 24.2 - elif Z == 18: # Ar I + if Z == 18: # Ar I return 10.0 return 0.6 * ionpot_ev @@ -73,25 +74,13 @@ def ar_xs(energy_ev, ionpot_ev, A, B, C, D): return ( 1e-14 - * ( - A * (1 - 1 / u) - + B * pow((1 - 1 / u), 2) - + C * math.log(u) - + D * math.log(u) / u - ) + * (A * (1 - 1 / u) + B * pow((1 - 1 / u), 2) + C * math.log(u) + D * math.log(u) / u) / (u * pow(ionpot_ev, 2)) ) def get_arxs_array_shell(arr_enev, shell): - ar_xs_array = np.array( - [ - ar_xs(energy_ev, shell.ionpot_ev, shell.A, shell.B, shell.C, shell.D) - for energy_ev in arr_enev - ] - ) - - return ar_xs_array + return np.array([ar_xs(energy_ev, shell.ionpot_ev, shell.A, shell.B, shell.C, shell.D) for energy_ev in arr_enev]) def get_arxs_array_ion(arr_enev, dfcollion, Z, ion_stage): diff --git a/pynonthermal/excitation.py b/pynonthermal/excitation.py index 30539da..5f2bf3d 100644 --- a/pynonthermal/excitation.py +++ b/pynonthermal/excitation.py @@ -1,10 +1,16 @@ -import numpy as np import math -import pandas as pd +import numpy as np +import pandas as pd import pynonthermal -from pynonthermal.constants import CLIGHT, EV, H, H_ionpot, K_B, ME, QE +from pynonthermal.constants import CLIGHT +from pynonthermal.constants import EV +from pynonthermal.constants import H +from pynonthermal.constants import H_ionpot +from pynonthermal.constants import K_B +from pynonthermal.constants import ME +from pynonthermal.constants import QE use_collstrengths = False @@ -17,17 +23,10 @@ def get_lte_pops(adata, Z, ion_stage, n_ion, temperature): Z = ion.Z ion_stage = ion.ion_stage - ltepartfunc = ion.levels.eval( - "g * exp(-energy_ev / @K_B / @temperature)" - ).sum() + ltepartfunc = ion.levels.eval("g * exp(-energy_ev / @K_B / @temperature)").sum() for levelindex, level in ion.levels.iterrows(): - ion_popfrac = ( - 1.0 - / ltepartfunc - * level.g - * math.exp(-level.energy_ev / K_B / temperature) - ) + ion_popfrac = 1.0 / ltepartfunc * level.g * math.exp(-level.energy_ev / K_B / temperature) levelnumberdensity = n_ion * ion_popfrac poprow = ( @@ -38,13 +37,11 @@ def get_lte_pops(adata, Z, ion_stage, n_ion, temperature): ) poplist.append(poprow) - dfpop = pd.DataFrame(poplist, columns=["level", "n_LTE", "n_NLTE", "ion_popfrac"]) - return dfpop + return pd.DataFrame(poplist, columns=["level", "n_LTE", "n_NLTE", "ion_popfrac"]) def get_xs_excitation(en_ev, row): - """Get the excitation cross section in cm^2 at energy en_ev [eV]""" - + """Get the excitation cross section in cm^2 at energy en_ev [eV].""" A_naught_squared = 2.800285203e-17 # Bohr radius squared in cm^2 coll_str = row.collstr @@ -57,13 +54,11 @@ def get_xs_excitation(en_ev, row): if coll_str >= 0 and use_collstrengths: # collision strength is available, so use it # Li et al. 2012 equation 11 - constantfactor = ( - pow(H_ionpot, 2) / row.lower_g * coll_str * math.pi * A_naught_squared - ) + constantfactor = pow(H_ionpot, 2) / row.lower_g * coll_str * math.pi * A_naught_squared return constantfactor * (en_ev * EV) ** -2 - elif not row.forbidden: + if not row.forbidden: nu_trans = epsilon_trans / H g = row.upper_g / row.lower_g fij = g * ME * pow(CLIGHT, 3) / (8 * pow(QE * nu_trans * math.pi, 2)) * row.A @@ -76,9 +71,7 @@ def get_xs_excitation(en_ev, row): prefactor = 45.585750051 # Eq 4 of Mewe 1972, possibly from Seaton 1962? - constantfactor = ( - prefactor * A_naught_squared * pow(H_ionpot / epsilon_trans, 2) * fij - ) + constantfactor = prefactor * A_naught_squared * pow(H_ionpot / epsilon_trans, 2) * fij U = en_ev / epsilon_trans_ev g_bar = A * np.log(U) + B @@ -89,9 +82,7 @@ def get_xs_excitation(en_ev, row): def get_xs_excitation_vector(engrid, row): - """Get an array containing the excitation cross section in cm^2 at every energy in the array engrid (eV) - """ - + """Get an array containing the excitation cross section in cm^2 at every energy in the array engrid (eV).""" A_naught_squared = 2.800285203e-17 # Bohr radius squared in cm^2 npts = len(engrid) xs_excitation_vec = np.empty(npts) @@ -100,21 +91,15 @@ def get_xs_excitation_vector(engrid, row): epsilon_trans = row.epsilon_trans_ev * EV epsilon_trans_ev = row.epsilon_trans_ev - startindex = pynonthermal.get_energyindex_gteq( - en_ev=epsilon_trans_ev, engrid=engrid - ) + startindex = pynonthermal.get_energyindex_gteq(en_ev=epsilon_trans_ev, engrid=engrid) xs_excitation_vec[:startindex] = 0.0 if coll_str >= 0 and use_collstrengths: # collision strength is available, so use it # Li et al. 2012 equation 11 - constantfactor = ( - pow(H_ionpot, 2) / row.lower_g * coll_str * math.pi * A_naught_squared - ) + constantfactor = pow(H_ionpot, 2) / row.lower_g * coll_str * math.pi * A_naught_squared - xs_excitation_vec[startindex:] = ( - constantfactor * (engrid[startindex:] * EV) ** -2 - ) + xs_excitation_vec[startindex:] = constantfactor * (engrid[startindex:] * EV) ** -2 elif not row.forbidden: nu_trans = epsilon_trans / H @@ -129,9 +114,7 @@ def get_xs_excitation_vector(engrid, row): prefactor = 45.585750051 # Eq 4 of Mewe 1972, possibly from Seaton 1962? - constantfactor = ( - prefactor * A_naught_squared * pow(H_ionpot / epsilon_trans, 2) * fij - ) + constantfactor = prefactor * A_naught_squared * pow(H_ionpot / epsilon_trans, 2) * fij U = engrid[startindex:] / epsilon_trans_ev g_bar = A * np.log(U) + B diff --git a/pynonthermal/spencerfano.py b/pynonthermal/spencerfano.py index 70a4a90..de70add 100644 --- a/pynonthermal/spencerfano.py +++ b/pynonthermal/spencerfano.py @@ -1,23 +1,22 @@ -import math -import matplotlib.pyplot as plt -import numpy as np +from __future__ import annotations -from scipy import linalg +import math from math import atan from pathlib import Path import artistools as at +import matplotlib.pyplot as plt +import numpy as np +from scipy import linalg + import pynonthermal import pynonthermal.collion import pynonthermal.excitation - -from .base import electronlossfunction +from pynonthermal.base import electronlossfunction class SpencerFanoSolver: - def __init__( - self, emin_ev=1, emax_ev=3000, npts=4000, verbose=False, use_ar1985=False - ): + def __init__(self, emin_ev=1, emax_ev=3000, npts=4000, verbose=False, use_ar1985=False): self._solved = False self.ionpopdict = {} # key is (Z, ion_stage) value is number density @@ -63,12 +62,11 @@ def __init__( self.sfmatrix = np.zeros((npts, npts)) def __enter__(self): - # print('enter method called') + """Enter the context manager.""" return self def __exit__(self, exc_type, exc_value, exc_traceback): - # print('exit method called') - pass + """Exit the context manager.""" def get_energyindex_lteq(self, en_ev): return pynonthermal.get_energyindex_lteq(en_ev, engrid=self.engrid) @@ -88,8 +86,7 @@ def add_excitation( epsilon_trans_ev, transitionkey=None, ): - """ - Add a bound-bound non-thermal collisional excitation to the solver. + """Add a bound-bound non-thermal collisional excitation to the solver. levelnumberdensity: the level population density in cm^-3 @@ -107,9 +104,7 @@ def add_excitation( self.excitationlists[(Z, ion_stage)] = {} if transitionkey is None: - transitionkey = len( - self.excitationlists[(Z, ion_stage)] - ) # simple number index + transitionkey = len(self.excitationlists[(Z, ion_stage)]) # simple number index assert transitionkey not in self.excitationlists[(Z, ion_stage)] self.excitationlists[(Z, ion_stage)][transitionkey] = ( @@ -117,41 +112,29 @@ def add_excitation( xs_vec, epsilon_trans_ev, ) - vec_xs_excitation_levelnumberdensity_deltae = ( - levelnumberdensity * self.deltaen * xs_vec - ) + vec_xs_excitation_levelnumberdensity_deltae = levelnumberdensity * self.deltaen * xs_vec xsstartindex = self.get_energyindex_lteq(en_ev=epsilon_trans_ev) for i, en in enumerate(self.engrid): stopindex = self.get_energyindex_lteq(en_ev=en + epsilon_trans_ev) - startindex = i if i > xsstartindex else xsstartindex + startindex = max(i, xsstartindex) # for j in range(startindex, stopindex): - self.sfmatrix[ - i, startindex:stopindex - ] += vec_xs_excitation_levelnumberdensity_deltae[startindex:stopindex] + self.sfmatrix[i, startindex:stopindex] += vec_xs_excitation_levelnumberdensity_deltae[startindex:stopindex] # do the last bit separately because we're not using the full deltaen interval delta_en_actual = en + epsilon_trans_ev - self.engrid[stopindex] self.sfmatrix[i, stopindex] += ( - vec_xs_excitation_levelnumberdensity_deltae[stopindex] - * delta_en_actual - / self.deltaen + vec_xs_excitation_levelnumberdensity_deltae[stopindex] * delta_en_actual / self.deltaen ) - def add_ion_ltepopexcitation( - self, Z, ion_stage, n_ion, temperature=3000, adata=None - ): + def add_ion_ltepopexcitation(self, Z, ion_stage, n_ion, temperature=3000, adata=None): if adata is None: # use ARTIS atomic data read by the artistools package to get the levels - adata = at.atomic.get_levels( - Path(pynonthermal.DATADIR, "artis_files"), get_transitions=True - ) + adata = at.atomic.get_levels(Path(pynonthermal.DATADIR, "artis_files"), get_transitions=True) - dfpops_thision = pynonthermal.excitation.get_lte_pops( - adata, Z, ion_stage, n_ion, temperature=temperature - ) + dfpops_thision = pynonthermal.excitation.get_lte_pops(adata, Z, ion_stage, n_ion, temperature=temperature) popdict = {x.level: x["n_NLTE"] for _, x in dfpops_thision.iterrows()} @@ -181,23 +164,16 @@ def add_ion_ltepopexcitation( dftransitions = ion.transitions.query(filterquery, inplace=False).copy() if not dftransitions.empty: - dftransitions.eval( + dftransitions = dftransitions.eval( "epsilon_trans_ev = @ion.levels.loc[upper].energy_ev.values -" " @ion.levels.loc[lower].energy_ev.values", - inplace=True, ) - dftransitions.query("epsilon_trans_ev >= @self.engrid[0]", inplace=True) + dftransitions = dftransitions.query("epsilon_trans_ev >= @self.engrid[0]") if not dftransitions.empty: - dftransitions.eval( - "lower_g = @ion.levels.loc[lower].g.values", inplace=True - ) - dftransitions.eval( - "upper_g = @ion.levels.loc[upper].g.values", inplace=True - ) - dftransitions["lower_pop"] = dftransitions.apply( - lambda x: popdict.get(x.lower, 0.0), axis=1 - ) + dftransitions = dftransitions.eval("lower_g = @ion.levels.loc[lower].g.values") + dftransitions = dftransitions.eval("upper_g = @ion.levels.loc[upper].g.values") + dftransitions["lower_pop"] = dftransitions.apply(lambda x: popdict.get(x.lower, 0.0), axis=1) if self.verbose: print( @@ -209,12 +185,10 @@ def add_ion_ltepopexcitation( ) for _, transition in dftransitions.iterrows(): - levelnumberdensity = transition.lower_pop epsilon_trans_ev = transition.epsilon_trans_ev if epsilon_trans_ev >= self.engrid[0]: - xs_vec = pynonthermal.excitation.get_xs_excitation_vector( - self.engrid, transition - ) + xs_vec = pynonthermal.excitation.get_xs_excitation_vector(self.engrid, transition) + levelnumberdensity = transition.lower_pop self.add_excitation( Z, ion_stage, @@ -238,11 +212,7 @@ def _add_ionisation_shell(self, n_ion, shell): if ionpot_ev <= self.engrid[0]: xsstartindex = 0 else: - xsstartindex = npts + 1 - for i in range(npts): - if ar_xs_array[i] > 0.0: - xsstartindex = i - break + xsstartindex = next((i for i in range(npts) if ar_xs_array[i] > 0.0), npts + 1) xsstartindex = self.get_energyindex_gteq(en_ev=ionpot_ev) # J * atan[(epsilon - ionpot_ev) / J] is the indefinite integral of @@ -250,11 +220,7 @@ def _add_ionisation_shell(self, n_ion, shell): # in Kozma & Fransson 1992 equation 4 prefactors = [ - n_ion - * ar_xs_array[j] - / atan((self.engrid[j] - ionpot_ev) / 2.0 / J) - * deltaen - for j in range(npts) + n_ion * ar_xs_array[j] / atan((self.engrid[j] - ionpot_ev) / 2.0 / J) * deltaen for j in range(npts) ] # Luke Shingles: the use of min and max on the epsilon limits keeps energies @@ -263,20 +229,12 @@ def _add_ionisation_shell(self, n_ion, shell): # I had neglected this, so the limits of integration were incorrect. The fix didn't massively affect # ionisation rates or spectra, but it was a source of error that let to energy fractions not adding up to 100%. - epsilon_uppers = [ - min((self.engrid[j] + ionpot_ev) / 2, self.engrid[j]) for j in range(npts) - ] - int_eps_uppers = [ - atan((epsilon_upper - ionpot_ev) / J) for epsilon_upper in epsilon_uppers - ] + epsilon_uppers = [min((self.engrid[j] + ionpot_ev) / 2, self.engrid[j]) for j in range(npts)] + int_eps_uppers = [atan((epsilon_upper - ionpot_ev) / J) for epsilon_upper in epsilon_uppers] # for the resulting arrays, use index j - i corresponding to energy endash - en - epsilon_lowers1 = [ - max(self.engrid[j] - self.engrid[0], ionpot_ev) for j in range(npts) - ] - int_eps_lowers1 = [ - atan((epsilon_lower - ionpot_ev) / J) for epsilon_lower in epsilon_lowers1 - ] + epsilon_lowers1 = [max(self.engrid[j] - self.engrid[0], ionpot_ev) for j in range(npts)] + int_eps_lowers1 = [atan((epsilon_lower - ionpot_ev) / J) for epsilon_lower in epsilon_lowers1] for i, en in enumerate(self.engrid): # endash ranges from en to SF_EMAX, but skip over the zero-cross section points @@ -293,9 +251,7 @@ def _add_ionisation_shell(self, n_ion, shell): # integral at y(E') where E' >= E and E' = engrid[j] if epsilon_lowers1[j - i] <= epsilon_uppers[j]: - self.sfmatrix[i, j] += prefactors[j] * ( - int_eps_uppers[j] - int_eps_lowers1[j - i] - ) + self.sfmatrix[i, j] += prefactors[j] * (int_eps_uppers[j] - int_eps_lowers1[j - i]) if 2 * en + ionpot_ev < self.engrid[-1] + (self.engrid[1] - self.engrid[0]): secondintegralstartindex = self.get_energyindex_lteq(2 * en + ionpot_ev) @@ -310,9 +266,7 @@ def _add_ionisation_shell(self, n_ion, shell): for j in range(secondintegralstartindex, npts): if epsilon_lower2 <= epsilon_uppers[j]: int_eps_lower2 = atan((epsilon_lower2 - ionpot_ev) / J) - self.sfmatrix[i, j] -= prefactors[j] * ( - int_eps_uppers[j] - int_eps_lower2 - ) + self.sfmatrix[i, j] -= prefactors[j] * (int_eps_uppers[j] - int_eps_lower2) def add_ionisation(self, Z, ion_stage, n_ion): assert not self._solved @@ -328,9 +282,7 @@ def add_ionisation(self, Z, ion_stage, n_ion): ) assert n_ion > 0.0 self.ionpopdict[(Z, ion_stage)] = n_ion - dfcollion_thision = self.dfcollion.query( - "Z == @Z and ion_stage == @ion_stage", inplace=False - ) + dfcollion_thision = self.dfcollion.query("Z == @Z and ion_stage == @ion_stage", inplace=False) for index, shell in dfcollion_thision.iterrows(): assert shell.ionpot_ev >= self.engrid[0] @@ -339,7 +291,7 @@ def add_ionisation(self, Z, ion_stage, n_ion): def calculate_n_e(self): # number density of free electrons [cm-^3] self._n_e = 0.0 - for Z, ion_stage in self.ionpopdict.keys(): + for Z, ion_stage in self.ionpopdict: charge = ion_stage - 1 assert charge >= 0 self._n_e += charge * self.ionpopdict[(Z, ion_stage)] @@ -353,7 +305,7 @@ def get_n_e(self): def get_n_ion_tot(self): # total number density of all nuclei [cm^-3] n_ion_tot = 0.0 - for Z, ion_stage in self.ionpopdict.keys(): + for Z, ion_stage in self.ionpopdict: n_ion_tot += self.ionpopdict[(Z, ion_stage)] return n_ion_tot @@ -404,20 +356,14 @@ def calculate_nt_frac_excitation_ion(self, Z, ion_stage): xs_excitation_vec_sum_alltrans = np.zeros(npts) - for transitionkey, ( + for ( levelnumberdensity, xsvec, epsilon_trans_ev, - ) in self.excitationlists[(Z, ion_stage)].items(): - xs_excitation_vec_sum_alltrans += ( - levelnumberdensity * epsilon_trans_ev * xsvec - ) + ) in self.excitationlists[(Z, ion_stage)].values(): + xs_excitation_vec_sum_alltrans += levelnumberdensity * epsilon_trans_ev * xsvec - return ( - np.dot(xs_excitation_vec_sum_alltrans, self.yvec) - * deltaen - / self.depositionratedensity_ev - ) + return np.dot(xs_excitation_vec_sum_alltrans, self.yvec) * deltaen / self.depositionratedensity_ev def calculate_N_e(self, energy_ev): # Kozma & Fransson equation 6. @@ -430,30 +376,22 @@ def calculate_N_e(self, energy_ev): deltaen = self.engrid[1] - self.engrid[0] - for Z, ion_stage in self.ionpopdict.keys(): + for Z, ion_stage in self.ionpopdict: N_e_ion = 0.0 n_ion = self.ionpopdict[(Z, ion_stage)] if self.excitationlists and (Z, ion_stage) in self.excitationlists: - for levelnumberdensity, xsvec, epsilon_trans_ev in self.excitationlists[ - (Z, ion_stage) - ].values(): + for levelnumberdensity, xsvec, epsilon_trans_ev in self.excitationlists[(Z, ion_stage)].values(): if energy_ev + epsilon_trans_ev >= self.engrid[0]: - i = self.get_energyindex_lteq( - en_ev=energy_ev + epsilon_trans_ev - ) - N_e_ion += ( - (levelnumberdensity / n_ion) * self.yvec[i] * xsvec[i] - ) + i = self.get_energyindex_lteq(en_ev=energy_ev + epsilon_trans_ev) + N_e_ion += (levelnumberdensity / n_ion) * self.yvec[i] * xsvec[i] # enbelow = engrid[i] # enabove = engrid[i + 1] # x = (energy_ev - enbelow) / (enabove - enbelow) # yvecinterp = (1 - x) * yvec[i] + x * yvec[i + 1] # N_e_ion += (levelnumberdensity / n_ion) * yvecinterp * get_xs_excitation(energy_ev + epsilon_trans_ev, row) - dfcollion_thision = self.dfcollion.query( - "Z == @Z and ion_stage == @ion_stage", inplace=False - ) + dfcollion_thision = self.dfcollion.query("Z == @Z and ion_stage == @ion_stage", inplace=False) for index, shell in dfcollion_thision.iterrows(): ionpot_ev = shell.ionpot_ev @@ -461,9 +399,7 @@ def calculate_N_e(self, energy_ev): enlambda = min(self.engrid[-1] - energy_ev, energy_ev + ionpot_ev) J = pynonthermal.collion.get_J(shell.Z, shell.ion_stage, ionpot_ev) - ar_xs_array = pynonthermal.collion.get_arxs_array_shell( - self.engrid, shell - ) + ar_xs_array = pynonthermal.collion.get_arxs_array_shell(self.engrid, shell) # integral from ionpot to enlambda integral1startindex = self.get_energyindex_lteq(en_ev=ionpot_ev) @@ -476,9 +412,7 @@ def calculate_N_e(self, energy_ev): deltaen * self.yvec[k] * ar_xs_array[k] - * pynonthermal.collion.Psecondary( - e_p=self.engrid[k], epsilon=endash, ionpot_ev=ionpot_ev, J=J - ) + * pynonthermal.collion.Psecondary(e_p=self.engrid[k], epsilon=endash, ionpot_ev=ionpot_ev, J=J) ) # interpolate the y value @@ -491,21 +425,17 @@ def calculate_N_e(self, energy_ev): # e_p=energy_ev + endash, epsilon=endash, ionpot_ev=ionpot_ev, J=J) # integral from 2E + I up to E_max - integral2startindex = self.get_energyindex_lteq( - en_ev=2 * energy_ev + ionpot_ev - ) + integral2startindex = self.get_energyindex_lteq(en_ev=2 * energy_ev + ionpot_ev) N_e_ion += deltaen * sum( - [ - self.yvec[j] - * ar_xs_array[j] - * pynonthermal.collion.Psecondary( - e_p=self.engrid[j], - epsilon=energy_ev + ionpot_ev, - ionpot_ev=ionpot_ev, - J=J, - ) - for j in range(integral2startindex, len(self.engrid)) - ] + self.yvec[j] + * ar_xs_array[j] + * pynonthermal.collion.Psecondary( + e_p=self.engrid[j], + epsilon=energy_ev + ionpot_ev, + ionpot_ev=ionpot_ev, + J=J, + ) + for j in range(integral2startindex, len(self.engrid)) ) N_e += n_ion * N_e_ion @@ -516,7 +446,6 @@ def calculate_N_e(self, energy_ev): def calculate_frac_heating(self): # Kozma & Fransson equation 8 - self._frac_heating = 0.0 E_0 = self.engrid[0] n_e = self.get_n_e() @@ -525,20 +454,10 @@ def calculate_frac_heating(self): self._frac_heating += ( deltaen / self.depositionratedensity_ev - * sum( - [ - electronlossfunction(en_ev, n_e) * self.yvec[i] - for i, en_ev in enumerate(self.engrid) - ] - ) + * sum(electronlossfunction(en_ev, n_e) * self.yvec[i] for i, en_ev in enumerate(self.engrid)) ) - frac_heating_E_0_part = ( - E_0 - * self.yvec[0] - * electronlossfunction(E_0, n_e) - / self.depositionratedensity_ev - ) + frac_heating_E_0_part = E_0 * self.yvec[0] * electronlossfunction(E_0, n_e) / self.depositionratedensity_ev self._frac_heating += frac_heating_E_0_part @@ -549,13 +468,9 @@ def calculate_frac_heating(self): npts_integral = math.ceil(E_0 / deltaen) * 5 # if self.verbose: # print(f'N_e npts_integral: {npts_integral}') - arr_en, deltaen2 = np.linspace( - 0.0, E_0, num=npts_integral, retstep=True, endpoint=True - ) + arr_en, deltaen2 = np.linspace(0.0, E_0, num=npts_integral, retstep=True, endpoint=True) arr_en_N_e = [en_ev * self.calculate_N_e(en_ev) for en_ev in arr_en] - frac_heating_N_e += ( - 1.0 / self.depositionratedensity_ev * sum(arr_en_N_e) * deltaen2 - ) + frac_heating_N_e += 1.0 / self.depositionratedensity_ev * sum(arr_en_N_e) * deltaen2 if self.verbose: print(f" frac_heating(E 1: frac_ionisation_shell = 0.0 - print( - "WARNING: Ignoring invalid frac_ionisation_shell of" - f" {frac_ionisation_shell}." - ) + print("WARNING: Ignoring invalid frac_ionisation_shell of" f" {frac_ionisation_shell}.") self._frac_ionisation_ion[(Z, ion_stage)] += frac_ionisation_shell eta_over_ionpot_sum += frac_ionisation_shell / shell.ionpot_ev self._frac_ionisation_tot += self._frac_ionisation_ion[(Z, ion_stage)] - eff_ionpot = ( - X_ion / eta_over_ionpot_sum if eta_over_ionpot_sum else float("inf") - ) + eff_ionpot = X_ion / eta_over_ionpot_sum if eta_over_ionpot_sum else float("inf") # eff_ionpot_usevalence = ( # ionpot_valence * X_ion / self._frac_ionisation_ion[(Z, ion_stage)] # if self._frac_ionisation_ion[(Z, ion_stage)] > 0. else float('inf')) if self.verbose: - print( - " frac_ionisation:" - f" {self._frac_ionisation_ion[(Z, ion_stage)]:.4f}" - ) + print(" frac_ionisation:" f" {self._frac_ionisation_ion[(Z, ion_stage)]:.4f}") if self.excitationlists: if n_ion > 0.0: - self._frac_excitation_ion[ - (Z, ion_stage) - ] = self.calculate_nt_frac_excitation_ion(Z, ion_stage) + self._frac_excitation_ion[(Z, ion_stage)] = self.calculate_nt_frac_excitation_ion(Z, ion_stage) else: self._frac_excitation_ion[(Z, ion_stage)] = 0.0 @@ -668,23 +565,15 @@ def analyse_ntspectrum(self): self._frac_excitation_tot += self._frac_excitation_ion[(Z, ion_stage)] if self.verbose: - print( - " frac_excitation:" - f" {self._frac_excitation_ion[(Z, ion_stage)]:.4f}" - ) + print(" frac_excitation:" f" {self._frac_excitation_ion[(Z, ion_stage)]:.4f}") else: self._frac_excitation_ion[(Z, ion_stage)] = 0.0 - self._nt_ionisation_ratecoeff[(Z, ion_stage)] = ( - self.depositionratedensity_ev / n_ion_tot / eff_ionpot - ) + self._nt_ionisation_ratecoeff[(Z, ion_stage)] = self.depositionratedensity_ev / n_ion_tot / eff_ionpot if self.verbose: print(f" eff_ionpot: {eff_ionpot:.2f} [eV]") # print(f' eff_ionpot_usevalence: {eff_ionpot_usevalence:.2f} [eV]') - print( - "ionisation ratecoeff:" - f" {self._nt_ionisation_ratecoeff[(Z, ion_stage)]:.2e} [/s]" - ) + print("ionisation ratecoeff:" f" {self._nt_ionisation_ratecoeff[(Z, ion_stage)]:.2e} [/s]") # complicated eff_ionpot thing should match a simple integral of xs * vec * dE # print(f'ionisation ratecoeff: {integralgamma:.2e} [/s]') @@ -708,8 +597,7 @@ def analyse_ntspectrum(self): if self.verbose: print(f" frac_heating: {frac_heating:.4f}") print( - " frac_sum:" - f" {self._frac_excitation_tot + self._frac_ionisation_tot + frac_heating:.4f}" + " frac_sum:" f" {self._frac_excitation_tot + self._frac_ionisation_tot + frac_heating:.4f}" ) def get_n_e_nt(self): @@ -756,46 +644,31 @@ def get_ionisation_ratecoeff(self, Z, ion_stage): def get_excitation_ratecoeff(self, Z, ion_stage, transitionkey): # integral in Kozma & Fransson equation 9 - levelnumberdensity, xsvec, epsilon_trans_ev = self.excitationlists[ - (Z, ion_stage) - ][transitionkey] + levelnumberdensity, xsvec, epsilon_trans_ev = self.excitationlists[(Z, ion_stage)][transitionkey] return np.dot(xsvec, self.yvec) * self.deltaen / self.depositionratedensity_ev def get_frac_sum(self): - return ( - self.get_frac_heating() - + self.get_frac_excitation_tot() - + self.get_frac_ionisation_tot() - ) + return self.get_frac_heating() + self.get_frac_excitation_tot() + self.get_frac_ionisation_tot() def get_d_etaheating_by_d_en_vec(self): assert self._solved - d_etaheating_by_d_en_vec = [ - self.electronlossfunction(self.engrid[i]) - * self.yvec[i] - / self.depositionratedensity_ev + return [ + self.electronlossfunction(self.engrid[i]) * self.yvec[i] / self.depositionratedensity_ev for i in range(len(self.engrid)) ] - return d_etaheating_by_d_en_vec - def get_d_etaexcitation_by_d_en_vec(self): assert self._solved part_integrand = np.zeros(len(self.engrid)) - for Z, ion_stage in self.excitationlists.keys(): - for transitionkey, ( + for Z, ion_stage in self.excitationlists: + for ( levelnumberdensity, xsvec, epsilon_trans_ev, - ) in self.excitationlists[(Z, ion_stage)].items(): - part_integrand += ( - levelnumberdensity - * epsilon_trans_ev - * xsvec - / self.depositionratedensity_ev - ) + ) in self.excitationlists[(Z, ion_stage)].values(): + part_integrand += levelnumberdensity * epsilon_trans_ev * xsvec / self.depositionratedensity_ev return self.yvec * part_integrand @@ -803,24 +676,18 @@ def get_d_etaion_by_d_en_vec(self): assert self._solved part_integrand = np.zeros(len(self.engrid)) - for Z, ion_stage in self.ionpopdict.keys(): + for Z, ion_stage in self.ionpopdict: n_ion = self.ionpopdict[(Z, ion_stage)] - dfcollion_thision = self.dfcollion.query( - "Z == @Z and ion_stage == @ion_stage", inplace=False - ) + dfcollion_thision = self.dfcollion.query("Z == @Z and ion_stage == @ion_stage", inplace=False) for index, shell in dfcollion_thision.iterrows(): xsvec = pynonthermal.collion.get_arxs_array_shell(self.engrid, shell) - part_integrand += ( - n_ion * shell.ionpot_ev * xsvec / self.depositionratedensity_ev - ) + part_integrand += n_ion * shell.ionpot_ev * xsvec / self.depositionratedensity_ev return self.yvec * part_integrand - def plot_yspectrum( - self, en_y_on_d_en=False, xscalelog=False, outputfilename=None, axis=None - ): + def plot_yspectrum(self, en_y_on_d_en=False, xscalelog=False, outputfilename=None, axis=None): assert self._solved fs = 12 if axis is None: @@ -893,7 +760,7 @@ def plot_channels(self, outputfilename=None, axis=None, xscalelog=False): etaheat_int[0] += E_0 * self.yvec[0] * self.electronlossfunction(E_0) - etatot_int = etaion_int + etaexc_int + etaheat_int + # etatot_int = etaion_int + etaexc_int + etaheat_int # go below E_0 deltaen = E_0 / 20.0 @@ -903,7 +770,7 @@ def plot_channels(self, outputfilename=None, axis=None, xscalelog=False): etaheat_int_low = np.zeros(len(engrid_low)) etaion_int_low = np.zeros(len(engrid_low)) etaexc_int_low = np.zeros(len(engrid_low)) - x = 0 + for i in reversed(range(len(engrid_low))): en_ev = engrid_low[i] N_e = self.calculate_N_e(en_ev) @@ -917,7 +784,7 @@ def plot_channels(self, outputfilename=None, axis=None, xscalelog=False): etaion_int_low[i] = etaion_int[0] # cross sections start above E_0 etaexc_int_low[i] = etaexc_int[0] - etatot_int_low = etaion_int_low + etaexc_int_low + etaheat_int_low + # etatot_int_low = etaion_int_low + etaexc_int_low + etaheat_int_low engridfull = np.append(engrid_low, self.engrid) # axes[0].plot(engridfull, np.append(etaion_int_low, etaion_int), marker="None", lw=1.5, color='C0', label='Ionisation') @@ -955,9 +822,7 @@ def plot_channels(self, outputfilename=None, axis=None, xscalelog=False): if self.get_frac_excitation_tot() > 0.0: axis.plot( engridfull, - np.append(np.zeros(npts_low), d_etaexc_by_d_en_vec) - * engridfull - / detaymax, + np.append(np.zeros(npts_low), d_etaexc_by_d_en_vec) * engridfull / detaymax, marker="None", lw=1.5, color="C1", @@ -976,12 +841,10 @@ def plot_channels(self, outputfilename=None, axis=None, xscalelog=False): ) axis.set_ylim(bottom=0, top=1.0) - axis.legend( - loc="best", handlelength=2, frameon=False, numpoints=1, prop={"size": 10} - ) + axis.legend(loc="best", handlelength=2, frameon=False, numpoints=1, prop={"size": 10}) axis.set_ylabel(r"E d$\eta$ / dE [eV$^{-1}$]", fontsize=fs) - etatot_int = etaion_int + etaexc_int + etaheat_int + # etatot_int = etaion_int + etaexc_int + etaheat_int # ax.annotate(modellabel, xy=(0.97, 0.95), xycoords='axes fraction', horizontalalignment='right', # verticalalignment='top', fontsize=fs) @@ -999,7 +862,7 @@ def plot_channels(self, outputfilename=None, axis=None, xscalelog=False): else: plt.show() - def plot_spec_channels(self, outputfilename, xscalelog=False): + def plot_spec_channels(self, outputfilename: Path | str, xscalelog: bool = False): fig, axes = plt.subplots( nrows=2, ncols=1, diff --git a/pynonthermal/tests/__init__.py b/pynonthermal/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pynonthermal/tests/test_sfsolve.py b/pynonthermal/tests/test_sfsolve.py index 096ce1c..85dd7f2 100644 --- a/pynonthermal/tests/test_sfsolve.py +++ b/pynonthermal/tests/test_sfsolve.py @@ -1,5 +1,7 @@ -import numpy as np from pathlib import Path + +import numpy as np + import pynonthermal outputfolder = Path(__file__).absolute().parent / "output" @@ -13,10 +15,8 @@ def test_helium(): (2, 1, 1.0 - x_e), (2, 2, x_e), ] - - with pynonthermal.SpencerFanoSolver( - emin_ev=1, emax_ev=3000, npts=2000, verbose=True - ) as sf: + print(dir(pynonthermal)) + with pynonthermal.SpencerFanoSolver(emin_ev=1, emax_ev=3000, npts=2000, verbose=True) as sf: for Z, ion_stage, n_ion in ions: sf.add_ionisation(Z, ion_stage, n_ion=n_ion) sf.add_ion_ltepopexcitation(Z, ion_stage, n_ion=n_ion) @@ -33,6 +33,4 @@ def test_helium(): assert np.isclose(frac_excitation_tot, 0.3315, atol=0.05) assert np.isclose(frac_ionisation_tot, 0.4849, atol=0.05) - sf.plot_spec_channels( - outputfilename=outputfolder / "spec_channels.pdf", xscalelog=True - ) + sf.plot_spec_channels(outputfilename=outputfolder / "spec_channels.pdf", xscalelog=True) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..518908c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,177 @@ +[build-system] +requires = ["setuptools>=68.2", "setuptools_scm[toml]>=8"] +build-backend = "setuptools.build_meta" + +[project] +name = "pynonthermal" +authors = [ + {name = "Luke J. Shingles", email = "luke.shingles@gmail.com"}, +] +description="A non-thermal electron deposition (Spencer-Fano equation) solver." +classifiers = [ + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Intended Audience :: Science/Research", +] +dynamic = ["version", "dependencies", "entry-points", "readme", "scripts"] +requires-python = ">=3.8" +license = { file = "LICENSE" } + +[project.urls] +Repository = "https://www.github.com/lukeshingles/pynonthermal/" + +[tool.mypy] +check_untyped_defs = false +disallow_any_explicit = false +disallow_any_generics = false +disallow_any_unimported = false +disallow_incomplete_defs = false +disallow_subclassing_any = false +disallow_untyped_calls = false +disallow_untyped_defs = false +error_summary = true +enable_error_code = [ + "comparison-overlap", + "ignore-without-code", + "redundant-expr", + "redundant-self", + "truthy-bool", + "unused-awaitable", +] +exclude = "(build)|(dist)|(tests)|(data)" +ignore_missing_imports = true +implicit_optional = false +implicit_reexport = true +packages = "pynonthermal" +plugins = 'numpy.typing.mypy_plugin' +pretty = true +python_version = '3.12' +scripts_are_modules = true +strict_equality = true +extra_checks = true +warn_redundant_casts = true +warn_unreachable = false +warn_unused_configs = true +warn_unused_ignores = true + +[tool.pyright] +exclude=['**/node_modules','**/__pycache__','**/.*', 'build', 'dist'] +useLibraryCodeForTypes = false + +[tool.pytest.ini_options] +addopts = " --durations=0 --typeguard-packages=pynonthermal" + +[tool.ruff] +line-length = 120 +target-version = "py38" +fix = true +show-fixes = true + +[tool.ruff.lint] +select = ["ALL"] +ignore = [ + "ARG001", # ignored because variables in df.eval() are not detected + "ANN", + "B005", # strip-with-multi-characters + "B007", # variable not used in loop body (but sometimes it is with DataFrame.eval) + "B905", # zip without explicit strict parameter + "C901", # complex-structure + "COM812", # missing-trailing-comma + "CPY001", # missing-copyright-notice + "D100", # undocumented-public-module + "D101", # undocumented-public-class + "D102", # undocumented-public-method + "D103", # undocumented-public-function + "D104", # undocumented-public-package + "D107", # undocumented-public-init + "D203", # one-blank-line-before-class + "D205", # blank-line-after-summary + "D213", # multi-line-summary-second-line + "D417", # undocumented-param + "E501", # Line too long + "E741", # mixed-case-variable-in-global-scope + "ERA001", # commented-out-code + "FBT", + "FIX002", # line contains TODO + "ISC001", # single-line-implicit-string-concatenation + "N802", # Function name should be lowercase + "N803", # Argument name should be lowercase + "N806", # non-lowercase-variable-in-function + "N816", # variable-name-too-short + "N999", # invalid-module-name + "PD901", # df is a bad variable name + "PERF203", # try-except-in-loop + #"PGH001", # No builtin `eval()` allowed + "PLR0911", # too-many-return-statements + "PLR0912", # too-many-branches + "PLR0913", # too-many-arguments + "PLR0915", # too-many-statements + "PLR2004", # magic-value-comparison + "PLW2901", # redefined-loop-name + "PYI024", # Use `typing.NamedTuple` instead of `collections.namedtuple` + "S101", # Use of assert detected + "S301", # suspicious-pickle-usage + #"S307", # suspicious-eval-usage + "S311", # suspicious-non-cryptographic-random-usage + "S603", # subprocess-without-shell-equals-true + "S607", # start-process-with-partial-path + "SLF001", # private-member-access + "T201", # print found + "TD002", # missing-todo-author + "TD003", # missing-todo-link +] +fixable = ["ALL"] +unfixable = [ + "COM812", # missing-trailing-comma + "ERA001", # commented-out-code (will just delete it!) + "F401", # unused-import + "F841", # unused-variable + "SIM222", # expr-or-true + "SIM223", # expr-and-false +] + +[tool.ruff.lint.flake8-import-conventions.extend-aliases] +"artistools" = "at" +"matplotlib" = "mpl" +"matplotlib.pyplot" = "plt" +"matplotlib.typing" = "mplt" +"numpy.typing" = "npt" +"typing" = "t" +"polars" = "pl" +"polars.selectors" = "cs" + +[tool.ruff.lint.flake8-tidy-imports] +ban-relative-imports = "all" + +[tool.ruff.lint.per-file-ignores] +"__init__.py" = ["F401"] + +[tool.ruff.lint.isort] +force-single-line = true +order-by-type = false + +[tool.setuptools] +packages = ["pynonthermal"] +include-package-data = true +license-files = ["LICENSE"] + +#[tool.setuptools.packages.find] +#where = ["."] + +[tool.setuptools.dynamic] +readme = {file = ["README.md"]} +dependencies = {file = ["requirements.txt"]} + +[tool.setuptools_scm] +write_to = "_version.py" +local_scheme = "no-local-version" + +[tool.vulture] +exclude = [".*", "build/", ".eggs/"] +#ignore_names = ["visit_*", "do_*"] +paths = ["pynonthermal"] +sort_by_size = true diff --git a/quickstart.ipynb b/quickstart.ipynb index 68c0646..8b0840c 100644 --- a/quickstart.ipynb +++ b/quickstart.ipynb @@ -29,17 +29,17 @@ "\n", "Setting up Spencer-Fano equation with 2000 energy points from 1.0 to 3000.0 eV...\n", " source is a box function from 2701.45 to 3000.00 eV with E_init 2850.73 [eV/s/cm3]\n", - " including Z=8 ion_stage 1 (O I) ionisation with n_ion 9.9e-01\n", - " including Z=8 ion_stage 1 (O I) excitation with T 6000 K (ntransitions 248, maxnlevelslower 5, maxnlevelsupper 250)\n", - " including Z=8 ion_stage 2 (O II) ionisation with n_ion 1.0e-02\n", - " including Z=8 ion_stage 2 (O II) excitation with T 6000 K (ntransitions 307, maxnlevelslower 5, maxnlevelsupper 250)\n", + " including Z=8 ionstage 1 (O I) ionisation with n_ion 9.9e-01\n", + " including Z=8 ionstage 1 (O I) excitation with T 6000 K (ntransitions 248, maxnlevelslower 5, maxnlevelsupper 250)\n", + " including Z=8 ionstage 2 (O II) ionisation with n_ion 1.0e-02\n", + " including Z=8 ionstage 2 (O II) excitation with T 6000 K (ntransitions 307, maxnlevelslower 5, maxnlevelsupper 250)\n", " n_ion_tot: 1.00e+00 [/cm3] (total ion density)\n", " n_e: 1.00e-02 [/cm3] (free electron density)\n", " x_e: 1.00e-02 [/cm3] (electons per nucleus)\n", "deposition: 3000.00 [eV/s/cm3]\n", " n_e_nt: 2.43e+09 [/cm3]\n", "\n", - "====> Z= 8 ion_stage 1 O I (valence potential 13.6 eV)\n", + "====> Z= 8 ionstage 1 O I (valence potential 13.6 eV)\n", " n_ion: 9.90e-01 [/cm3]\n", " n_ion/n_ion_tot: 0.99000\n", "frac_ionisation_shell(n 2 l 1): 0.3604 (ionpot 13.60 eV)\n", @@ -49,7 +49,7 @@ " eff_ionpot: 30.95 [eV]\n", "ionisation ratecoeff: 9.69e+01 [/s]\n", "\n", - "====> Z= 8 ion_stage 2 O II (valence potential 35.1 eV)\n", + "====> Z= 8 ionstage 2 O II (valence potential 35.1 eV)\n", " n_ion: 1.00e-02 [/cm3]\n", " n_ion/n_ion_tot: 0.01000\n", "frac_ionisation_shell(n 2 l 1): 0.0028 (ionpot 35.10 eV)\n", @@ -71,15 +71,15 @@ "# Kozma & Fransson 1992 Figure 2 - Pure-Oxygen Plasma\n", "x_e = 1e-2\n", "ions = [\n", - " # (Z, ionstage, number_density)\n", + " # (Z, ion_stage, number_density)\n", " (8, 1, 1. - x_e),\n", " (8, 2, x_e),\n", "]\n", "\n", "sf = pynonthermal.SpencerFanoSolver(emin_ev=1, emax_ev=3000, npts=2000, verbose=True)\n", - "for Z, ionstage, n_ion in ions:\n", - " sf.add_ionisation(Z, ionstage, n_ion)\n", - " sf.add_ion_ltepopexcitation(Z, ionstage, n_ion, temperature=6000)\n", + "for Z, ion_stage, n_ion in ions:\n", + " sf.add_ionisation(Z, ion_stage, n_ion)\n", + " sf.add_ion_ltepopexcitation(Z, ion_stage, n_ion, temperature=6000)\n", "\n", "sf.solve(depositionratedensity_ev=3.e3)\n", "\n", diff --git a/requirements.txt b/requirements.txt index ec7907c..bfec7ef 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,5 +10,6 @@ pytest-cov>=2.10.1 pytest-runner>=5.2 PyYAML>=5.3.1 scipy>=1.5.4 +typeguard>=4.1.5 wheel>=0.36 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index b3c880d..0000000 --- a/setup.cfg +++ /dev/null @@ -1,6 +0,0 @@ -[aliases] -test=pytest - -[flake8] -max-line-length=120 -exclude=migrations \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100755 index a2bf176..0000000 --- a/setup.py +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env python3 - -# coding: utf-8 -"""Plotting and analysis tools for the ARTIS 3D supernova radiative transfer code.""" - -import datetime -import sys - -from pathlib import Path -from setuptools import find_packages, setup -from setuptools.command.test import test as TestCommand - - -class PyTest(TestCommand): - """Setup the py.test test runner.""" - - def finalize_options(self): - """Set options for the command line.""" - TestCommand.finalize_options(self) - self.test_args = [] - self.test_suite = True - - def run_tests(self): - """Execute the test runner command.""" - # Import here, because outside the required eggs aren't loaded yet - import pytest - - sys.exit(pytest.main(self.test_args)) - - -print(datetime.datetime.now().isoformat()) -setup( - name="pynonthermal", - version="2021.10.12", - # version=datetime.datetime.now().isoformat(), - author="Luke Shingles", - author_email="luke.shingles@gmail.com", - packages=find_packages(), - url="https://www.github.com/lukeshingles/pynonthermal/", - license="MIT", - description="A non-thermal electron deposition (Spencer-Fano equation) solver.", - long_description=(Path(__file__).absolute().parent / "README.md").open("rt").read(), - long_description_content_type="text/markdown", - install_requires=(Path(__file__).absolute().parent / "requirements.txt") - .open("rt") - .read() - .splitlines(), - python_requires=">==3.8", - # test_suite='tests', - setup_requires=["coveralls", "pytest", "pytest-runner", "pytest-cov"], - tests_require=["coveralls", "pytest", "pytest-runner", "pytest-cov"], - include_package_data=True, -)