├── .circleci ├── config.yml └── early_exit.sh ├── .codecov.yaml ├── .codespellrc ├── .coveragerc ├── .cruft.json ├── .flake8 ├── .github └── workflows │ ├── ci.yml │ ├── label_sync.yml │ └── sub_package_update.yml ├── .gitignore ├── .isort.cfg ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── .rtd-environment.yml ├── .ruff.toml ├── CHANGELOG.rst ├── LICENSE.rst ├── MANIFEST.in ├── README.rst ├── changelog └── README.rst ├── docs ├── Makefile ├── code_ref │ ├── fermi.rst │ ├── goes_xrs.rst │ ├── index.rst │ ├── iris.rst │ ├── lyra.rst │ ├── response.rst │ ├── rhessi.rst │ ├── sunkit_instruments.rst │ └── suvi.rst ├── conf.py ├── index.rst ├── make.bat ├── nitpick-exceptions ├── robots.txt ├── topic_guide │ ├── channel_response.rst │ └── index.rst └── whatsnew │ ├── changelog.rst │ └── index.rst ├── examples ├── calculate_goes_temperature_and_emission_measure.py ├── plot_suvi_thematic_map.py └── readme.txt ├── licenses ├── LICENSE.rst ├── README.rst └── TEMPLATE_LICENSE.rst ├── pyproject.toml ├── pytest.ini ├── setup.py ├── sunkit_instruments ├── __init__.py ├── _dev │ ├── __init__.py │ └── scm_version.py ├── conftest.py ├── data │ ├── README.rst │ ├── __init__.py │ └── test │ │ ├── __init__.py │ │ ├── annotation_lyra.db │ │ ├── annotation_manual.db │ │ ├── annotation_ppt.db │ │ ├── annotation_science.db │ │ ├── go1520110607.fits │ │ ├── goes_15_test_chianti_tem_idl.sav │ │ ├── goes_16_test_chianti_tem_idl.sav │ │ ├── hsi_calib_ev_20020220_1106_20020220_1106_25_40.fits │ │ ├── hsi_image_20101016_191218.fits │ │ ├── hsi_imagecube_clean_20150930_1307_1tx1e.fits │ │ ├── hsi_imagecube_clean_20151214_2255_2tx2e.fits │ │ ├── hsi_obssumm_20110404_042.fits.gz │ │ ├── hsi_obssumm_20120601_018_truncated.fits.gz │ │ ├── hsi_obssumm_filedb_201104.txt │ │ ├── iris_l2_20130801_074720_4040000014_SJI_1400_t000.fits │ │ ├── lyra_20150101-000000_lev3_std_truncated.fits.gz │ │ ├── sci_gxrs-l2-irrad_g15_d20170910_v0-0-0_truncated.nc │ │ └── sci_xrsf-l2-flx1s_g16_d20170910_v2-1-0_truncated.nc ├── fermi │ ├── __init__.py │ ├── fermi.py │ └── tests │ │ ├── __init__.py │ │ └── test_fermi.py ├── goes_xrs │ ├── __init__.py │ ├── goes_chianti_tem.py │ ├── goes_xrs.py │ └── tests │ │ ├── __init__.py │ │ └── test_goes_xrs.py ├── iris │ ├── __init__.py │ ├── iris.py │ └── tests │ │ ├── __init__.py │ │ └── test_iris.py ├── lyra │ ├── __init__.py │ ├── lyra.py │ └── tests │ │ ├── __init__.py │ │ └── test_lyra.py ├── response │ ├── __init__.py │ ├── abstractions.py │ ├── tests │ │ ├── test_channel.py │ │ └── test_source_spectra.py │ └── thermal.py ├── rhessi │ ├── __init__.py │ ├── rhessi.py │ └── tests │ │ ├── __init__.py │ │ └── test_rhessi.py ├── suvi │ ├── __init__.py │ ├── _variables.py │ ├── data │ │ ├── SUVI_FM1_131A_eff_area.txt │ │ ├── SUVI_FM1_171A_eff_area.txt │ │ ├── SUVI_FM1_195A_eff_area.txt │ │ ├── SUVI_FM1_284A_eff_area.txt │ │ ├── SUVI_FM1_304A_eff_area.txt │ │ ├── SUVI_FM1_94A_eff_area.txt │ │ ├── SUVI_FM1_gain.txt │ │ ├── SUVI_FM2_131A_eff_area.txt │ │ ├── SUVI_FM2_171A_eff_area.txt │ │ ├── SUVI_FM2_195A_eff_area.txt │ │ ├── SUVI_FM2_284A_eff_area.txt │ │ ├── SUVI_FM2_304A_eff_area.txt │ │ ├── SUVI_FM2_94A_eff_area.txt │ │ ├── SUVI_FM2_gain.txt │ │ ├── SUVI_FM3_131A_eff_area.txt │ │ ├── SUVI_FM3_171A_eff_area.txt │ │ ├── SUVI_FM3_195A_eff_area.txt │ │ ├── SUVI_FM3_284A_eff_area.txt │ │ ├── SUVI_FM3_304A_eff_area.txt │ │ ├── SUVI_FM3_94A_eff_area.txt │ │ ├── SUVI_FM3_gain.txt │ │ ├── SUVI_FM4_131A_eff_area.txt │ │ ├── SUVI_FM4_171A_eff_area.txt │ │ ├── SUVI_FM4_195A_eff_area.txt │ │ ├── SUVI_FM4_284A_eff_area.txt │ │ ├── SUVI_FM4_304A_eff_area.txt │ │ ├── SUVI_FM4_94A_eff_area.txt │ │ └── SUVI_FM4_gain.txt │ ├── io.py │ ├── suvi.py │ └── tests │ │ ├── __init__.py │ │ ├── conftest.py │ │ ├── test_io.py │ │ └── test_suvi.py ├── tests │ └── __init__.py ├── utils.py └── version.py └── tox.ini /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | no-backports: &no-backports 2 | name: Skip any branches called backport* 3 | command: | 4 | if [[ "${CIRCLE_BRANCH}" = *"backport"* ]]; then 5 | circleci step halt 6 | fi 7 | 8 | skip-check: &skip-check 9 | name: Check for [ci skip] 10 | command: bash .circleci/early_exit.sh 11 | 12 | merge-check: &merge-check 13 | name: Check if we need to merge upstream main 14 | command: | 15 | if [[ -n "${CIRCLE_PR_NUMBER}" ]]; then 16 | git fetch origin --tags 17 | git fetch origin +refs/pull/$CIRCLE_PR_NUMBER/merge:pr/$CIRCLE_PR_NUMBER/merge 18 | git checkout -qf pr/$CIRCLE_PR_NUMBER/merge 19 | fi 20 | 21 | apt-run: &apt-install 22 | name: Install apt packages 23 | command: | 24 | apt update 25 | apt install -y graphviz build-essential 26 | 27 | version: 2 28 | jobs: 29 | twine-check: 30 | docker: 31 | - image: continuumio/miniconda3 32 | steps: 33 | - checkout 34 | - run: *no-backports 35 | - run: *skip-check 36 | - run: *merge-check 37 | - run: pip install -U pep517 38 | - run: python -m pep517.build --source . 39 | - run: python -m pip install -U --user --force-reinstall twine 40 | - run: python -m twine check dist/* 41 | 42 | html-docs: 43 | docker: 44 | - image: continuumio/miniconda3 45 | steps: 46 | - checkout 47 | - run: *no-backports 48 | - run: *skip-check 49 | - run: *merge-check 50 | - run: *apt-install 51 | - run: pip install -U tox 52 | - run: tox -e build_docs 53 | - run: 54 | name: Prepare for upload 55 | command: | 56 | # If it's not a PR, don't upload 57 | if [ -z "${CIRCLE_PULL_REQUEST}" ]; then 58 | rm -r docs/_build/html/* 59 | else 60 | # If it is a PR, delete sources, because it's a lot of files 61 | # which we don't really need to upload 62 | rm -r docs/_build/html/_sources 63 | fi 64 | - store_artifacts: 65 | path: docs/_build/html 66 | 67 | workflows: 68 | version: 2 69 | 70 | twine-check: 71 | jobs: 72 | - twine-check 73 | 74 | test-documentation: 75 | jobs: 76 | - html-docs 77 | 78 | notify: 79 | webhooks: 80 | - url: https://giles.cadair.dev/circleci 81 | -------------------------------------------------------------------------------- /.circleci/early_exit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | commitmessage=$(git log --pretty=%B -n 1) 4 | if [[ $commitmessage = *"[ci skip]"* ]] || [[ $commitmessage = *"[skip ci]"* ]]; then 5 | echo "Skipping build because [ci skip] found in commit message" 6 | circleci step halt 7 | fi 8 | -------------------------------------------------------------------------------- /.codecov.yaml: -------------------------------------------------------------------------------- 1 | comment: off 2 | coverage: 3 | status: 4 | project: 5 | default: 6 | threshold: 0.2% 7 | 8 | codecov: 9 | require_ci_to_pass: false 10 | notify: 11 | wait_for_ci: true 12 | -------------------------------------------------------------------------------- /.codespellrc: -------------------------------------------------------------------------------- 1 | [codespell] 2 | skip = *.asdf,*.fits,*.fts,*.header,*.json,*.xsh,*cache*,*egg*,*extern*,.git,.idea,.tox,_build,*truncated,*.svg,.asv_env,.history 3 | ignore-words-list = 4 | afile, 5 | alog, 6 | datas, 7 | nd, 8 | nin, 9 | observ, 10 | ot, 11 | precess, 12 | precessed, 13 | sav, 14 | te, 15 | upto 16 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | sunkit_instruments/conftest.py 4 | sunkit_instruments/*setup_package* 5 | sunkit_instruments/extern/* 6 | sunkit_instruments/version* 7 | */sunkit_instruments/conftest.py 8 | */sunkit_instruments/*setup_package* 9 | */sunkit_instruments/extern/* 10 | */sunkit_instruments/version* 11 | 12 | [report] 13 | exclude_lines = 14 | # Have to re-enable the standard pragma 15 | pragma: no cover 16 | # Don't complain about packages we have installed 17 | except ImportError 18 | # Don't complain if tests don't hit assertions 19 | raise AssertionError 20 | raise NotImplementedError 21 | # Don't complain about script hooks 22 | def main(.*): 23 | # Ignore branches that don't pertain to this version of Python 24 | pragma: py{ignore_python_version} 25 | # Don't complain about IPython completion helper 26 | def _ipython_key_completions_ 27 | # typing.TYPE_CHECKING is False at runtime 28 | if TYPE_CHECKING: 29 | # Ignore typing overloads 30 | @overload 31 | -------------------------------------------------------------------------------- /.cruft.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "https://github.com/sunpy/package-template", 3 | "commit": "1bdd28c1e2d725d9ae9d9c0b6ad682d75687f45d", 4 | "checkout": null, 5 | "context": { 6 | "cookiecutter": { 7 | "package_name": "sunkit-instruments", 8 | "module_name": "sunkit_instruments", 9 | "short_description": "A SunPy-affiliated package for solar instrum", 10 | "author_name": "The SunPy Community", 11 | "author_email": "sunpy@googlegroups.com", 12 | "project_url": "https://sunpy.org", 13 | "github_repo": "sunpy/sunkit-instruments", 14 | "sourcecode_url": "https://github.com/sunpy/sunkit-instruments", 15 | "download_url": "https://pypi.org/project/sunkit-instruments", 16 | "documentation_url": "https://docs.sunpy.org/projects/sunkit-instruments", 17 | "changelog_url": "https://docs.sunpy.org/projects/sunkit-instruments/en/stable/whatsnew/changelog.html", 18 | "issue_tracker_url": "https://github.com/sunpy/sunkit-instruments/issues", 19 | "license": "BSD 3-Clause", 20 | "minimum_python_version": "3.10", 21 | "use_compiled_extensions": "n", 22 | "enable_dynamic_dev_versions": "y", 23 | "include_example_code": "n", 24 | "include_cruft_update_github_workflow": "y", 25 | "use_extended_ruff_linting": "n", 26 | "_sphinx_theme": "sunpy", 27 | "_parent_project": "", 28 | "_install_requires": "", 29 | "_copy_without_render": [ 30 | "docs/_templates", 31 | "docs/_static", 32 | ".github/workflows/sub_package_update.yml" 33 | ], 34 | "_template": "https://github.com/sunpy/package-template", 35 | "_commit": "1bdd28c1e2d725d9ae9d9c0b6ad682d75687f45d" 36 | } 37 | }, 38 | "directory": null 39 | } 40 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = 3 | # missing-whitespace-around-operator 4 | E225 5 | # missing-whitespace-around-arithmetic-operator 6 | E226 7 | # line-too-long 8 | E501 9 | # unused-import 10 | F401 11 | # undefined-local-with-import-star 12 | F403 13 | # redefined-while-unused 14 | F811 15 | # Line break occurred before a binary operator 16 | W503, 17 | # Line break occurred after a binary operator 18 | W504 19 | max-line-length = 110 20 | exclude = 21 | .git 22 | __pycache__ 23 | docs/conf.py 24 | build 25 | sunkit-instruments/__init__.py 26 | rst-directives = 27 | plot 28 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # Main CI Workflow 2 | name: CI 3 | 4 | on: 5 | push: 6 | branches: 7 | - 'main' 8 | - '*.*' 9 | - '!*backport*' 10 | tags: 11 | - 'v*' 12 | - '!*dev*' 13 | - '!*pre*' 14 | - '!*post*' 15 | pull_request: 16 | # Allow manual runs through the web UI 17 | workflow_dispatch: 18 | schedule: 19 | # ┌───────── minute (0 - 59) 20 | # │ ┌───────── hour (0 - 23) 21 | # │ │ ┌───────── day of the month (1 - 31) 22 | # │ │ │ ┌───────── month (1 - 12 or JAN-DEC) 23 | # │ │ │ │ ┌───────── day of the week (0 - 6 or SUN-SAT) 24 | - cron: '0 7 * * 3' # Every Wed at 07:00 UTC 25 | 26 | concurrency: 27 | group: ${{ github.workflow }}-${{ github.ref }} 28 | cancel-in-progress: true 29 | 30 | jobs: 31 | core: 32 | uses: OpenAstronomy/github-actions-workflows/.github/workflows/tox.yml@v1 33 | with: 34 | submodules: false 35 | coverage: codecov 36 | toxdeps: tox-pypi-filter 37 | posargs: -n auto 38 | envs: | 39 | - linux: py313 40 | secrets: 41 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 42 | 43 | sdist_verify: 44 | runs-on: ubuntu-latest 45 | steps: 46 | - uses: actions/checkout@v4 47 | - uses: actions/setup-python@v5 48 | with: 49 | python-version: '3.13' 50 | - run: python -m pip install -U --user build 51 | - run: python -m build . --sdist 52 | - run: python -m pip install -U --user twine 53 | - run: python -m twine check dist/* 54 | 55 | test: 56 | needs: [core, sdist_verify] 57 | uses: OpenAstronomy/github-actions-workflows/.github/workflows/tox.yml@v1 58 | with: 59 | submodules: false 60 | coverage: codecov 61 | toxdeps: tox-pypi-filter 62 | posargs: -n auto 63 | envs: | 64 | - windows: py311 65 | - macos: py312 66 | - linux: py310-oldestdeps 67 | - linux: py313-devdeps 68 | 69 | secrets: 70 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 71 | 72 | docs: 73 | needs: [core] 74 | uses: OpenAstronomy/github-actions-workflows/.github/workflows/tox.yml@v1 75 | with: 76 | default_python: '3.13' 77 | submodules: false 78 | pytest: false 79 | toxdeps: tox-pypi-filter 80 | libraries: | 81 | apt: 82 | - graphviz 83 | envs: | 84 | - linux: build_docs 85 | 86 | online: 87 | if: "!startsWith(github.event.ref, 'refs/tags/v')" 88 | needs: [test] 89 | uses: OpenAstronomy/github-actions-workflows/.github/workflows/tox.yml@v1 90 | with: 91 | default_python: '3.13' 92 | submodules: false 93 | coverage: codecov 94 | toxdeps: tox-pypi-filter 95 | posargs: -n auto --dist loadgroup 96 | envs: | 97 | - linux: py313-online 98 | secrets: 99 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 100 | 101 | publish: 102 | # Build wheels on PRs only when labelled. Releases will only be published if tagged ^v.* 103 | # see https://github-actions-workflows.openastronomy.org/en/latest/publish.html#upload-to-pypi 104 | if: | 105 | github.event_name != 'pull_request' || 106 | ( 107 | github.event_name == 'pull_request' && 108 | contains(github.event.pull_request.labels.*.name, 'Run publish') 109 | ) 110 | needs: [test, docs] 111 | uses: OpenAstronomy/github-actions-workflows/.github/workflows/publish_pure_python.yml@v1 112 | with: 113 | python-version: '3.13' 114 | test_extras: 'tests' 115 | test_command: 'pytest -p no:warnings --doctest-rst --pyargs sunkit_instruments' 116 | submodules: false 117 | secrets: 118 | pypi_token: ${{ secrets.pypi_token }} 119 | -------------------------------------------------------------------------------- /.github/workflows/label_sync.yml: -------------------------------------------------------------------------------- 1 | name: Label Sync 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | # ┌───────── minute (0 - 59) 6 | # │ ┌───────── hour (0 - 23) 7 | # │ │ ┌───────── day of the month (1 - 31) 8 | # │ │ │ ┌───────── month (1 - 12 or JAN-DEC) 9 | # │ │ │ │ ┌───────── day of the week (0 - 6 or SUN-SAT) 10 | - cron: '0 0 * * *' # run every day at midnight UTC 11 | 12 | # Give permissions to write issue labels 13 | permissions: 14 | issues: write 15 | 16 | jobs: 17 | label_sync: 18 | runs-on: ubuntu-latest 19 | name: Label Sync 20 | steps: 21 | - uses: srealmoreno/label-sync-action@850ba5cef2b25e56c6c420c4feed0319294682fd 22 | with: 23 | config-file: https://raw.githubusercontent.com/sunpy/.github/main/labels.yml 24 | -------------------------------------------------------------------------------- /.github/workflows/sub_package_update.yml: -------------------------------------------------------------------------------- 1 | # This template is taken from the cruft example code, for further information please see: 2 | # https://cruft.github.io/cruft/#automating-updates-with-github-actions 3 | name: Automatic Update from package template 4 | permissions: 5 | contents: write 6 | pull-requests: write 7 | 8 | on: 9 | # Allow manual runs through the web UI 10 | workflow_dispatch: 11 | schedule: 12 | # ┌───────── minute (0 - 59) 13 | # │ ┌───────── hour (0 - 23) 14 | # │ │ ┌───────── day of the month (1 - 31) 15 | # │ │ │ ┌───────── month (1 - 12 or JAN-DEC) 16 | # │ │ │ │ ┌───────── day of the week (0 - 6 or SUN-SAT) 17 | - cron: '0 7 * * 1' # Every Monday at 7am UTC 18 | 19 | jobs: 20 | update: 21 | runs-on: ubuntu-latest 22 | strategy: 23 | fail-fast: true 24 | steps: 25 | - uses: actions/checkout@v4 26 | 27 | - uses: actions/setup-python@v5 28 | with: 29 | python-version: "3.11" 30 | 31 | - name: Install Cruft 32 | run: python -m pip install git+https://github.com/Cadair/cruft@patch-p1 33 | 34 | - name: Check if update is available 35 | continue-on-error: false 36 | id: check 37 | run: | 38 | CHANGES=0 39 | if [ -f .cruft.json ]; then 40 | if ! cruft check; then 41 | CHANGES=1 42 | fi 43 | else 44 | echo "No .cruft.json file" 45 | fi 46 | 47 | echo "has_changes=$CHANGES" >> "$GITHUB_OUTPUT" 48 | 49 | - name: Run update if available 50 | id: cruft_update 51 | if: steps.check.outputs.has_changes == '1' 52 | run: | 53 | git config --global user.email "${{ github.actor }}@users.noreply.github.com" 54 | git config --global user.name "${{ github.actor }}" 55 | 56 | cruft_output=$(cruft update --skip-apply-ask --refresh-private-variables) 57 | echo $cruft_output 58 | git restore --staged . 59 | 60 | if [[ "$cruft_output" == *"Failed to cleanly apply the update, there may be merge conflicts."* ]]; then 61 | echo merge_conflicts=1 >> $GITHUB_OUTPUT 62 | else 63 | echo merge_conflicts=0 >> $GITHUB_OUTPUT 64 | fi 65 | 66 | - name: Check if only .cruft.json is modified 67 | id: cruft_json 68 | if: steps.check.outputs.has_changes == '1' 69 | run: | 70 | git status --porcelain=1 71 | if [[ "$(git status --porcelain=1)" == " M .cruft.json" ]]; then 72 | echo "Only .cruft.json is modified. Exiting workflow early." 73 | echo "has_changes=0" >> "$GITHUB_OUTPUT" 74 | else 75 | echo "has_changes=1" >> "$GITHUB_OUTPUT" 76 | fi 77 | 78 | - name: Create pull request 79 | if: steps.cruft_json.outputs.has_changes == '1' 80 | uses: peter-evans/create-pull-request@v7 81 | with: 82 | token: ${{ secrets.GITHUB_TOKEN }} 83 | add-paths: "." 84 | commit-message: "Automatic package template update" 85 | branch: "cruft/update" 86 | delete-branch: true 87 | draft: ${{ steps.cruft_update.outputs.merge_conflicts == '1' }} 88 | title: "Updates from the package template" 89 | labels: | 90 | No Changelog Entry Needed 91 | body: | 92 | This is an autogenerated PR, which will applies the latest changes from the [SunPy Package Template](https://github.com/sunpy/package-template). 93 | If this pull request has been opened as a draft there are conflicts which need fixing. 94 | 95 | **To run the CI on this pull request you will need to close it and reopen it.** 96 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Python: https://raw.githubusercontent.com/github/gitignore/master/Python.gitignore 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | tmp/ 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | pip-wheel-metadata/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | sunkit_instruments/_version.py 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | junit/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # pyenv 84 | .python-version 85 | 86 | # celery beat schedule file 87 | celerybeat-schedule 88 | 89 | # SageMath parsed files 90 | *.sage.py 91 | 92 | # Environments 93 | .env 94 | .venv 95 | env/ 96 | venv/ 97 | ENV/ 98 | env.bak/ 99 | venv.bak/ 100 | 101 | # Spyder project settings 102 | .spyderproject 103 | .spyproject 104 | 105 | # Rope project settings 106 | .ropeproject 107 | 108 | # mkdocs documentation 109 | /site 110 | 111 | # mypy 112 | .mypy_cache/ 113 | 114 | ### https://raw.github.com/github/gitignore/master/Global/OSX.gitignore 115 | 116 | .DS_Store 117 | .AppleDouble 118 | .LSOverride 119 | 120 | # Icon must ends with two \r. 121 | Icon 122 | 123 | 124 | # Thumbnails 125 | ._* 126 | 127 | # Files that might appear on external disk 128 | .Spotlight-V100 129 | .Trashes 130 | 131 | ### Linux: https://raw.githubusercontent.com/github/gitignore/master/Global/Linux.gitignore 132 | 133 | *~ 134 | 135 | # temporary files which can be created if a process still has a handle open of a deleted file 136 | .fuse_hidden* 137 | 138 | # KDE directory preferences 139 | .directory 140 | 141 | # Linux trash folder which might appear on any partition or disk 142 | .Trash-* 143 | 144 | # .nfs files are created when an open file is removed but is still being accessed 145 | .nfs* 146 | 147 | ### MacOS: https://raw.githubusercontent.com/github/gitignore/master/Global/macOS.gitignore 148 | 149 | # General 150 | .DS_Store 151 | .AppleDouble 152 | .LSOverride 153 | 154 | # Icon must end with two \r 155 | Icon 156 | 157 | 158 | # Thumbnails 159 | ._* 160 | 161 | # Files that might appear in the root of a volume 162 | .DocumentRevisions-V100 163 | .fseventsd 164 | .Spotlight-V100 165 | .TemporaryItems 166 | .Trashes 167 | .VolumeIcon.icns 168 | .com.apple.timemachine.donotpresent 169 | 170 | # Directories potentially created on remote AFP share 171 | .AppleDB 172 | .AppleDesktop 173 | Network Trash Folder 174 | Temporary Items 175 | .apdisk 176 | 177 | ### Windows: https://raw.githubusercontent.com/github/gitignore/master/Global/Windows.gitignore 178 | 179 | # Windows thumbnail cache files 180 | Thumbs.db 181 | ehthumbs.db 182 | ehthumbs_vista.db 183 | 184 | # Dump file 185 | *.stackdump 186 | 187 | # Folder config file 188 | [Dd]esktop.ini 189 | 190 | # Recycle Bin used on file shares 191 | $RECYCLE.BIN/ 192 | 193 | # Windows Installer files 194 | *.cab 195 | *.msi 196 | *.msix 197 | *.msm 198 | *.msp 199 | 200 | # Windows shortcuts 201 | *.lnk 202 | 203 | ### VScode: https://raw.githubusercontent.com/github/gitignore/master/Global/VisualStudioCode.gitignore 204 | .vscode/* 205 | .vs/* 206 | 207 | ### Extra Python Items and SunPy Specific 208 | .hypothesis 209 | .pytest_cache 210 | sunpydata.sqlite 211 | sunpydata.sqlite-journal 212 | sunkit_instruments/_compiler.c 213 | sunkit_instruments/cython_version.py 214 | docs/_build 215 | docs/generated 216 | docs/api/ 217 | docs/whatsnew/latest_changelog.txt 218 | examples/**/*.asdf 219 | examples/**/*.csv 220 | # This is incase you run the figure tests 221 | figure_test_images* 222 | tags 223 | sunkit_instruments/_version.py 224 | docs/sg_execution_times.rst 225 | 226 | ### Pycharm(?) 227 | .idea 228 | 229 | # Release script 230 | .github_cache 231 | 232 | # Misc Stuff 233 | .history 234 | *.orig 235 | .tmp 236 | requirements-min.txt 237 | .ruff_cache 238 | 239 | # Example files 240 | dr_suvi-l2-thmap_g16_s20220101T000000Z_e20220101T000400Z_v1-0-2.fits 241 | -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | balanced_wrapping = true 3 | skip = 4 | docs/conf.py 5 | sunkit_instruments/__init__.py 6 | default_section = THIRDPARTY 7 | include_trailing_comma = true 8 | known_astropy = astropy, asdf 9 | known_sunpy = sunpy 10 | known_first_party = sunkit_instruments 11 | length_sort = false 12 | length_sort_sections = stdlib 13 | line_length = 110 14 | multi_line_output = 3 15 | no_lines_before = LOCALFOLDER 16 | sections = STDLIB, THIRDPARTY, ASTROPY, SUNPY, FIRSTPARTY, LOCALFOLDER 17 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | # This should be before any formatting hooks like isort 3 | - repo: https://github.com/astral-sh/ruff-pre-commit 4 | rev: "v0.11.12" 5 | hooks: 6 | - id: ruff 7 | args: ["--fix"] 8 | exclude: ".*(.fits|.fts|.fit|.nc|.gz|.txt|tca.*|extern.*|.rst|.md|docs/conf.py)$" 9 | - repo: https://github.com/PyCQA/isort 10 | rev: 6.0.1 11 | hooks: 12 | - id: isort 13 | exclude: ".*(.fits|.fts|.fit|.header|.txt|tca.*|extern.*|sunkit_instruments/extern)$" 14 | - repo: https://github.com/pre-commit/pre-commit-hooks 15 | rev: v5.0.0 16 | hooks: 17 | - id: check-ast 18 | - id: check-case-conflict 19 | - id: trailing-whitespace 20 | exclude: ".*(.fits|.fts|.fit|.nc|.header|.txt)$" 21 | - id: check-yaml 22 | - id: debug-statements 23 | - id: check-added-large-files 24 | args: ["--enforce-all", "--maxkb=1054"] 25 | - id: end-of-file-fixer 26 | exclude: ".*(.fits|.fts|.fit|.nc|.gz|.header|.txt|tca.*|.json)$|^CITATION.rst$" 27 | - id: mixed-line-ending 28 | exclude: ".*(.fits|.fts|.fit|.nc|.gz|.header|.txt|tca.*)$" 29 | - repo: https://github.com/codespell-project/codespell 30 | rev: v2.4.1 31 | hooks: 32 | - id: codespell 33 | args: [ "--write-changes" ] 34 | ci: 35 | autofix_prs: false 36 | autoupdate_schedule: "quarterly" 37 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-lts-latest 5 | tools: 6 | python: "mambaforge-latest" 7 | jobs: 8 | post_checkout: 9 | - git fetch --unshallow || true 10 | pre_install: 11 | - git update-index --assume-unchanged .rtd-environment.yml docs/conf.py 12 | 13 | conda: 14 | environment: .rtd-environment.yml 15 | 16 | sphinx: 17 | builder: html 18 | configuration: docs/conf.py 19 | fail_on_warning: false 20 | 21 | formats: 22 | - htmlzip 23 | 24 | python: 25 | install: 26 | - method: pip 27 | extra_requirements: 28 | - all 29 | - docs 30 | path: . 31 | -------------------------------------------------------------------------------- /.rtd-environment.yml: -------------------------------------------------------------------------------- 1 | name: sunkit-instruments 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | - python=3.12 6 | - pip 7 | - graphviz!=2.42.*,!=2.43.* 8 | -------------------------------------------------------------------------------- /.ruff.toml: -------------------------------------------------------------------------------- 1 | target-version = "py310" 2 | line-length = 120 3 | exclude = [ 4 | ".git,", 5 | "__pycache__", 6 | "build", 7 | "sunkit-instruments/version.py", 8 | ] 9 | 10 | [lint] 11 | select = [ 12 | "E", 13 | "F", 14 | "W", 15 | "UP", 16 | "PT", 17 | ] 18 | extend-ignore = [ 19 | # pycodestyle (E, W) 20 | "E501", # ignore line length will use a formatter instead 21 | # pyupgrade (UP) 22 | "UP038", # Use | in isinstance - not compatible with models and is slower 23 | # pytest (PT) 24 | "PT001", # Always use pytest.fixture() 25 | "PT023", # Always use () on pytest decorators 26 | "PT011", # TODO: Fix (no match= on pytest.raises) 27 | # flake8-pie (PIE) 28 | "PIE808", # Disallow passing 0 as the first argument to range 29 | # flake8-use-pathlib (PTH) 30 | "PTH123", # open() should be replaced by Path.open() 31 | # Ruff (RUF) 32 | "RUF003", # Ignore ambiguous quote marks, doesn't allow ' in comments 33 | "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar` 34 | "RUF013", # PEP 484 prohibits implicit `Optional` 35 | "RUF015", # Prefer `next(iter(...))` over single element slice 36 | ] 37 | 38 | [lint.per-file-ignores] 39 | "setup.py" = [ 40 | "INP001", # File is part of an implicit namespace package. 41 | ] 42 | "conftest.py" = [ 43 | "INP001", # File is part of an implicit namespace package. 44 | ] 45 | "docs/conf.py" = [ 46 | "E402" # Module imports not at top of file 47 | ] 48 | "docs/*.py" = [ 49 | "INP001", # File is part of an implicit namespace package. 50 | ] 51 | "examples/**.py" = [ 52 | "T201", # allow use of print in examples 53 | "INP001", # File is part of an implicit namespace package. 54 | ] 55 | "__init__.py" = [ 56 | "E402", # Module level import not at top of cell 57 | "F401", # Unused import 58 | "F403", # from {name} import * used; unable to detect undefined names 59 | "F405", # {name} may be undefined, or defined from star imports 60 | ] 61 | "test_*.py" = [ 62 | "E402", # Module level import not at top of cell 63 | ] 64 | 65 | [lint.pydocstyle] 66 | convention = "numpy" 67 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | 0.6.0 (2025-03-12) 2 | ================== 3 | 4 | Breaking Changes 5 | ---------------- 6 | 7 | - Increased minimum Python version to 3.10 (`#125 `__) 8 | - Increased the minimum version of ``sunpy`` to 6.0.0. (`#126 `__) 9 | - Update the hash of the CHIANTI data file retrieved by the data manager used in `sunkit_instruments.goes_xrs.calculate_temperature_em` 10 | so that the latest version of the file is used. (`#143 `__) 11 | - Updated SUVI Flight Model files for FM1 (16) and FM2 (17). (`#168 `__) 12 | 13 | 14 | New Features 15 | ------------ 16 | 17 | - Added `sunkit_instruments.response.SourceSpectra` to provide a container for 18 | spectra as a function of temperature and wavelength needed for computing temperature 19 | response functions. (`#98 `__) 20 | - Added `sunkit_instruments.response.abstractions.AbstractChannel` to standardize an interface 21 | for computing wavelength and temperature response functions. (`#98 `__) 22 | - Added support for SUVI Flight Models FM3 (18) and FM4 (19). (`#168 `__) 23 | 24 | 25 | Bug Fixes 26 | --------- 27 | 28 | - In the ``fermi``, the function ``get_detector_sun_angles_for_time`` returns the angle with respect to the Sun of each Fermi/GBM detector. 29 | However, these files contain gaps due to the South Atlantic Anomaly. 30 | If the time requested falls in one of these gaps, the code will return the detector angles for the next available time. 31 | This can be several minutes different from the time requested. 32 | Now, a warning to the user will be raised if the time returned by the code is more than 1 minute different from the time requested (1 minute is the nominal cadence of the spacecraft weekly file), and explains that this is likely due to a South Atlantic Anomaly encounter. (`#128 `__) 33 | - The function ``plot_detector_sun_angles`` was broken, due to the formatting of the time axis. (`#130 `__) 34 | - Fixed a bug in `~sunkit_instruments.lyra.remove_lytaf_events_from_timeseries` where units were not being correctly passed 35 | to new timeseries. (`#143 `__) 36 | 37 | 38 | Documentation 39 | ------------- 40 | 41 | - Add a topic guide on a vocabulary for instrument response functions. (`#111 `__) 42 | 43 | 44 | Internal Changes 45 | ---------------- 46 | 47 | - Re-templated the entire library to use the new sunpy template. (`#133 `__) 48 | 49 | 50 | 0.5.0 (2023-11-17) 51 | ================== 52 | 53 | Maintenance release, no new features or bugfixes. 54 | 55 | Breaking Changes 56 | ---------------- 57 | 58 | - Increased minimum version of ``sunpy`` to 5.0.0 59 | - Increased minimum version of Python to 3.9 60 | 61 | 0.4.0 (2023-04-04) 62 | ================== 63 | 64 | Backwards Incompatible Changes 65 | ------------------------------ 66 | 67 | - This removes the older version of `sunkit_instruments.goes_xrs.calculate_temperature_em` that no longer works for the re-processed netcdf files and new GOES-R data. 68 | 69 | This also removes the ``sunkit_instruments.goes_xrs.calculate_radiative_loss_rate`` and ``sunkit_instruments.goes_xrs.calculate_xray_luminosity`` functions that also no longer work in their current form. 70 | 71 | The new `sunkit_instruments.goes_xrs.calculate_temperature_em` function now returns a new sunpy.timeseries.GenericTimeSeries with the temperature and emission measure rather than appending columns to the passed XRSTimeSeries. (`#81 `__) 72 | 73 | 74 | Features 75 | -------- 76 | 77 | - Create new function (`sunkit_instruments.goes_xrs.calculate_temperature_em`) to calculate the temperature and emission measure from the GOES XRS measurements including the new re-processed XRS data and GOES-R data. 78 | 79 | See :ref:`sphx_glr_generated_gallery_calculate_goes_temperature_and_emission_measure.py` for an example. (`#81 `__) 80 | 81 | 82 | 0.3.0 (2022-05-24) 83 | ================== 84 | 85 | Backwards Incompatible Changes 86 | ------------------------------ 87 | 88 | - Dropped Python 3.7 support. (`#53 `__) 89 | - Minimum version of ``sunpy`` is now 4.0 (LTS). (`#61 `__) 90 | 91 | 92 | Features 93 | -------- 94 | 95 | - Added functions for `SUVI `__: 96 | 97 | * :func:`sunkit_instruments.suvi.read_suvi` to read FITS or NetCDF SUVI files. 98 | * :func:`sunkit_instruments.suvi.files_to_map` to read SUVI files and turn them into :class:`sunpy.map.GenericMap`. 99 | * :func:`sunkit_instruments.suvi.despike_l1b_file` and :func:`sunkit_instruments.suvi.despike_l1b_array` to despike SUVI L1b files. 100 | * :func:`sunkit_instruments.suvi.get_response` to get the response function for a given SUVI L1b file or wavelength. (`#61 `__) 101 | 102 | 103 | Bug Fixes 104 | --------- 105 | 106 | - Fermi pointing file names changed from "_p202_v001" to "_p310_v001" upstream. (`#48 `__) 107 | 108 | 109 | 0.2.0 (2021-02-13) 110 | ================== 111 | 112 | Features 113 | -------- 114 | 115 | - Add :func:`sunkit_instruments.rhessi.imagecube2map` function to extract `sunpy.map.MapSequence` objects from a RHESSI 4D image cube. (`#35 `__) 116 | 117 | 118 | 0.1.0 (2020-09-30) 119 | ================== 120 | 121 | Features 122 | -------- 123 | 124 | - Creation of the package with all code from ``sunpy.instr``. 125 | -------------------------------------------------------------------------------- /LICENSE.rst: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020-2025, The SunPy Developers 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, this 10 | list of conditions and the following disclaimer in the documentation and/or 11 | other materials provided with the distribution. 12 | * Neither the name of the SunPy Developers nor the names of its contributors may be 13 | used to endorse or promote products derived from this software without 14 | specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 20 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 23 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # Exclude specific files 2 | # All files which are tracked by git and not explicitly excluded here are included by setuptools_scm 3 | # Prune folders 4 | prune build 5 | prune docs/_build 6 | prune docs/api 7 | global-exclude *.pyc *.o 8 | 9 | # This subpackage is only used in development checkouts 10 | # and should not be included in built tarballs 11 | prune sunkit_instruments/_dev 12 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | A SunPy affiliated package for solar instrument-specific tools 2 | -------------------------------------------------------------- 3 | 4 | .. image:: http://img.shields.io/badge/powered%20by-SunPy-orange.svg?style=flat 5 | :target: http://www.sunpy.org 6 | :alt: Powered by SunPy Badge 7 | 8 | What is sunkit-instruments? 9 | --------------------------- 10 | 11 | sunkit-instruments is a SunPy-affiliated package for solar instrument-specific tools. 12 | Its purpose is not to be a repository for all tools for all instruments. 13 | Instead it is intended to perform three main roles: 14 | 15 | 1. Hold instrument tools that are so few they do not warrant their own package; 16 | 2. Hold tools for instruments with no instrument team or the instrument team does not currently support solar applications; 17 | 3. Act as an incubator for instrument-specific tools that can evolve into a separate instrument package, backed by an instrument team. 18 | 19 | For instrument teams, this package can act as a forum to engage with their user base and learn how to best develop their tools within the SunPy/scientific Python ecosystem. 20 | It also lowers the barrier to publishing Python-based instrument tools by providing packaging and release infrastructure and support. 21 | However should instrument teams want to develop at their own pace or provide a large number of tools, 22 | they should consider starting their own package for full control. 23 | We encourage and support instrument teams in choosing this route and hope they will still engage and collaborate with the SunPy and wider community during their development. 24 | We point to the recent development of `aiapy `__ as a great example of this type of collaboration. 25 | 26 | Usage of Generative AI 27 | ---------------------- 28 | 29 | We expect authentic engagement in our community. 30 | Be wary of posting output from Large Language Models or similar generative AI as comments on GitHub or any other platform, as such comments tend to be formulaic and low quality content. 31 | If you use generative AI tools as an aid in developing code or documentation changes, ensure that you fully understand the proposed changes and can explain why they are the correct approach and an improvement to the current state. 32 | 33 | License 34 | ------- 35 | 36 | This project is Copyright (c) The SunPy Developers and licensed under the terms of the BSD 3-Clause license. 37 | This package is based upon the `Openastronomy packaging guide `_ which is licensed under the BSD 3-clause licence. See the licenses folder for more information. 38 | 39 | Code of Conduct 40 | --------------- 41 | 42 | sunkit-instruments follows the SunPy Project's `Code of Conduct `__ 43 | -------------------------------------------------------------------------------- /changelog/README.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Changelog 3 | ========= 4 | 5 | .. note:: 6 | 7 | This README was adapted from the pytest changelog readme under the terms of the MIT licence. 8 | 9 | This directory contains "news fragments" which are short files that contain a small **ReST**-formatted text that will be added to the next ``CHANGELOG``. 10 | 11 | The ``CHANGELOG`` will be read by users, so this description should be aimed at sunpy users instead of describing internal changes which are only relevant to the developers. 12 | 13 | Make sure to use full sentences with correct case and punctuation, for example:: 14 | 15 | Add support for Helioprojective coordinates in `sunpy.coordinates.frames`. 16 | 17 | Please try to use Sphinx intersphinx using backticks. 18 | 19 | Each file should be named like ``.[.].rst``, where ```` is a pull request number, ``COUNTER`` is an optional number if a PR needs multiple entries with the same type and ```` is one of: 20 | 21 | * ``breaking``: A change which requires users to change code and is not backwards compatible. (Not to be used for removal of deprecated features.) 22 | * ``feature``: New user facing features and any new behavior. 23 | * ``bugfix``: Fixes a reported bug. 24 | * ``doc``: Documentation addition or improvement, like rewording an entire session or adding missing docs. 25 | * ``removal``: Feature deprecation and/or feature removal. 26 | * ``trivial``: A change which has no user facing effect or is tiny change. 27 | 28 | So for example: ``123.feature.rst``, ``456.bugfix.rst``. 29 | 30 | If you are unsure what pull request type to use, don't hesitate to ask in your PR. 31 | 32 | Note that the ``towncrier`` tool will automatically reflow your text, so it will work best if you stick to a single paragraph, but multiple sentences and links are OK and encouraged. 33 | You can install ``towncrier`` and then run ``towncrier --draft`` if you want to get a preview of how your change will look in the final release notes. 34 | -------------------------------------------------------------------------------- /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/code_ref/fermi.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | Fermi 3 | ===== 4 | 5 | .. automodapi:: sunkit_instruments.fermi 6 | -------------------------------------------------------------------------------- /docs/code_ref/goes_xrs.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | GOES XRS 3 | ======== 4 | .. automodapi:: sunkit_instruments.goes_xrs 5 | -------------------------------------------------------------------------------- /docs/code_ref/index.rst: -------------------------------------------------------------------------------- 1 | .. _reference: 2 | 3 | ============= 4 | API Reference 5 | ============= 6 | 7 | .. toctree:: 8 | :maxdepth: 2 9 | 10 | sunkit_instruments 11 | fermi 12 | goes_xrs 13 | iris 14 | lyra 15 | rhessi 16 | suvi 17 | response 18 | -------------------------------------------------------------------------------- /docs/code_ref/iris.rst: -------------------------------------------------------------------------------- 1 | ==== 2 | IRIS 3 | ==== 4 | 5 | .. automodapi:: sunkit_instruments.iris 6 | -------------------------------------------------------------------------------- /docs/code_ref/lyra.rst: -------------------------------------------------------------------------------- 1 | ==== 2 | LYRA 3 | ==== 4 | 5 | .. automodapi:: sunkit_instruments.lyra 6 | -------------------------------------------------------------------------------- /docs/code_ref/response.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Response 3 | ======== 4 | 5 | .. automodapi:: sunkit_instruments.response 6 | 7 | .. automodapi:: sunkit_instruments.response.abstractions 8 | -------------------------------------------------------------------------------- /docs/code_ref/rhessi.rst: -------------------------------------------------------------------------------- 1 | ====== 2 | RHESSI 3 | ====== 4 | 5 | .. automodapi:: sunkit_instruments.rhessi 6 | -------------------------------------------------------------------------------- /docs/code_ref/sunkit_instruments.rst: -------------------------------------------------------------------------------- 1 | .. automodapi:: sunkit_instruments 2 | -------------------------------------------------------------------------------- /docs/code_ref/suvi.rst: -------------------------------------------------------------------------------- 1 | ==== 2 | SUVI 3 | ==== 4 | 5 | Helper routines for the Solar UltraViolet Imager (SUVI) instrument on the GOES-R series of satellites. 6 | 7 | .. note:: 8 | 9 | SUVI data (and data from other GOES instrumentation): 10 | 11 | * `L1b and L2 FITS files `__ 12 | * `L1b netCDF files `__ , a publicly available Amazon S3 bucket, look on page 2 after all the ABI data. 13 | 14 | .. automodapi:: sunkit_instruments.suvi 15 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | """ 2 | Configuration file for the Sphinx documentation builder. 3 | """ 4 | # -- stdlib imports ------------------------------------------------------------ 5 | 6 | import os 7 | import datetime 8 | from packaging.version import Version 9 | 10 | # -- Read the Docs Specific Configuration -------------------------------------- 11 | 12 | on_rtd = os.environ.get("READTHEDOCS", None) == "True" 13 | if on_rtd: 14 | os.environ["SUNPY_CONFIGDIR"] = "/home/docs/" 15 | os.environ["HOME"] = "/home/docs/" 16 | os.environ["LANG"] = "C" 17 | os.environ["LC_ALL"] = "C" 18 | os.environ["PARFIVE_HIDE_PROGRESS"] = "True" 19 | 20 | # -- Project information ----------------------------------------------------- 21 | 22 | # The full version, including alpha/beta/rc tags 23 | from sunkit_instruments import __version__ 24 | 25 | _version = Version(__version__) 26 | version = release = str(_version) 27 | # Avoid "post" appearing in version string in rendered docs 28 | if _version.is_postrelease: 29 | version = release = _version.base_version 30 | # Avoid long githashes in rendered Sphinx docs 31 | elif _version.is_devrelease: 32 | version = release = f"{_version.base_version}.dev{_version.dev}" 33 | is_development = _version.is_devrelease 34 | is_release = not(_version.is_prerelease or _version.is_devrelease) 35 | 36 | project = "sunkit-instruments" 37 | author = "The SunPy Community" 38 | copyright = f"{datetime.datetime.now().year}, {author}" 39 | 40 | # -- General configuration ----------------------------------------------------- 41 | 42 | # Wrap large function/method signatures 43 | maximum_signature_line_length = 80 44 | 45 | # Add any Sphinx extension module names here, as strings. They can be 46 | # extensions coming with Sphinx (named "sphinx.ext.*") or your custom 47 | # ones. 48 | suppress_warnings = [ 49 | "app.add_directive", 50 | ] 51 | extensions = [ 52 | "matplotlib.sphinxext.plot_directive", 53 | "sphinx_automodapi.automodapi", 54 | "sphinx_automodapi.smart_resolver", 55 | "sphinx_changelog", 56 | "sphinx_gallery.gen_gallery", 57 | "sphinx.ext.autodoc", 58 | "sphinx.ext.coverage", 59 | "sphinx.ext.doctest", 60 | "sphinx.ext.inheritance_diagram", 61 | "sphinx.ext.intersphinx", 62 | "sphinx.ext.mathjax", 63 | "sphinx.ext.napoleon", 64 | "sphinx.ext.todo", 65 | "sphinx.ext.viewcode", 66 | ] 67 | 68 | # Add any paths that contain templates here, relative to this directory. 69 | # templates_path = ["_templates"] 70 | 71 | # List of patterns, relative to source directory, that match files and 72 | # directories to ignore when looking for source files. 73 | # This pattern also affects html_static_path and html_extra_path. 74 | html_extra_path = ["robots.txt"] 75 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 76 | 77 | # The suffix(es) of source filenames. 78 | # You can specify multiple suffix as a list of string: 79 | source_suffix = ".rst" 80 | 81 | # The master toctree document. 82 | master_doc = "index" 83 | 84 | # Treat everything in single ` as a Python reference. 85 | default_role = "py:obj" 86 | 87 | # Enable nitpicky mode, which forces links to be non-broken 88 | nitpicky = True 89 | # This is not used. See docs/nitpick-exceptions file for the actual listing. 90 | nitpick_ignore = [] 91 | for line in open('nitpick-exceptions'): 92 | if line.strip() == "" or line.startswith("#"): 93 | continue 94 | dtype, target = line.split(None, 1) 95 | target = target.strip() 96 | nitpick_ignore.append((dtype, target)) 97 | 98 | # -- Options for intersphinx extension ----------------------------------------- 99 | 100 | intersphinx_mapping = { 101 | "python": ( 102 | "https://docs.python.org/3/", 103 | (None, "http://www.astropy.org/astropy-data/intersphinx/python3.inv"), 104 | ), 105 | "numpy": ( 106 | "https://numpy.org/doc/stable/", 107 | (None, "http://www.astropy.org/astropy-data/intersphinx/numpy.inv"), 108 | ), 109 | "scipy": ( 110 | "https://docs.scipy.org/doc/scipy/reference/", 111 | (None, "http://www.astropy.org/astropy-data/intersphinx/scipy.inv"), 112 | ), 113 | "matplotlib": ( 114 | "https://matplotlib.org/stable/", 115 | (None, "http://www.astropy.org/astropy-data/intersphinx/matplotlib.inv"), 116 | ), 117 | "sunpy": ("https://docs.sunpy.org/en/stable/", None), 118 | "astropy": ("https://docs.astropy.org/en/stable/", None), 119 | "sqlalchemy": ("https://docs.sqlalchemy.org/en/latest/", None), 120 | "pandas": ("https://pandas.pydata.org/pandas-docs/stable/", None), 121 | "skimage": ("https://scikit-image.org/docs/stable/", None), 122 | "drms": ("https://docs.sunpy.org/projects/drms/en/stable/", None), 123 | "parfive": ("https://parfive.readthedocs.io/en/stable/", None), 124 | "reproject": ("https://reproject.readthedocs.io/en/stable/", None), 125 | "aiapy": ("https://aiapy.readthedocs.io/en/stable/", None), 126 | } 127 | 128 | # -- Options for HTML output --------------------------------------------------- 129 | 130 | # The theme to use for HTML and HTML Help pages. See the documentation for 131 | # a list of builtin themes. 132 | html_theme = "sunpy" 133 | 134 | # Render inheritance diagrams in SVG 135 | graphviz_output_format = "svg" 136 | 137 | graphviz_dot_args = [ 138 | "-Nfontsize=10", 139 | "-Nfontname=Helvetica Neue, Helvetica, Arial, sans-serif", 140 | "-Efontsize=10", 141 | "-Efontname=Helvetica Neue, Helvetica, Arial, sans-serif", 142 | "-Gfontsize=10", 143 | "-Gfontname=Helvetica Neue, Helvetica, Arial, sans-serif", 144 | ] 145 | 146 | # Add any paths that contain custom static files (such as style sheets) here, 147 | # relative to this directory. They are copied after the builtin static files, 148 | # so a file named "default.css" will overwrite the builtin "default.css". 149 | # html_static_path = ["_static"] 150 | 151 | # By default, when rendering docstrings for classes, sphinx.ext.autodoc will 152 | # make docs with the class-level docstring and the class-method docstrings, 153 | # but not the __init__ docstring, which often contains the parameters to 154 | # class constructors across the scientific Python ecosystem. The option below 155 | # will append the __init__ docstring to the class-level docstring when rendering 156 | # the docs. For more options, see: 157 | # https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#confval-autoclass_content 158 | autoclass_content = "both" 159 | 160 | # -- Sphinx Gallery ------------------------------------------------------------ 161 | 162 | from sunpy_sphinx_theme import PNG_ICON 163 | 164 | sphinx_gallery_conf = { 165 | "backreferences_dir": os.path.join("generated", "modules"), 166 | "filename_pattern": "^((?!skip_).)*$", 167 | "examples_dirs": os.path.join("..", "examples"), 168 | "gallery_dirs": os.path.join("generated", "gallery"), 169 | "matplotlib_animations": True, 170 | "default_thumb_file": PNG_ICON, 171 | "abort_on_example_error": False, 172 | "plot_gallery": "True", 173 | "remove_config_comments": True, 174 | "doc_module": ("sunpy"), 175 | "only_warn_on_example_error": True, 176 | } 177 | 178 | # -- Other options ---------------------------------------------------------- 179 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ******************************** 2 | sunkit-instruments Documentation 3 | ******************************** 4 | 5 | A package for instrument-specific data structures and processing in the SunPy ecosystem. 6 | 7 | Installation 8 | ============ 9 | 10 | For detailed installation instructions, see the `installation guide`_ in the ``sunpy`` docs. 11 | This takes you through the options for getting a virtual environment and installing ``sunpy``. 12 | You will need to replace "sunpy" with "sunkit-instruments". 13 | 14 | Getting Help 15 | ============ 16 | 17 | Stop by our `chat room `__ if you have any questions. 18 | 19 | Contributing 20 | ============ 21 | 22 | Help is always welcome so let us know what you like to work on, or check out the `issues page`_ for the list of known outstanding items. 23 | If you would like to get involved, please read our `contributing guide`_, this talks about ``sunpy`` but the same is for ``sunkit-instruments``. 24 | 25 | If you want help develop ``sunkit-instruments`` you will need to install it from GitHub. 26 | The best way to do this is to create a new python virtual environment. 27 | Once you have that virtual environment, you will want to fork the repo and then run:: 28 | 29 | $ git clone https://github.com//sunkit-instruments.git 30 | $ cd sunkit-instruments 31 | $ pip install -e ".[dev]" 32 | 33 | .. _installation guide: https://docs.sunpy.org/en/stable/tutorial/installation.html 34 | .. _issues page: https://github.com/sunpy/sunkit-instruments/issues 35 | .. _contributing guide: https://docs.sunpy.org/en/latest/dev_guide/contents/newcomers.html 36 | 37 | 38 | Note that the code in this package is **not** maintained by or necessarily contributed to by instrument teams. 39 | Some instruments have individual packages for analysis in Python, including: 40 | 41 | - `aiapy `__ 42 | - `eispac `__ 43 | - `xrtpy `__ 44 | 45 | .. toctree:: 46 | :maxdepth: 1 47 | :hidden: 48 | 49 | code_ref/index 50 | generated/gallery/index 51 | topic_guide/index 52 | whatsnew/index 53 | -------------------------------------------------------------------------------- /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/nitpick-exceptions: -------------------------------------------------------------------------------- 1 | # Prevents sphinx nitpicky mode picking up on optional 2 | # (see https://github.com/sphinx-doc/sphinx/issues/6861) 3 | # Even if it was "fixed", still broken 4 | py:class optional 5 | # See https://github.com/numpy/numpy/issues/10039 6 | py:obj numpy.datetime64 7 | 8 | py:class Unit 9 | -------------------------------------------------------------------------------- /docs/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: /*/latest/ 3 | Allow: /en/latest/ # Fallback for bots that don't understand wildcards 4 | Allow: /*/stable/ 5 | Allow: /en/stable/ # Fallback for bots that don't understand wildcards 6 | Disallow: / 7 | -------------------------------------------------------------------------------- /docs/topic_guide/channel_response.rst: -------------------------------------------------------------------------------- 1 | .. _sunkit-instruments-topic-guide-channel-response: 2 | 3 | ************************************ 4 | A Vocabulary for Instrument Response 5 | ************************************ 6 | 7 | This topic guide provides a vocabulary for response functions for imaging instruments. 8 | The reason to provide a single vocabulary for instrument response calculations is to define a specification for a common interface that can be used by multiple instruments. 9 | This reduces the amount of effort needed to develop analysis software for new instruments and enables cross-instrument comparisons as upstream packages and users can program against a single interface for these response calculations. 10 | An abstract implementation of this vocabulary is provided in this package. 11 | 12 | Temperature Response 13 | -------------------- 14 | 15 | The temperature response describes the instrument sensitivity as a function of temperature. 16 | It is a useful quantity when performing thermal analysis of imaging data, such as a differential emission measure or filter ratio analysis. 17 | The temperature response is defined as, 18 | 19 | .. math:: 20 | 21 | K(T) = \int\mathrm{d}\lambda\,R(\lambda)S(\lambda,T)\quad[\mathrm{DN}\,\mathrm{pixel}^{-1}\,\mathrm{s}^{-1} \,\mathrm{cm}^5] 22 | 23 | It has a physical type of data number (DN) per pixel per unit time per unit emission measure. 24 | Note that the temperature response is a function of *both* the instrument properties as well as the atomic physics of the emitting source. 25 | The temperature response is related to the observed intensity in a given pixel by, 26 | 27 | .. math:: 28 | 29 | I = \int\mathrm{d}T\,K(T)\mathrm{DEM}(T)\quad[\mathrm{DN}\,\mathrm{pixel}^{-1}\,\mathrm{s}^{-1}], 30 | 31 | where, 32 | 33 | .. math:: 34 | 35 | \mathrm{DEM}(T)=n^2\frac{dh}{dT} 36 | 37 | is the line-of-sight *differential* emission measure distribution in a given pixel. 38 | It is typically expressed in units of :math:`\mathrm{cm}^{-5}\,\mathrm{K}^{-1}`. 39 | 40 | Source Spectra 41 | -------------- 42 | 43 | The source spectra describes how a source is emitting as a function of wavelength and temperature. 44 | It is denoted by :math:`S(\lambda, T)`. 45 | The source spectra has a physical type of photon per unit time per unit wavelength per solid angle per unit density. 46 | The units are commonly expressed as 47 | :math:`\mathrm{photon}\,\mathrm{s}^{-1}\,\mathring{\mathrm{A}}^{-1}\,\mathrm{sr}^{-1}\,\mathrm{cm}^3`. 48 | As such, it may also be referred to as the *spectral radiance per unit emission measure*. 49 | The source spectra is specified by the user and can be computed from atomic databases (e.g. CHIANTI). 50 | This quantity is independent of any instrument properties. 51 | 52 | 53 | Wavelength Response 54 | ------------------- 55 | 56 | The wavelength response describes the instrument sensitivity as a function of wavelength and time. 57 | The wavelength response is defined as, 58 | 59 | .. math:: 60 | 61 | R(\lambda,t) = A_{\mathrm{eff}}(\lambda,t)f(\lambda)\frac{pg}{s}\quad[\mathrm{cm}^2\,\mathrm{DN}\,\mathrm{photon}^{-1}\,\mathrm{sr}\,\mathrm{pixel}^{-1}] 62 | 63 | It has a physical type of area DN per photon solid angle per pixel. 64 | 65 | Camera Gain 66 | ----------- 67 | 68 | The camera gain, :math:`g`, describes the conversion between electrons and data number (DN). 69 | This is a property of the detector. 70 | The units of the camera gain are :math:`\mathrm{DN}\,\mathrm{electron}^{-1}`. 71 | 72 | Photon-to-Energy Conversion 73 | --------------------------- 74 | 75 | The photon-to-energy conversion is given by the amount of energy carried by a photon of wavelength :math:`\lambda`, 76 | 77 | .. math:: 78 | 79 | f(\lambda) = \frac{hc}{\lambda}\quad[\mathrm{eV}\,\mathrm{photon}^{-1}] 80 | 81 | where :math:`h` is Planck's constant and :math:`c` is the speed of light. 82 | It has a physical type of energy per photon. 83 | 84 | .. note:: 85 | 86 | Use the `~astropy.units.spectral` unit equivalency to provide a list of appropriate `astropy.units` equivalencies for this conversion. 87 | 88 | Energy-to-Electron Conversion 89 | ----------------------------- 90 | 91 | The energy-to-electron conversion, :math:`s`, describes the conversion between electrons released in the detector and the energy of an incoming photon. 92 | This is commonly referred to as the *electron-hole-pair-creation energy*. 93 | It has a physical type of energy per electron. 94 | For silicon detectors, a value of :math:`s=3.65\,\mathrm{eV}\,\mathrm{electron}^{-1}` is typically used as this is approximately the energy required to free an electron in silicon. 95 | 96 | Pixel Solid Angle 97 | ----------------- 98 | 99 | The pixel area, :math:`p`, is the angular area in the plane of the sky subtended by a single detector pixel. 100 | It has a physical type of solid angle per pixel. 101 | The units of the pixel area are typically expressed as :math:`\mathrm{sr}\,\mathrm{pixel}^{-1}`. 102 | Typically, this quantity can be determined as the product of the spatial plate scale in each direction. 103 | In the FITS standard, these keys are denoted by "CDELTi", with "i" typically taking on values of 1 or 2. 104 | Note that the pixel area is sometimes confusingly referred to as the plate scale. 105 | However, here we explicitly define the plate scale to be the angular *distance* subtended by one side of a pixel. 106 | 107 | Effective Area 108 | -------------- 109 | 110 | The effective area describes the instrument sensitivity as a function of wavelength and time. 111 | It is given by, 112 | 113 | .. math:: 114 | 115 | A_{\mathrm{eff}}(\lambda,t) = A_{\mathrm{geo}}M(\lambda)F(\lambda)Q(\lambda)D(\lambda,t)\quad[\mathrm{cm}^2] 116 | 117 | The effective area has a physical type of area. 118 | :math:`A_\mathrm{eff}(\lambda,t=0)` is defined as the effective area at the start of the mission. 119 | Each component of the effective area is described in detail below. 120 | 121 | Geometrical Area 122 | **************** 123 | 124 | The geometrical collecting area, :math:`A_\mathrm{geo}`, is the cross-sectional area of the telescope aperture. 125 | It has a physical type of area. 126 | The units of the geometrical collecting area are commonly expressed as :math:`\mathrm{cm}^2`. 127 | For example, for a telescope with a circular aperture of diameter :math:`d`, the geometrical collecting area is :math:`A_\mathrm{geo}=\pi d^2/4`. 128 | 129 | Mirror Reflectance 130 | ****************** 131 | 132 | The mirror reflectance, :math:`M(\lambda)`, is a dimensionless quantity describing the efficiency of the mirror(s) in the instrument as a function of wavelength. 133 | If the instrument contains multiple mirrors (e.g. a primary and secondary mirror), this quantity is the product of the reflectance of each mirror. 134 | :math:`M(\lambda)` should always be between 0 and 1. 135 | 136 | Filter Transmittance 137 | ******************** 138 | 139 | The filter transmittance, :math:`F(\lambda)`, is a dimensionless quantity describing the efficiency of the filter(s) as a function of wavelength. 140 | This is typically calculated by computing the transmittance through a given compound of a specified thickness. 141 | In the case of a multilayer coating, the transmittance is the product of the transmittance of each layer of the coating. 142 | Similarly, if an instrument contains multiple filters (e.g. an entrance and focal-plane filter), this quantity is the product of the transmittance of each mirror. 143 | :math:`F(\lambda)` should always be between 0 and 1. 144 | 145 | Effective Quantum Efficiency 146 | **************************** 147 | 148 | The effective quantum efficiency, :math:`Q(\lambda)`, is a dimensionless quantity describing the efficiency of the detector. 149 | :math:`Q(\lambda)` should always be between 0 and 1. 150 | This quantity may also be referred to as the `external quantum efficiency `__. 151 | Note that the *quantum efficiency* is usually defined as the number of electron-hole pairs measured per photon. 152 | 153 | Degradation 154 | *********** 155 | 156 | The degradation, :math:`D(\lambda,t)`, is a dimensionless quantity describing how the effective area degrades as a function of time and also how that degradation varies with wavelength. 157 | The time dimension, :math:`t`, corresponds to the lifetime of the mission. 158 | :math:`D(\lambda,t)` should always be between 0 and 1. 159 | The degradation need not be equal to 1 at :math:`t=0`. 160 | For example, there could be some known degradation due to contamination in the telescope known at the time of launch. 161 | This quantity should include all sources of degradation in the instrument. 162 | For example, if there is a known degradation model for the filter and the CCD, :math:`D(\lambda,t)` will be the product of these two degradation factors. 163 | -------------------------------------------------------------------------------- /docs/topic_guide/index.rst: -------------------------------------------------------------------------------- 1 | .. _sunkit-instruments-topic-guide-index: 2 | 3 | ************ 4 | Topic Guides 5 | ************ 6 | 7 | 8 | These topic guides provide a set of in-depth explanations for various parts of the ``sunkit-instruments`` package. 9 | They are designed to be read in a standalone manner, without running code at the same time. 10 | Although there are code snippets in various parts of each topic guide, these are present to help explanation, and are not structured in a way that they can be run as you are reading a topic guide. 11 | 12 | .. toctree:: 13 | :maxdepth: 1 14 | 15 | channel_response 16 | -------------------------------------------------------------------------------- /docs/whatsnew/changelog.rst: -------------------------------------------------------------------------------- 1 | .. _changelog: 2 | 3 | ************** 4 | Full Changelog 5 | ************** 6 | 7 | .. changelog:: 8 | :towncrier: ../../ 9 | :towncrier-skip-if-empty: 10 | :changelog_file: ../../CHANGELOG.rst 11 | -------------------------------------------------------------------------------- /docs/whatsnew/index.rst: -------------------------------------------------------------------------------- 1 | *************** 2 | Release History 3 | *************** 4 | 5 | .. toctree:: 6 | :maxdepth: 1 7 | 8 | changelog 9 | -------------------------------------------------------------------------------- /examples/calculate_goes_temperature_and_emission_measure.py: -------------------------------------------------------------------------------- 1 | """ 2 | ====================================================================== 3 | Calculate the GOES-XRS temperature and emission measure during a flare 4 | ====================================================================== 5 | 6 | This example shows you how to estimate the GOES-XRS isothermal temperature and emission measure during a solar flare. 7 | This is done using the observed flux ratio of the short (0.5-4 angstrom) to long (1-8 angstrom) 8 | channels, based on the methods described in White et al. (2005). 9 | The functionality available here is the same as the functionality available in SSWIDL. 10 | """ 11 | 12 | import matplotlib.pyplot as plt 13 | 14 | from sunpy import timeseries as ts 15 | from sunpy.data.sample import GOES_XRS_TIMESERIES 16 | 17 | from sunkit_instruments import goes_xrs 18 | 19 | ####################################### 20 | # Let's begin by creating a GOES-XRS timeseries. 21 | # We can use the sample data here to load in an example of a flare. 22 | 23 | goes_ts = ts.TimeSeries(GOES_XRS_TIMESERIES) 24 | goes_ts.plot() 25 | 26 | ############################################################################## 27 | # The estimation is only valid for large fluxes (i.e. during a flare), 28 | # so let's truncate the timeseries over the time of the flare. 29 | 30 | goes_flare = goes_ts.truncate("2011-06-07 06:15", "2011-06-07 09:00") 31 | 32 | ############################################################################## 33 | # Now let's calculate the temperature and emission measure estimates from 34 | # these channel fluxes. We can do this by using the function 35 | # `sunkit_instruments.goes_xrs.calculate_temperature_em` which 36 | # takes a `sunpy.timeseries.sources.XRSTimeSeries` and returns a new timeseries 37 | # which contains both the respective temperature and emission measure values. 38 | 39 | goes_temp_em = goes_xrs.calculate_temperature_em(goes_flare) 40 | 41 | ############################################################################## 42 | # We can see that goes_temp_em is now a timeseries that contains the temperature and emission measure 43 | # by printing out the column names. 44 | 45 | print(goes_temp_em.columns) 46 | 47 | ############################################################################## 48 | # Now let's plot these all together. 49 | 50 | fig, (ax1, ax2, ax3) = plt.subplots(3, sharex=True) 51 | goes_flare.plot(axes=ax1) 52 | goes_temp_em.plot(columns=["temperature"], axes=ax2) 53 | goes_temp_em.plot(columns=["emission_measure"], axes=ax3) 54 | ax3.set_yscale("log") 55 | plt.show() 56 | -------------------------------------------------------------------------------- /examples/plot_suvi_thematic_map.py: -------------------------------------------------------------------------------- 1 | """ 2 | ==================================== 3 | Plotting a Level 2 SUVI Thematic Map 4 | ==================================== 5 | 6 | This example shows how to read a SUVI L2 Thematic Map FITS file and plot it. 7 | 8 | SUVI L2 Thematic Maps are recognized by pattern in the filename, i.e. they contain "-l2-thmap". 9 | 10 | .. note:: 11 | `GOES-16 L2 Thematic Maps are available here. `__ 12 | """ 13 | 14 | import matplotlib.pyplot as plt 15 | from matplotlib.colors import ListedColormap 16 | from matplotlib.patches import Patch 17 | from parfive import Downloader 18 | 19 | from astropy.io import fits 20 | 21 | from sunkit_instruments.suvi._variables import SOLAR_CLASS_NAME, SOLAR_COLORS 22 | 23 | ############################################################################### 24 | # We start with getting the data. This is done by downloading the data from ``data.ngdc.noaa.gov``. 25 | # 26 | # In this case, we will use requests as to keep this example self contained 27 | # but using your browser will also work. 28 | # 29 | # Using the url: 30 | # https://data.ngdc.noaa.gov/platforms/solar-space-observing-satellites/goes/goes16/l2/data/suvi-l2-thmap/2022/01/01/dr_suvi-l2-thmap_g16_s20220101T000000Z_e20220101T000400Z_v1-0-2.fits 31 | 32 | url = [ 33 | "https://data.ngdc.noaa.gov/platforms/solar-space-observing-satellites/goes/goes16/l2/data/suvi-l2-thmap/2022/01/01/dr_suvi-l2-thmap_g16_s20220101T000000Z_e20220101T000400Z_v1-0-2.fits" 34 | ] 35 | (file,) = Downloader.simple_download(url) 36 | 37 | ################################################################################ 38 | # First let's read a SUVI L2 Thematic Map FITS file. 39 | 40 | with fits.open(file) as hdu: 41 | thmap_data = hdu[0].data 42 | time_stamp = hdu[0].header["DATE-OBS"][0:19] 43 | 44 | ################################################################################ 45 | # Now we will plot it. 46 | 47 | # Here we have some logic to get the correct color map for the SUVI L2 Thematic Map. 48 | colortable = [ 49 | SOLAR_COLORS[SOLAR_CLASS_NAME[i]] if i in SOLAR_CLASS_NAME else "black" 50 | for i in range(max(list(SOLAR_CLASS_NAME.keys())) + 1) 51 | ] 52 | cmap = ListedColormap(colortable) 53 | 54 | # Now the plotting code. 55 | fig, ax = plt.subplots(constrained_layout=True) 56 | ax.imshow( 57 | thmap_data, 58 | origin="lower", 59 | cmap=cmap, 60 | vmin=-1, 61 | vmax=len(colortable), 62 | interpolation="none", 63 | ) 64 | ax.set_axis_off() 65 | ax.text(0, 158, time_stamp, fontsize=14, color="white") 66 | legend_elements = [ 67 | Patch(facecolor=color, edgecolor="black", label=label.replace("_", " ")) 68 | for label, color in SOLAR_COLORS.items() 69 | ] 70 | ax.legend( 71 | handles=legend_elements, 72 | loc="upper right", 73 | ncol=3, 74 | fancybox=True, 75 | shadow=False, 76 | ) 77 | 78 | plt.show() 79 | -------------------------------------------------------------------------------- /examples/readme.txt: -------------------------------------------------------------------------------- 1 | *************** 2 | Example Gallery 3 | *************** 4 | 5 | This gallery contains examples of how to use `sunkit_instruments`. 6 | -------------------------------------------------------------------------------- /licenses/LICENSE.rst: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024, The SunPy Community 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, this 10 | list of conditions and the following disclaimer in the documentation and/or 11 | other materials provided with the distribution. 12 | * Neither the name of the sunkit-instruments team nor the names of its contributors may be 13 | used to endorse or promote products derived from this software without 14 | specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 20 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 23 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /licenses/README.rst: -------------------------------------------------------------------------------- 1 | Licenses 2 | ======== 3 | 4 | This directory holds license and credit information for the package, 5 | works the package is derived from, and/or datasets. 6 | 7 | Ensure that you pick a package licence which is in this folder and it matches 8 | the one mentioned in the top level README.rst file. If you are using the 9 | pre-rendered version of this template check for the word 'Other' in the README. 10 | -------------------------------------------------------------------------------- /licenses/TEMPLATE_LICENSE.rst: -------------------------------------------------------------------------------- 1 | This project is based upon the OpenAstronomy package template 2 | (https://github.com/OpenAstronomy/package-template/) which is licensed under the terms 3 | of the following licence. 4 | 5 | --- 6 | 7 | Copyright (c) 2018, OpenAstronomy Developers 8 | All rights reserved. 9 | 10 | Redistribution and use in source and binary forms, with or without modification, 11 | are permitted provided that the following conditions are met: 12 | 13 | * Redistributions of source code must retain the above copyright notice, this 14 | list of conditions and the following disclaimer. 15 | * Redistributions in binary form must reproduce the above copyright notice, this 16 | list of conditions and the following disclaimer in the documentation and/or 17 | other materials provided with the distribution. 18 | * Neither the name of the Astropy Team nor the names of its contributors may be 19 | used to endorse or promote products derived from this software without 20 | specific prior written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 23 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 24 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 26 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 27 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 28 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 29 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 31 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=62.1", 4 | "setuptools_scm[toml]>=8.0.0", 5 | "wheel", 6 | ] 7 | build-backend = "setuptools.build_meta" 8 | 9 | [project] 10 | name = "sunkit_instruments" 11 | description = "A SunPy-affiliated package for solar instruments" 12 | requires-python = ">=3.10" 13 | readme = { file = "README.rst", content-type = "text/x-rst" } 14 | license = { file = "licenses/LICENSE.rst" } 15 | authors = [ 16 | { name = "The SunPy Community", email = "sunpy@googlegroups.com" }, 17 | ] 18 | dependencies = [ 19 | "sunpy[map,net,timeseries,visualization]>=6.0.0", 20 | "xarray>=2023.12.0", 21 | "astropy>=5.3.0", 22 | "packaging>=23.0", 23 | # !=1.10.0 due to https://github.com/scipy/scipy/issues/17718 24 | "scipy>=1.9.0,!=1.10.0", 25 | "numpy>=1.23.5", 26 | "h5py>=3.7.0", 27 | "matplotlib>=3.6.0", 28 | "pandas>=1.4.0", 29 | ] 30 | dynamic = ["version"] 31 | 32 | [project.optional-dependencies] 33 | tests = [ 34 | "pytest", 35 | "pytest-astropy", 36 | "pytest-doctestplus", 37 | "pytest-cov", 38 | "pytest-xdist", 39 | ] 40 | docs = [ 41 | "sphinx", 42 | "sphinx-automodapi", 43 | "sphinx-changelog", 44 | "sunpy-sphinx-theme", 45 | "packaging", 46 | "sphinx-gallery", 47 | ] 48 | 49 | [project.urls] 50 | Homepage = "https://sunpy.org" 51 | "Source Code" = "https://github.com/sunpy/sunkit-instruments" 52 | Download = "https://pypi.org/project/sunkit-instruments" 53 | Documentation = "https://docs.sunpy.org/projects/sunkit-instruments" 54 | Changelog = "https://docs.sunpy.org/projects/sunkit-instruments/en/stable/whatsnew/changelog.html" 55 | "Issue Tracker" = "https://github.com/sunpy/sunkit-instruments/issues" 56 | 57 | [tool.setuptools] 58 | zip-safe = false 59 | include-package-data = true 60 | 61 | [tool.setuptools.packages.find] 62 | include = ["sunkit_instruments*"] 63 | exclude = ["sunkit_instruments._dev*"] 64 | 65 | [tool.setuptools_scm] 66 | version_file = "sunkit_instruments/_version.py" 67 | 68 | [tool.gilesbot] 69 | [tool.gilesbot.circleci_artifacts] 70 | enabled = true 71 | 72 | [tool.gilesbot.pull_requests] 73 | enabled = true 74 | 75 | [tool.gilesbot.towncrier_changelog] 76 | enabled = true 77 | verify_pr_number = true 78 | changelog_skip_label = "No Changelog Entry Needed" 79 | help_url = "https://github.com/sunpy/sunkit-instruments/blob/main/changelog/README.rst" 80 | 81 | changelog_missing_long = "There isn't a changelog file in this pull request. Please add a changelog file to the `changelog/` directory following the instructions in the changelog [README](https://github.com/sunpy/sunkit-instruments/blob/main/changelog/README.rst)." 82 | 83 | type_incorrect_long = "The changelog file you added is not one of the allowed types. Please use one of the types described in the changelog [README](https://github.com/sunpy/sunkit-instruments/blob/main/changelog/README.rst)" 84 | 85 | number_incorrect_long = "The number in the changelog file you added does not match the number of this pull request. Please rename the file." 86 | 87 | # TODO: This should be in towncrier.toml but Giles currently only works looks in 88 | # pyproject.toml we should move this back when it's fixed. 89 | [tool.towncrier] 90 | package = "sunkit_instruments" 91 | filename = "CHANGELOG.rst" 92 | directory = "changelog/" 93 | issue_format = "`#{issue} `__" 94 | title_format = "{version} ({project_date})" 95 | 96 | [[tool.towncrier.type]] 97 | directory = "breaking" 98 | name = "Breaking Changes" 99 | showcontent = true 100 | 101 | [[tool.towncrier.type]] 102 | directory = "deprecation" 103 | name = "Deprecations" 104 | showcontent = true 105 | 106 | [[tool.towncrier.type]] 107 | directory = "removal" 108 | name = "Removals" 109 | showcontent = true 110 | 111 | [[tool.towncrier.type]] 112 | directory = "feature" 113 | name = "New Features" 114 | showcontent = true 115 | 116 | [[tool.towncrier.type]] 117 | directory = "bugfix" 118 | name = "Bug Fixes" 119 | showcontent = true 120 | 121 | [[tool.towncrier.type]] 122 | directory = "doc" 123 | name = "Documentation" 124 | showcontent = true 125 | 126 | [[tool.towncrier.type]] 127 | directory = "trivial" 128 | name = "Internal Changes" 129 | showcontent = true 130 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | minversion = 7.0 3 | testpaths = 4 | sunkit_instruments 5 | docs 6 | norecursedirs = 7 | .tox 8 | build 9 | docs/_build 10 | docs/generated 11 | *.egg-info 12 | examples 13 | sunkit_instruments/_dev 14 | .history 15 | sunkit_instruments/extern 16 | doctest_plus = enabled 17 | doctest_optionflags = 18 | NORMALIZE_WHITESPACE 19 | FLOAT_CMP 20 | ELLIPSIS 21 | text_file_format = rst 22 | addopts = 23 | --doctest-rst 24 | -p no:unraisableexception 25 | -p no:threadexception 26 | filterwarnings = 27 | # Turn all warnings into errors so they do not pass silently. 28 | error 29 | # Do not fail on pytest config issues (i.e. missing plugins) but do show them 30 | always::pytest.PytestConfigWarning 31 | # A list of warnings to ignore follows. If you add to this list, you MUST 32 | # add a comment or ideally a link to an issue that explains why the warning 33 | # is being ignored 34 | # Do not need to worry about numpy warnings raised by xarray internally 35 | ignore:numpy.core.multiarray is deprecated:DeprecationWarning 36 | # Zeep relies on deprecated cgi in Python 3.11 37 | # Needs a release of zeep 4.2.2 or higher 38 | # https://github.com/mvantellingen/python-zeep/pull/1364 39 | ignore:'cgi' is deprecated and slated for removal in Python 3.13:DeprecationWarning 40 | # Ignore RuntimeWarning about numpy.ndarray size changed, caused by binary incompatibility 41 | ignore:numpy.ndarray size changed:RuntimeWarning 42 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import setup 3 | 4 | setup() 5 | -------------------------------------------------------------------------------- /sunkit_instruments/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | sunkit-instruments 3 | ================== 4 | 5 | A SunPy Affiliated Package for solar instrument-specific tools. 6 | 7 | * Homepage: https://sunpy.org 8 | * Documentation: https://docs.sunpy.org/projects/sunkit-instruments/en/latest/ 9 | * Source code: https://github.com/sunpy/sunkit-instruments 10 | """ 11 | from .version import version as __version__ 12 | 13 | __all__ = ["__version__"] 14 | -------------------------------------------------------------------------------- /sunkit_instruments/_dev/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This package contains utilities that are only used when developing in a 3 | copy of the source repository. 4 | These files are not installed, and should not be assumed to exist at 5 | runtime. 6 | """ 7 | -------------------------------------------------------------------------------- /sunkit_instruments/_dev/scm_version.py: -------------------------------------------------------------------------------- 1 | # Try to use setuptools_scm to get the current version; this is only used 2 | # in development installations from the git repository. 3 | from pathlib import Path 4 | 5 | try: 6 | from setuptools_scm import get_version 7 | 8 | version = get_version(root=Path('../..'), relative_to=__file__) 9 | except ImportError: 10 | raise 11 | except Exception as e: 12 | raise ValueError('setuptools_scm can not determine version.') from e 13 | -------------------------------------------------------------------------------- /sunkit_instruments/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture() 5 | def sunpy_cache(mocker, tmp_path): 6 | """ 7 | Provide a way to add local files to the cache. 8 | 9 | This can be useful when mocking remote requests. 10 | """ 11 | from types import MethodType 12 | 13 | from sunpy.data.data_manager.cache import Cache 14 | from sunpy.data.data_manager.downloader import ParfiveDownloader 15 | from sunpy.data.data_manager.storage import InMemStorage 16 | 17 | cache = Cache(ParfiveDownloader(), InMemStorage(), tmp_path, None) 18 | 19 | def add(self, url, path): 20 | self._storage.store( 21 | { 22 | "url": url, 23 | "file_path": path, 24 | "file_hash": "none", # hash doesn't matter 25 | } 26 | ) 27 | 28 | cache.add = MethodType(add, cache) 29 | 30 | def func(mocked): 31 | mocker.patch(mocked, cache) 32 | return cache 33 | 34 | return func 35 | -------------------------------------------------------------------------------- /sunkit_instruments/data/README.rst: -------------------------------------------------------------------------------- 1 | Data directory 2 | ============== 3 | 4 | This directory contains data files included with the package source 5 | code distribution. Note that this is intended only for relatively small files 6 | - large files should be externally hosted and downloaded as needed. 7 | -------------------------------------------------------------------------------- /sunkit_instruments/data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunpy/sunkit-instruments/590dd43244c63673d2f0f9e93022698d0791e179/sunkit_instruments/data/__init__.py -------------------------------------------------------------------------------- /sunkit_instruments/data/test/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This package contains all of sunkit-instruments's test data. 3 | """ 4 | 5 | import os 6 | import re 7 | import glob 8 | import fnmatch 9 | 10 | from astropy.utils.data import get_pkg_data_filename 11 | 12 | import sunkit_instruments 13 | 14 | __all__ = ["rootdir", "file_list", "get_test_filepath", "test_data_filenames"] 15 | 16 | rootdir = os.path.join(os.path.dirname(sunkit_instruments.__file__), "data", "test") 17 | file_list = glob.glob(os.path.join(rootdir, "*.[!p]*")) 18 | 19 | 20 | def get_test_filepath(filename, **kwargs): 21 | """ 22 | Return the full path to a test file in the ``data/test`` directory. 23 | 24 | Parameters 25 | ---------- 26 | filename : `str` 27 | The name of the file inside the ``data/test`` directory. 28 | 29 | Return 30 | ------ 31 | filepath : `str` 32 | The full path to the file. 33 | 34 | Notes 35 | ----- 36 | This is a wrapper around `astropy.utils.data.get_pkg_data_filename` which 37 | sets the ``package`` kwarg to be 'sunkit_instruments.data.test`. 38 | """ 39 | return get_pkg_data_filename( 40 | filename, package="sunkit_instruments.data.test", **kwargs 41 | ) 42 | 43 | 44 | def test_data_filenames(): 45 | """ 46 | Return a list of all test files in ``data/test`` directory. 47 | 48 | This ignores any ``py``, ``pyc`` and ``__*__`` files in these directories. 49 | 50 | Return 51 | ------ 52 | `list` 53 | The name of all test files in ``data/test`` directory. 54 | """ 55 | test_data_filenames_list = [] 56 | excludes = ["*.pyc", "*" + os.path.sep + "__*__", "*.py"] 57 | excludes = r"|".join([fnmatch.translate(x) for x in excludes]) or r"$." 58 | 59 | for root, dirs, files in os.walk(rootdir): 60 | files = [os.path.join(root, f) for f in files] 61 | files = [f for f in files if not re.match(excludes, f)] 62 | files = [file.replace(rootdir + os.path.sep, "") for file in files] 63 | test_data_filenames_list.extend(files) 64 | 65 | return test_data_filenames_list 66 | -------------------------------------------------------------------------------- /sunkit_instruments/data/test/annotation_lyra.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunpy/sunkit-instruments/590dd43244c63673d2f0f9e93022698d0791e179/sunkit_instruments/data/test/annotation_lyra.db -------------------------------------------------------------------------------- /sunkit_instruments/data/test/annotation_manual.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunpy/sunkit-instruments/590dd43244c63673d2f0f9e93022698d0791e179/sunkit_instruments/data/test/annotation_manual.db -------------------------------------------------------------------------------- /sunkit_instruments/data/test/annotation_ppt.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunpy/sunkit-instruments/590dd43244c63673d2f0f9e93022698d0791e179/sunkit_instruments/data/test/annotation_ppt.db -------------------------------------------------------------------------------- /sunkit_instruments/data/test/annotation_science.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunpy/sunkit-instruments/590dd43244c63673d2f0f9e93022698d0791e179/sunkit_instruments/data/test/annotation_science.db -------------------------------------------------------------------------------- /sunkit_instruments/data/test/go1520110607.fits: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunpy/sunkit-instruments/590dd43244c63673d2f0f9e93022698d0791e179/sunkit_instruments/data/test/go1520110607.fits -------------------------------------------------------------------------------- /sunkit_instruments/data/test/goes_15_test_chianti_tem_idl.sav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunpy/sunkit-instruments/590dd43244c63673d2f0f9e93022698d0791e179/sunkit_instruments/data/test/goes_15_test_chianti_tem_idl.sav -------------------------------------------------------------------------------- /sunkit_instruments/data/test/goes_16_test_chianti_tem_idl.sav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunpy/sunkit-instruments/590dd43244c63673d2f0f9e93022698d0791e179/sunkit_instruments/data/test/goes_16_test_chianti_tem_idl.sav -------------------------------------------------------------------------------- /sunkit_instruments/data/test/hsi_calib_ev_20020220_1106_20020220_1106_25_40.fits: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunpy/sunkit-instruments/590dd43244c63673d2f0f9e93022698d0791e179/sunkit_instruments/data/test/hsi_calib_ev_20020220_1106_20020220_1106_25_40.fits -------------------------------------------------------------------------------- /sunkit_instruments/data/test/hsi_image_20101016_191218.fits: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunpy/sunkit-instruments/590dd43244c63673d2f0f9e93022698d0791e179/sunkit_instruments/data/test/hsi_image_20101016_191218.fits -------------------------------------------------------------------------------- /sunkit_instruments/data/test/hsi_imagecube_clean_20150930_1307_1tx1e.fits: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunpy/sunkit-instruments/590dd43244c63673d2f0f9e93022698d0791e179/sunkit_instruments/data/test/hsi_imagecube_clean_20150930_1307_1tx1e.fits -------------------------------------------------------------------------------- /sunkit_instruments/data/test/hsi_imagecube_clean_20151214_2255_2tx2e.fits: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunpy/sunkit-instruments/590dd43244c63673d2f0f9e93022698d0791e179/sunkit_instruments/data/test/hsi_imagecube_clean_20151214_2255_2tx2e.fits -------------------------------------------------------------------------------- /sunkit_instruments/data/test/hsi_obssumm_20110404_042.fits.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunpy/sunkit-instruments/590dd43244c63673d2f0f9e93022698d0791e179/sunkit_instruments/data/test/hsi_obssumm_20110404_042.fits.gz -------------------------------------------------------------------------------- /sunkit_instruments/data/test/hsi_obssumm_20120601_018_truncated.fits.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunpy/sunkit-instruments/590dd43244c63673d2f0f9e93022698d0791e179/sunkit_instruments/data/test/hsi_obssumm_20120601_018_truncated.fits.gz -------------------------------------------------------------------------------- /sunkit_instruments/data/test/hsi_obssumm_filedb_201104.txt: -------------------------------------------------------------------------------- 1 | HESSI Filedb File: 2 | Created: 2014-04-19T08:15:48.000 3 | Number of Files: 30 4 | Filename Orb_st Orb_end Start_time End_time Status_flag Npackets Drift_start Drift_end Data source 5 | hsi_obssumm_20110401_043.fit 0 0 01-Apr-11 00:00:00 02-Apr-11 00:00:00 0 0 0.000 0.000 6 | hsi_obssumm_20110402_048.fit 0 0 02-Apr-11 00:00:00 03-Apr-11 00:00:00 0 0 0.000 0.000 7 | hsi_obssumm_20110403_048.fit 0 0 03-Apr-11 00:00:00 04-Apr-11 00:00:00 0 0 0.000 0.000 8 | hsi_obssumm_20110404_042.fit 0 0 04-Apr-11 00:00:00 05-Apr-11 00:00:00 0 0 0.000 0.000 9 | hsi_obssumm_20110405_031.fit 0 0 05-Apr-11 00:00:00 06-Apr-11 00:00:00 0 0 0.000 0.000 10 | hsi_obssumm_20110406_041.fit 0 0 06-Apr-11 00:00:00 07-Apr-11 00:00:00 0 0 0.000 0.000 11 | hsi_obssumm_20110407_031.fit 0 0 07-Apr-11 00:00:00 08-Apr-11 00:00:00 0 0 0.000 0.000 12 | hsi_obssumm_20110408_038.fit 0 0 08-Apr-11 00:00:00 09-Apr-11 00:00:00 0 0 0.000 0.000 13 | hsi_obssumm_20110409_030.fit 0 0 09-Apr-11 00:00:00 10-Apr-11 00:00:00 0 0 0.000 0.000 14 | hsi_obssumm_20110410_028.fit 0 0 10-Apr-11 00:00:00 11-Apr-11 00:00:00 0 0 0.000 0.000 15 | hsi_obssumm_20110411_024.fit 0 0 11-Apr-11 00:00:00 12-Apr-11 00:00:00 0 0 0.000 0.000 16 | hsi_obssumm_20110412_024.fit 0 0 12-Apr-11 00:00:00 13-Apr-11 00:00:00 0 0 0.000 0.000 17 | hsi_obssumm_20110413_022.fit 0 0 13-Apr-11 00:00:00 14-Apr-11 00:00:00 0 0 0.000 0.000 18 | hsi_obssumm_20110414_017.fit 0 0 14-Apr-11 00:00:00 15-Apr-11 00:00:00 0 0 0.000 0.000 19 | hsi_obssumm_20110415_020.fit 0 0 15-Apr-11 00:00:00 16-Apr-11 00:00:00 0 0 0.000 0.000 20 | hsi_obssumm_20110416_016.fit 0 0 16-Apr-11 00:00:00 17-Apr-11 00:00:00 0 0 0.000 0.000 21 | hsi_obssumm_20110417_017.fit 0 0 17-Apr-11 00:00:00 18-Apr-11 00:00:00 0 0 0.000 0.000 22 | hsi_obssumm_20110418_017.fit 0 0 18-Apr-11 00:00:00 19-Apr-11 00:00:00 0 0 0.000 0.000 23 | hsi_obssumm_20110419_024.fit 0 0 19-Apr-11 00:00:00 20-Apr-11 00:00:00 0 0 0.000 0.000 24 | hsi_obssumm_20110420_029.fit 0 0 20-Apr-11 00:00:00 21-Apr-11 00:00:00 0 0 0.000 0.000 25 | hsi_obssumm_20110421_025.fit 0 0 21-Apr-11 00:00:00 22-Apr-11 00:00:00 0 0 0.000 0.000 26 | hsi_obssumm_20110422_029.fit 0 0 22-Apr-11 00:00:00 23-Apr-11 00:00:00 0 0 0.000 0.000 27 | hsi_obssumm_20110423_030.fit 0 0 23-Apr-11 00:00:00 24-Apr-11 00:00:00 0 0 0.000 0.000 28 | hsi_obssumm_20110424_025.fit 0 0 24-Apr-11 00:00:00 25-Apr-11 00:00:00 0 0 0.000 0.000 29 | hsi_obssumm_20110425_026.fit 0 0 25-Apr-11 00:00:00 26-Apr-11 00:00:00 0 0 0.000 0.000 30 | hsi_obssumm_20110426_022.fit 0 0 26-Apr-11 00:00:00 27-Apr-11 00:00:00 0 0 0.000 0.000 31 | hsi_obssumm_20110427_029.fit 0 0 27-Apr-11 00:00:00 28-Apr-11 00:00:00 0 0 0.000 0.000 32 | hsi_obssumm_20110428_021.fit 0 0 28-Apr-11 00:00:00 29-Apr-11 00:00:00 0 0 0.000 0.000 33 | hsi_obssumm_20110429_028.fit 0 0 29-Apr-11 00:00:00 30-Apr-11 00:00:00 0 0 0.000 0.000 34 | hsi_obssumm_20110430_029.fit 0 0 30-Apr-11 00:00:00 01-May-11 00:00:00 0 0 0.000 0.000 35 | -------------------------------------------------------------------------------- /sunkit_instruments/data/test/iris_l2_20130801_074720_4040000014_SJI_1400_t000.fits: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunpy/sunkit-instruments/590dd43244c63673d2f0f9e93022698d0791e179/sunkit_instruments/data/test/iris_l2_20130801_074720_4040000014_SJI_1400_t000.fits -------------------------------------------------------------------------------- /sunkit_instruments/data/test/lyra_20150101-000000_lev3_std_truncated.fits.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunpy/sunkit-instruments/590dd43244c63673d2f0f9e93022698d0791e179/sunkit_instruments/data/test/lyra_20150101-000000_lev3_std_truncated.fits.gz -------------------------------------------------------------------------------- /sunkit_instruments/data/test/sci_gxrs-l2-irrad_g15_d20170910_v0-0-0_truncated.nc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunpy/sunkit-instruments/590dd43244c63673d2f0f9e93022698d0791e179/sunkit_instruments/data/test/sci_gxrs-l2-irrad_g15_d20170910_v0-0-0_truncated.nc -------------------------------------------------------------------------------- /sunkit_instruments/data/test/sci_xrsf-l2-flx1s_g16_d20170910_v2-1-0_truncated.nc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunpy/sunkit-instruments/590dd43244c63673d2f0f9e93022698d0791e179/sunkit_instruments/data/test/sci_xrsf-l2-flx1s_g16_d20170910_v2-1-0_truncated.nc -------------------------------------------------------------------------------- /sunkit_instruments/fermi/__init__.py: -------------------------------------------------------------------------------- 1 | from .fermi import * # NOQA 2 | -------------------------------------------------------------------------------- /sunkit_instruments/fermi/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunpy/sunkit-instruments/590dd43244c63673d2f0f9e93022698d0791e179/sunkit_instruments/fermi/tests/__init__.py -------------------------------------------------------------------------------- /sunkit_instruments/fermi/tests/test_fermi.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from numpy.testing import assert_almost_equal 3 | 4 | import astropy.units as u 5 | 6 | from sunpy.time import parse_time 7 | 8 | from sunkit_instruments import fermi 9 | 10 | 11 | @pytest.mark.remote_data 12 | def test_download_weekly_pointing_file(): 13 | # set a test date 14 | date = parse_time("2011-10-01") 15 | afile = fermi.download_weekly_pointing_file(date) 16 | assert isinstance(afile, str) 17 | assert afile.endswith(".fits") 18 | 19 | 20 | @pytest.fixture 21 | def pointing_file(): 22 | # set a test date 23 | date = parse_time("2012-02-15") 24 | file = fermi.download_weekly_pointing_file(date) 25 | return date, file 26 | 27 | 28 | @pytest.mark.remote_data 29 | def test_detector_angles(pointing_file): 30 | det = fermi.get_detector_sun_angles_for_date(pointing_file[0], pointing_file[1]) 31 | assert len(det) == 13 32 | assert_almost_equal(det["n0"][0].value, 21.79, decimal=1) 33 | assert_almost_equal(det["n1"][0].value, 30.45, decimal=1) 34 | assert_almost_equal(det["n2"][0].value, 74.44, decimal=1) 35 | assert_almost_equal(det["n3"][0].value, 30.58, decimal=1) 36 | assert_almost_equal(det["n4"][0].value, 73.93, decimal=1) 37 | assert_almost_equal(det["n5"][0].value, 58.78, decimal=1) 38 | assert_almost_equal(det["n6"][0].value, 47.56, decimal=1) 39 | assert_almost_equal(det["n7"][0].value, 70.89, decimal=1) 40 | assert_almost_equal(det["n8"][0].value, 106.54, decimal=1) 41 | assert_almost_equal(det["n9"][0].value, 70.17, decimal=1) 42 | assert_almost_equal(det["n10"][0].value, 106.95, decimal=1) 43 | assert_almost_equal(det["n11"][0].value, 121.32, decimal=1) 44 | 45 | 46 | @pytest.mark.remote_data 47 | def test_detector_angles_2(pointing_file): 48 | det2 = fermi.get_detector_sun_angles_for_time( 49 | parse_time("2012-02-15 02:00"), pointing_file[1] 50 | ) 51 | assert len(det2) == 13 52 | assert isinstance(det2, dict) 53 | assert_almost_equal(det2["n0"].value, 83.54, decimal=1) 54 | assert_almost_equal(det2["n1"].value, 66.50, decimal=1) 55 | assert_almost_equal(det2["n10"].value, 123.39, decimal=1) 56 | assert_almost_equal(det2["n11"].value, 170.89, decimal=1) 57 | assert_almost_equal(det2["n2"].value, 58.84, decimal=1) 58 | assert_almost_equal(det2["n3"].value, 66.44, decimal=1) 59 | assert_almost_equal(det2["n4"].value, 57.06, decimal=1) 60 | assert_almost_equal(det2["n5"].value, 8.85, decimal=1) 61 | assert_almost_equal(det2["n6"].value, 111.95, decimal=1) 62 | assert_almost_equal(det2["n7"].value, 127.12, decimal=1) 63 | assert_almost_equal(det2["n8"].value, 122.93, decimal=1) 64 | assert_almost_equal(det2["n9"].value, 126.82, decimal=1) 65 | 66 | 67 | def test_met_to_utc(): 68 | time = fermi.met_to_utc(500000000) 69 | assert (time - parse_time("2016-11-05T00:53:16.000")) < 1e-7 * u.s 70 | -------------------------------------------------------------------------------- /sunkit_instruments/goes_xrs/__init__.py: -------------------------------------------------------------------------------- 1 | from .goes_chianti_tem import * # NOQA 2 | from .goes_xrs import * # NOQA 3 | -------------------------------------------------------------------------------- /sunkit_instruments/goes_xrs/goes_chianti_tem.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | from scipy import interpolate 4 | 5 | from astropy import units as u 6 | from astropy.io import fits 7 | from astropy.time import Time 8 | 9 | from sunpy import timeseries as ts 10 | from sunpy.data import manager 11 | from sunpy.time import parse_time 12 | from sunpy.util.exceptions import warn_user 13 | 14 | __all__ = ["calculate_temperature_em"] 15 | 16 | MAX_SUPPORTED_SATELLITE = 17 17 | 18 | 19 | def calculate_temperature_em(goes_ts, abundance="coronal"): 20 | """ 21 | This function calculates the isothermal temperature and 22 | corresponding volume emission measure of the solar soft X-ray 23 | emitting plasma observed by GOES/XRS. 24 | 25 | These are calculated based on methods described in White et al. 2005 [1]_ (see notes) 26 | for which the GOES fluxes and channel ratios are used together with look-up tables 27 | of CHIANTI atomic models to estimate isothermal temperature and emission measure. 28 | Technically speaking, the method interpolates on the channel flux ratio using 29 | pre-calcuated tables for the fluxes at a series of temperatures for fixed emission 30 | measure. 31 | 32 | The method here is almost an exact replica of what is available in SSWIDL, 33 | namely, goes_chianti_tem.pro, and it has been tested against that for consistency. 34 | 35 | It now also works for the GOES-16 and -17 data, and for the re-processed netcdf 36 | GOES/XRS files for GOES 13-15. 37 | 38 | Also note that this has only been tested on the high resolutions 1s/2s/3s data of GOES/XRS. 39 | 40 | Parameters 41 | ---------- 42 | goes_ts : `~sunpy.timeseries.sources.XRSTimeSeries` 43 | The GOES/XRS timeseries containing the data of both the xrsa and xrsb channels (in units of W/m**2). 44 | abundance: str, optional 45 | Which abundances to use for the calculation, the default is "coronal". 46 | Can be either "coronal" or "photospheric". 47 | 48 | Returns 49 | ------- 50 | `~sunpy.timeseries.GenericTimeSeries` 51 | Contains the temperature and emission measure calculated from the input ``goes_ts`` time series. 52 | 53 | Example 54 | ------- 55 | >>> from sunpy import timeseries as ts 56 | >>> import sunpy.data.sample # doctest: +REMOTE_DATA 57 | >>> from sunkit_instruments import goes_xrs 58 | >>> goes_ts = ts.TimeSeries(sunpy.data.sample.GOES_XRS_TIMESERIES) # doctest: +REMOTE_DATA 59 | >>> goes_flare = goes_ts.truncate("2011-06-07 06:20", "2011-06-07 07:30") # doctest: +REMOTE_DATA 60 | >>> goes_temp_emiss = goes_xrs.calculate_temperature_em(goes_flare) # doctest: +REMOTE_DATA 61 | 62 | Notes 63 | ----- 64 | This function works with both the NOAA-provided netcdf files, for which the data is given in "true" fluxes 65 | and with the older FITS files provided by the SDAC, for which the data are scaled to be consistent with GOES-7. 66 | The routine determines from the `sunpy.timeseries.sources.XRSTimeSeries` metadata 67 | whether the SWPC scaling factors need to be removed (which are present in the FITS data). 68 | 69 | See also: https://hesperia.gsfc.nasa.gov/goes/goes.html#Temperature/Emission%20Measure 70 | 71 | In regards to the re-processed GOES 8-15 data, please refer to the documentation here: 72 | 73 | * https://www.ncei.noaa.gov/data/goes-space-environment-monitor/access/science/xrs/GOES_1-15_XRS_Science-Quality_Data_Readme.pdf 74 | This includes information of the SWPC scaling factors that were applied to prior data (e.g. the FITS data). 75 | 76 | For the GOES-R data please refer to here: 77 | 78 | * https://data.ngdc.noaa.gov/platforms/solar-space-observing-satellites/goes/goes16/l2/docs/GOES-R_XRS_L2_Data_Readme.pdf 79 | 80 | References 81 | ---------- 82 | .. [1] White, S. M., Thomas, R. J., & Schwartz, R. A. 2005, 83 | Sol. Phys., 227, 231, DOI: 10.1007/s11207-005-2445-z 84 | """ 85 | if not isinstance(goes_ts, ts.XRSTimeSeries): 86 | raise TypeError( 87 | f"Input time series must be a XRSTimeSeries instance, not {type(goes_ts)}" 88 | ) 89 | 90 | if goes_ts.observatory is None: 91 | raise ValueError( 92 | "The GOES satellite number was not found in the input time series" 93 | ) 94 | 95 | satellite_number = int(goes_ts.observatory.split("-")[-1]) 96 | if (satellite_number < 1) or (satellite_number > MAX_SUPPORTED_SATELLITE): 97 | raise ValueError( 98 | f"GOES satellite number has to be between 1 and 17, {satellite_number} was found." 99 | ) 100 | 101 | allowed_abundances = ["photospheric", "coronal"] 102 | if abundance not in allowed_abundances: 103 | raise ValueError( 104 | f"The abundance can only be `coronal` or `photospheric`, not {abundance}." 105 | ) 106 | # Check if GOES-R and whether the primary detector values are given 107 | if satellite_number >= 16: 108 | if "xrsa_primary_chan" in goes_ts.columns: 109 | output = _manage_goesr_detectors( 110 | goes_ts, satellite_number, abundance=abundance 111 | ) 112 | else: 113 | warn_user( 114 | "No information about primary/secondary detectors in XRSTimeSeries, assuming primary for all" 115 | ) 116 | output = _chianti_temp_emiss(goes_ts, satellite_number, abundance=abundance) 117 | 118 | # Check if the older files are passed, and if so then the scaling factor needs to be removed. 119 | # The newer netcdf files now return "true" fluxes so this SWPC factor doesn't need to be removed. 120 | else: 121 | if ("Origin" in goes_ts.meta.metas[0]) and ( 122 | goes_ts.meta.metas[0].get("Origin") == "SDAC/GSFC" 123 | ): 124 | remove_scaling = True 125 | else: 126 | remove_scaling = False 127 | output = _chianti_temp_emiss( 128 | goes_ts, 129 | satellite_number, 130 | abundance=abundance, 131 | remove_scaling=remove_scaling, 132 | ) 133 | 134 | return output 135 | 136 | 137 | @manager.require( 138 | "goes_chianti_response_table", 139 | [ 140 | "https://sohoftp.nascom.nasa.gov/solarsoft/gen/idl/synoptic/goes/goes_chianti_response_latest.fits" 141 | ], 142 | "cb00c05850e3dc3bbd856eb07c1a372758d689d0845ee591d6e2531afeab0382", 143 | ) 144 | def _chianti_temp_emiss( 145 | goes_ts, satellite_number, secondary=0, abundance="coronal", remove_scaling=False 146 | ): 147 | """ 148 | Calculate isothermal temperature and emission measure from GOES XRS observations. 149 | 150 | This uses the latest formulated response tables including the responses for GOES1-17 to interpolate for temperature or emission measure 151 | from the measured true fluxes, which is read in from the FITS files goes_chianti_response_latest.fits. 152 | 153 | From the ratio of the two channels the temperature is computed from a spline fit from a lookup response table for 101 temperatures and 154 | then the emission measure is derived from the temperature and the long flux. 155 | 156 | Parameters 157 | ---------- 158 | goes_ts : `~sunpy.timeseries.sources.XRSTimeSeries` 159 | The GOES XRS timeseries containing the data of both the xrsa and xrsb channels (in units of W/m**2). 160 | sat : `int` 161 | GOES satellite number. 162 | secondary: `int`, optional 163 | Values 0, 1, 2, 3 indicate A1+B1, A2+B1, A1+B2, A2+B2 detector combos for GOES-R. 164 | Defaults to 0. 165 | abundance: str, optional 166 | Which abundances to use for the calculation, the default is "coronal". 167 | Can be either "coronal" or "photospheric". 168 | remove_scaling: `bool`, optional 169 | Checks whether to remove the SWPC scaling factors. 170 | This is only an issue for the older FITS files for GOES 8-15 XRS. 171 | Default is `False` as the netcdf files have the "true" fluxes. 172 | 173 | Returns 174 | ------- 175 | `~sunpy.timeseries.GenericTimeSeries` 176 | Contains the temperature and emission measure calculated from the input ``goes_ts`` time series. 177 | The two columns are: 178 | ``temperature`` : The temperature in MK. 179 | ``emission `measure` : The volume emission measure. 180 | Notes 181 | ----- 182 | Requires goes_chianti_resp.fits produced by goes_chianti_response.pro 183 | This file contains the pregenerated responses for default coronal and photospheric ion abundances using Chianti version 9.0.1. 184 | url = "https://hesperia.gsfc.nasa.gov/ssw/gen/idl/synoptic/goes/goes_chianti_response_latest.fits" 185 | 186 | The response table within the fits file starts counting the satellite number at 0. 187 | For example, for GOES 15 - then 14 is passed to the response table. This is dealt within the code, 188 | the satellite number to be passed to this function should be the actual GOES satellite number. 189 | """ 190 | 191 | longflux = goes_ts.quantity("xrsb").to(u.W / u.m**2) 192 | shortflux = goes_ts.quantity("xrsa").to(u.W / u.m**2) 193 | 194 | if "xrsb_quality" in goes_ts.columns: 195 | longflux[goes_ts.to_dataframe()["xrsb_quality"] != 0] = np.nan 196 | shortflux[goes_ts.to_dataframe()["xrsa_quality"] != 0] = np.nan 197 | 198 | obsdate = parse_time(goes_ts._data.index[0]) 199 | 200 | longflux_corrected = longflux 201 | # For some reason that I can't find documented anywhere other than in the IDL code, 202 | # the long channel needs to be scaled by this value for GOES-6 before 1983-06-28. 203 | if obsdate <= Time("1983-06-28") and satellite_number == 6: 204 | longflux_corrected = longflux * (4.43 / 5.32) 205 | 206 | shortflux_corrected = shortflux 207 | # Remove the SWPC scaling factors if needed. 208 | # The SPWC scaling factors of 0.7 and 0.85 for the XRSA and XSRB channels 209 | # respectively are documented in the NOAA readme file linked in the docstring. 210 | if remove_scaling and satellite_number >= 8 and satellite_number < 16: 211 | longflux_corrected = longflux_corrected / 0.7 212 | shortflux_corrected = shortflux / 0.85 213 | 214 | # Measurements of short channel flux of less than 1e-10 W/m**2 or 215 | # long channel flux less than 3e-8 W/m**2 are not considered good. 216 | # Ratio values corresponding to such fluxes are set to 0.003. 217 | index = np.logical_or( 218 | shortflux_corrected < u.Quantity(1e-10 * u.W / u.m**2), 219 | longflux_corrected < u.Quantity(3e-8 * u.W / u.m**2), 220 | ) 221 | fluxratio = shortflux_corrected / longflux_corrected 222 | fluxratio.value[index] = u.Quantity(0.003, unit=u.dimensionless_unscaled) 223 | 224 | # Work out detector index to use from the table response based on satellite number 225 | # The counting in the table starts at 0, and indexed in an odd way for the GOES-R 226 | # primary/secondary detectors. 227 | if satellite_number <= 15: 228 | sat = satellite_number - 1 # counting starts at 0 229 | else: 230 | sat = ( 231 | 15 + 4 * (satellite_number - 16) + secondary 232 | ) # to figure out which detector response table to use (see notes) 233 | 234 | resp_file_name = manager.get("goes_chianti_response_table") 235 | response_table = fits.getdata(resp_file_name, extension=1) 236 | rcor = response_table.FSHORT_COR / response_table.FLONG_COR # coronal 237 | rpho = response_table.FSHORT_PHO / response_table.FLONG_PHO # photospheric 238 | 239 | table_to_response_em = 10.0 ** ( 240 | 49.0 - response_table["ALOG10EM"][sat] 241 | ) # for some reason in units of 1e49 (which was to stop overflow errors since 10^49 was 242 | # too big to express as a standard float in IDL.) 243 | 244 | modeltemp = response_table["TEMP_MK"][sat] 245 | modelratio = rcor[sat] if abundance == "coronal" else rpho[sat] 246 | 247 | # Calculate the temperature and emission measure: 248 | 249 | # get spline fit to model data to get temperatures given the input flux ratio. 250 | spline = interpolate.splrep(modelratio, modeltemp, s=0) 251 | temp = interpolate.splev(fluxratio, spline, der=0) 252 | 253 | modelflux = ( 254 | response_table["FLONG_COR"][sat] 255 | if abundance == "coronal" 256 | else response_table["FLONG_PHO"][sat] 257 | ) 258 | 259 | modeltemp = response_table["TEMP_MK"][sat] 260 | 261 | spline = interpolate.splrep(modeltemp, modelflux * table_to_response_em, s=0) 262 | denom = interpolate.splev(temp, spline, der=0) 263 | 264 | emission_measure = longflux_corrected.value / denom 265 | 266 | goes_times = goes_ts._data.index 267 | df = pd.DataFrame( 268 | {"temperature": temp, "emission_measure": emission_measure * 1e49}, 269 | index=goes_times, 270 | ) 271 | 272 | units = {"temperature": u.MK, "emission_measure": u.cm ** (-3)} 273 | 274 | header = {"Info": "Estimated temperature and emission measure"} 275 | 276 | # return a new timeseries with temperature and emission measure 277 | temp_em = ts.TimeSeries(df, header, units) 278 | 279 | return temp_em 280 | 281 | 282 | def _manage_goesr_detectors(goes_ts, satellite_number, abundance="coronal"): 283 | """ 284 | This manages which response to use for the GOES primary and secondary detectors used in the 285 | observations for the GOES-R satellites (i.e. GOES 16 and 17). 286 | 287 | The GOES 16- and 17- have two detectors for each channel to extend the dynamic range of observations, 288 | namely XRS-A1, XRS-A2, XRS-B1, XRS-B2, for xrsa and xrsb, respectively. 289 | During large flares, both the primary and secondary detectors may be used and given that each have a different 290 | response, we need to match the data at individual times with the correct response. 291 | 292 | Note that the primary channel conditions are values of 0,1,2,3 to indicate A1+B1, A2+B1, A1+B2, A2+B2 detector combos for GOES-R. 293 | Here, we use the `xrsa{b}_primary_chan` columns to figure out which detectors are used for each timestep. 294 | """ 295 | 296 | # These are the conditions for which detector combinations to use. 297 | secondary_det_conditions = {0: [1, 1], 1: [2, 1], 2: [1, 2], 3: [2, 2]} 298 | 299 | # here we split the timeseries into sections for which primary or secondary detectors are used 300 | # and then calculate the response for each and then concatenate them back together. 301 | outputs = [] 302 | for k in secondary_det_conditions: 303 | dets = secondary_det_conditions[k] 304 | (second_ind,) = np.where( 305 | (goes_ts.quantity("xrsa_primary_chan") == dets[0]) 306 | & (goes_ts.quantity("xrsb_primary_chan") == dets[1]) 307 | ) 308 | 309 | goes_split = ts.TimeSeries(goes_ts._data.iloc[second_ind], goes_ts.units) 310 | 311 | if len(goes_split._data) > 0: 312 | output = _chianti_temp_emiss( 313 | goes_split, satellite_number, abundance=abundance, secondary=int(k) 314 | ) 315 | outputs.append(output) 316 | 317 | if len(outputs) > 1: 318 | full_output = outputs[0].concatenate(outputs[1:]) 319 | else: 320 | full_output = outputs[0] 321 | 322 | return full_output 323 | -------------------------------------------------------------------------------- /sunkit_instruments/goes_xrs/goes_xrs.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from astropy import units as u 4 | 5 | from sunpy.time import parse_time 6 | 7 | GOES_CONVERSION_DICT = { 8 | "X": u.Quantity(1e-4, "W/m^2"), 9 | "M": u.Quantity(1e-5, "W/m^2"), 10 | "C": u.Quantity(1e-6, "W/m^2"), 11 | "B": u.Quantity(1e-7, "W/m^2"), 12 | "A": u.Quantity(1e-8, "W/m^2"), 13 | } 14 | 15 | __all__ = [ 16 | "get_goes_event_list", 17 | "flux_to_flareclass", 18 | "flareclass_to_flux", 19 | ] 20 | 21 | 22 | def get_goes_event_list(timerange, goes_class_filter=None): 23 | """ 24 | Retrieve list of flares detected by GOES within a given time range. 25 | 26 | Parameters 27 | ---------- 28 | timerange : `sunpy.time.TimeRange` 29 | The time range to download the event list for. 30 | goes_class_filter: `str`, optional 31 | A string specifying a minimum GOES class for inclusion in the list, 32 | e.g., "M1", "X2". 33 | 34 | Returns 35 | ------- 36 | `list`: 37 | A list of all the flares found for the given time range. 38 | """ 39 | # Importing hek here to avoid calling code that relies on optional dependencies. 40 | from sunpy.net import attrs, hek 41 | 42 | # use HEK module to search for GOES events 43 | client = hek.HEKClient() 44 | event_type = "FL" 45 | tstart = timerange.start 46 | tend = timerange.end 47 | 48 | # query the HEK for a list of events detected by the GOES instrument 49 | # between tstart and tend (using a GOES-class filter) 50 | if goes_class_filter: 51 | result = client.search( 52 | attrs.Time(tstart, tend), 53 | attrs.hek.EventType(event_type), 54 | attrs.hek.FL.GOESCls > goes_class_filter, 55 | attrs.hek.OBS.Observatory == "GOES", 56 | ) 57 | else: 58 | result = client.search( 59 | attrs.Time(tstart, tend), 60 | attrs.hek.EventType(event_type), 61 | attrs.hek.OBS.Observatory == "GOES", 62 | ) 63 | 64 | # want to condense the results of the query into a more manageable 65 | # dictionary 66 | # keep event data, start time, peak time, end time, GOES-class, 67 | # location, active region source (as per GOES list standard) 68 | # make this into a list of dictionaries 69 | goes_event_list = [] 70 | 71 | for r in result: 72 | goes_event = { 73 | "event_date": parse_time(r["event_starttime"]).strftime("%Y-%m-%d"), 74 | "start_time": parse_time(r["event_starttime"]), 75 | "peak_time": parse_time(r["event_peaktime"]), 76 | "end_time": parse_time(r["event_endtime"]), 77 | "goes_class": str(r["fl_goescls"]), 78 | "goes_location": (r["event_coord1"], r["event_coord2"]), 79 | "noaa_active_region": r["ar_noaanum"], 80 | } 81 | goes_event_list.append(goes_event) 82 | 83 | return goes_event_list 84 | 85 | 86 | def flareclass_to_flux(flareclass): 87 | """ 88 | Converts a GOES flare class into the corresponding X-ray flux. 89 | 90 | Parameters 91 | ---------- 92 | flareclass : str 93 | The case-insensitive flare class (e.g., 'X3.2', 'm1.5', 'A9.6'). 94 | 95 | Returns 96 | ------- 97 | flux : `~astropy.units.Quantity` 98 | X-ray flux between 1 and 8 Angstroms as measured near Earth in W/m^2. 99 | 100 | Raises 101 | ------ 102 | TypeError 103 | Input must be a string. 104 | 105 | Examples 106 | -------- 107 | >>> from sunkit_instruments.goes_xrs import flareclass_to_flux 108 | >>> flareclass_to_flux('A1.0') 109 | 110 | >>> flareclass_to_flux('c4.7') 111 | 112 | >>> flareclass_to_flux('X2.4') 113 | 114 | """ 115 | if not isinstance(flareclass, str): 116 | raise TypeError(f"Input must be a string, not {type(flareclass)}") 117 | # TODO should probably make sure the string is in the expected format. 118 | flareclass = flareclass.upper() 119 | # invert the conversion dictionary 120 | # conversion_dict = {v: k for k, v in GOES_CONVERSION_DICT.items()} 121 | return float(flareclass[1:]) * GOES_CONVERSION_DICT[flareclass[0]] 122 | 123 | 124 | @u.quantity_input 125 | def flux_to_flareclass(goesflux: u.watt / u.m**2): 126 | """ 127 | Converts X-ray flux into the corresponding GOES flare class. 128 | 129 | Parameters 130 | ---------- 131 | flux : `~astropy.units.Quantity` 132 | X-ray flux between 1 and 8 Angstroms (usually measured by GOES) as 133 | measured at the Earth in W/m^2 134 | 135 | Returns 136 | ------- 137 | flareclass : str 138 | The flare class e.g.: 'X3.2', 'M1.5', 'A9.6'. 139 | 140 | Raises 141 | ------ 142 | ValueError 143 | Flux cannot be negative. 144 | 145 | References 146 | ---------- 147 | `Solar Flare Classification `__ 148 | 149 | Examples 150 | -------- 151 | >>> from sunkit_instruments.goes_xrs import flux_to_flareclass 152 | >>> import astropy.units as u 153 | >>> flux_to_flareclass(1e-08 * u.watt/u.m**2) 154 | 'A1' 155 | >>> flux_to_flareclass(4.7e-06 * u.watt/u.m**2) 156 | 'C4.7' 157 | >>> flux_to_flareclass(0.00024 * u.watt/u.m**2) 158 | 'X2.4' 159 | >>> flux_to_flareclass(7.8e-09 * u.watt/u.m**2) 160 | 'A0.78' 161 | >>> flux_to_flareclass(0.00682 * u.watt/u.m**2) 162 | 'X68.2' 163 | """ 164 | 165 | if goesflux.value < 0: 166 | raise ValueError("Flux cannot be negative") 167 | 168 | decade = np.floor(np.log10(goesflux.to("W/m**2").value)) 169 | # invert the conversion dictionary 170 | conversion_dict = {v: k for k, v in GOES_CONVERSION_DICT.items()} 171 | if decade < -8: 172 | str_class = "A" 173 | decade = -8 174 | elif decade > -4: 175 | str_class = "X" 176 | decade = -4 177 | else: 178 | str_class = conversion_dict.get(u.Quantity(10**decade, "W/m**2")) 179 | goes_subclass = 10**-decade * goesflux.to("W/m**2").value 180 | return f"{str_class}{goes_subclass:.3g}" 181 | -------------------------------------------------------------------------------- /sunkit_instruments/goes_xrs/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunpy/sunkit-instruments/590dd43244c63673d2f0f9e93022698d0791e179/sunkit_instruments/goes_xrs/tests/__init__.py -------------------------------------------------------------------------------- /sunkit_instruments/goes_xrs/tests/test_goes_xrs.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | from numpy.testing import assert_almost_equal, assert_array_equal 4 | from scipy.io import readsav 5 | 6 | import astropy.units as u 7 | from astropy.time import Time 8 | from astropy.units.quantity import Quantity 9 | 10 | from sunpy import timeseries 11 | from sunpy.time import TimeRange, is_time_equal, parse_time 12 | from sunpy.util.exceptions import SunpyUserWarning 13 | 14 | from sunkit_instruments import goes_xrs as goes 15 | from sunkit_instruments.data.test import get_test_filepath 16 | 17 | # Tests for the GOES temperature and emission measure calculations 18 | goes15_fits_filepath = get_test_filepath("go1520110607.fits") # test old FITS files 19 | goes15_filepath_nc = get_test_filepath( 20 | "sci_gxrs-l2-irrad_g15_d20170910_v0-0-0_truncated.nc" 21 | ) # test re-processed netcdf files 22 | goes16_filepath_nc = get_test_filepath( 23 | "sci_xrsf-l2-flx1s_g16_d20170910_v2-1-0_truncated.nc" 24 | ) # test the GOES-R data 25 | 26 | 27 | @pytest.mark.parametrize( 28 | ("goes_files", "max_temperature"), 29 | [ 30 | (goes15_fits_filepath, 11.9 * u.MK), 31 | (goes15_filepath_nc, 21.6 * u.MK), 32 | (goes16_filepath_nc, 21.9 * u.MK), 33 | ], 34 | ) 35 | @pytest.mark.remote_data 36 | def test_calculate_temperature_em(goes_files, max_temperature): 37 | goeslc = timeseries.TimeSeries(goes_files) 38 | goes_temp_em = goes.calculate_temperature_em(goeslc) 39 | # check that it returns a timeseries 40 | assert isinstance(goes_temp_em, timeseries.GenericTimeSeries) 41 | # check that both temperature and emission measure in the columns 42 | assert "temperature" in goes_temp_em.columns 43 | assert "emission_measure" in goes_temp_em.columns 44 | # check units 45 | assert goes_temp_em.units["emission_measure"].to(u.cm**-3) == 1 46 | assert goes_temp_em.units["temperature"].to(u.MK) == 1 47 | # check time index isn't changed 48 | assert np.all(goeslc.time == goes_temp_em.time) 49 | 50 | assert u.allclose( 51 | np.nanmax(goes_temp_em.quantity("temperature")), max_temperature, rtol=1 52 | ) 53 | 54 | 55 | @pytest.mark.remote_data 56 | def test_calculate_temperature_emiss_abundances(): 57 | goeslc = timeseries.TimeSeries(goes15_filepath_nc) 58 | goes_temp_em = goes.calculate_temperature_em(goeslc, abundance="photospheric") 59 | assert isinstance(goes_temp_em, timeseries.GenericTimeSeries) 60 | # make sure its the right value (different from above test default) 61 | assert u.allclose( 62 | np.nanmax(goes_temp_em.quantity("temperature")), 23.4 * u.MK, rtol=1 63 | ) 64 | 65 | # test when an unaccepted abundance is passed. 66 | with pytest.raises(ValueError): 67 | goes.calculate_temperature_em(goeslc, abundance="hello") 68 | 69 | 70 | @pytest.mark.remote_data 71 | def test_calculate_temperature_emiss_errs(): 72 | # check when not a XRS timeseries is passed 73 | with pytest.raises(TypeError): 74 | goes.calculate_temperature_em([]) 75 | lyra_ts = timeseries.TimeSeries( 76 | get_test_filepath("lyra_20150101-000000_lev3_std_truncated.fits.gz") 77 | ) 78 | with pytest.raises(TypeError): 79 | goes.calculate_temperature_em(lyra_ts) 80 | 81 | 82 | @pytest.mark.remote_data 83 | def test_calculate_temperature_emiss_no_primary_detector_columns_GOESR(): 84 | goeslc = timeseries.TimeSeries(goes16_filepath_nc) 85 | goeslc_removed_col = goeslc.remove_column("xrsa_primary_chan").remove_column( 86 | "xrsb_primary_chan" 87 | ) 88 | 89 | with pytest.warns(SunpyUserWarning): 90 | goes.calculate_temperature_em(goeslc_removed_col) 91 | 92 | 93 | # We also test against the IDL outputs for the GOES-15 and 16 test files 94 | idl_chianti_tem_15 = get_test_filepath("goes_15_test_chianti_tem_idl.sav") 95 | idl_chianti_tem_16 = get_test_filepath("goes_16_test_chianti_tem_idl.sav") 96 | 97 | 98 | @pytest.mark.parametrize( 99 | ("goes_files", "idl_files"), 100 | [ 101 | (goes15_filepath_nc, idl_chianti_tem_15), 102 | (goes16_filepath_nc, idl_chianti_tem_16), 103 | ], 104 | ) 105 | @pytest.mark.remote_data 106 | def test_comparison_with_IDL_version(goes_files, idl_files): 107 | """ 108 | Test that the outputs are the same for the IDL functionality goes_chianti_tem.pro. 109 | To create the test sav files in IDL: 110 | 111 | ; for netcdf files for GOES<16 need to pass remove_scaling=0 as no scaling in data 112 | file15 = "sci_gxrs-l2-irrad_g15_d20170910_v0-0-0_truncated.nc" 113 | read_goes_nc, file15, data15 114 | goes_chianti_tem, data15.B_FLUX, data15.A_FLUX, temperature, emissions_measure, satellite=15, remove_scaling=0 115 | save, temperature, emissions_measure, filename="goes_15_test_chianti_tem_idl.sav" 116 | 117 | ; GOES-R need to pass the primary and secondary detectory arrays too 118 | file16 = "sci_xrsf-l2-flx1s_g16_d20170910_v2-1-0_truncated.nc" 119 | read_goes_nc, file16, data16 120 | goes_chianti_tem, data16.XRSB_FLUX, data16.XRSA_FLUX, temperature, emissions_measure, satellite=16, a_prim=data16.XRSA_PRIMARY_CHAN, b_prim=data16.XRSB_PRIMARY_CHAN 121 | save, temperature, emissions_measure, filename="goes_16_test_idl.sav" 122 | 123 | """ 124 | goeslc = timeseries.TimeSeries(goes_files) 125 | goes_temp_em = goes.calculate_temperature_em(goeslc) 126 | 127 | idl_output = readsav(idl_files) 128 | # in the sunkit-instr version we only calculate the temp/emission measure for 129 | # times when the data quality is good, unlike the IDL version. So we need to account for this in the test. 130 | (nan_inds,) = np.where(goes_temp_em._data["temperature"].isnull()) 131 | idl_temperature = idl_output["temperature"].copy() 132 | idl_em = idl_output["emissions_measure"].copy() 133 | idl_temperature[nan_inds] = np.nan 134 | idl_em[nan_inds] = np.nan 135 | 136 | ## Only check during flare 137 | np.testing.assert_allclose( 138 | idl_temperature[500:], goes_temp_em._data["temperature"].values[500:], rtol=0.01 139 | ) 140 | # IDL output is in units of 1e49, so need to divide goes_temp_em emission measure by this 141 | np.testing.assert_allclose( 142 | idl_em[500:], 143 | goes_temp_em._data["emission_measure"].values[500:] / 1e49, 144 | rtol=0.01, 145 | ) 146 | 147 | 148 | # Test the other GOES-XRS functionality 149 | @pytest.mark.remote_data 150 | def test_goes_event_list(): 151 | # Set a time range to search 152 | trange = TimeRange("2011-06-07 00:00", "2011-06-08 00:00") 153 | # Test case where GOES class filter is applied 154 | result = goes.get_goes_event_list(trange, goes_class_filter="M1") 155 | assert isinstance(result, list) 156 | assert isinstance(result[0], dict) 157 | assert isinstance(result[0]["event_date"], str) 158 | assert isinstance(result[0]["goes_location"], tuple) 159 | assert isinstance(result[0]["peak_time"], Time) 160 | assert isinstance(result[0]["start_time"], Time) 161 | assert isinstance(result[0]["end_time"], Time) 162 | assert isinstance(result[0]["goes_class"], str) 163 | assert isinstance(result[0]["noaa_active_region"], np.int64) 164 | assert result[0]["event_date"] == "2011-06-07" 165 | assert result[0]["goes_location"] == (54, -21) 166 | # float error 167 | assert is_time_equal(result[0]["start_time"], parse_time((2011, 6, 7, 6, 16))) 168 | assert is_time_equal(result[0]["peak_time"], parse_time((2011, 6, 7, 6, 41))) 169 | assert is_time_equal(result[0]["end_time"], parse_time((2011, 6, 7, 6, 59))) 170 | assert result[0]["goes_class"] == "M2.5" 171 | assert result[0]["noaa_active_region"] == 11226 172 | # Test case where GOES class filter not applied 173 | result = goes.get_goes_event_list(trange) 174 | assert isinstance(result, list) 175 | assert isinstance(result[0], dict) 176 | assert isinstance(result[0]["event_date"], str) 177 | assert isinstance(result[0]["goes_location"], tuple) 178 | assert isinstance(result[0]["peak_time"], Time) 179 | assert isinstance(result[0]["start_time"], Time) 180 | assert isinstance(result[0]["end_time"], Time) 181 | assert isinstance(result[0]["goes_class"], str) 182 | assert isinstance(result[0]["noaa_active_region"], np.int64) 183 | assert result[0]["event_date"] == "2011-06-07" 184 | assert result[0]["goes_location"] == (54, -21) 185 | assert is_time_equal(result[0]["start_time"], parse_time((2011, 6, 7, 6, 16))) 186 | assert is_time_equal(result[0]["peak_time"], parse_time((2011, 6, 7, 6, 41))) 187 | assert is_time_equal(result[0]["end_time"], parse_time((2011, 6, 7, 6, 59))) 188 | assert result[0]["goes_class"] == "M2.5" 189 | assert result[0]["noaa_active_region"] == 11226 190 | 191 | 192 | def test_flux_to_classletter(): 193 | """ 194 | Test converting fluxes into a class letter. 195 | """ 196 | fluxes = Quantity(10 ** (-np.arange(9, 2.0, -1)), "W/m**2") 197 | classesletter = ["A", "A", "B", "C", "M", "X", "X"] 198 | calculated_classesletter = [goes.flux_to_flareclass(f)[0] for f in fluxes] 199 | calculated_classnumber = [float(goes.flux_to_flareclass(f)[1:]) for f in fluxes] 200 | assert_array_equal(classesletter, calculated_classesletter) 201 | assert_array_equal([0.1, 1, 1, 1, 1, 1, 10], calculated_classnumber) 202 | # now test the Examples 203 | assert goes.flux_to_flareclass(1e-08 * u.watt / u.m**2) == "A1" 204 | assert goes.flux_to_flareclass(0.00682 * u.watt / u.m**2) == "X68.2" 205 | assert goes.flux_to_flareclass(7.8e-09 * u.watt / u.m**2) == "A0.78" 206 | assert goes.flux_to_flareclass(0.00024 * u.watt / u.m**2) == "X2.4" 207 | assert goes.flux_to_flareclass(4.7e-06 * u.watt / u.m**2) == "C4.7" 208 | assert goes.flux_to_flareclass(6.9e-07 * u.watt / u.m**2) == "B6.9" 209 | assert goes.flux_to_flareclass(2.1e-05 * u.watt / u.m**2) == "M2.1" 210 | 211 | 212 | def test_class_to_flux(): 213 | classes = ["A3.49", "A0.23", "M1", "X2.3", "M5.8", "C2.3", "B3.45", "X20"] 214 | results = Quantity( 215 | [3.49e-8, 2.3e-9, 1e-5, 2.3e-4, 5.8e-5, 2.3e-6, 3.45e-7, 2e-3], "W/m2" 216 | ) 217 | for c, r in zip(classes, results): 218 | assert_almost_equal(r.value, goes.flareclass_to_flux(c).value) 219 | 220 | 221 | def test_joint_class_to_flux(): 222 | classes = ["A3.49", "A0.23", "M1", "X2.3", "M5.8", "C2.3", "B3.45", "X20"] 223 | for c in classes: 224 | assert c == goes.flux_to_flareclass(goes.flareclass_to_flux(c)) 225 | 226 | 227 | # TODO add a test to check for raising error 228 | -------------------------------------------------------------------------------- /sunkit_instruments/iris/__init__.py: -------------------------------------------------------------------------------- 1 | from .iris import * # NOQA 2 | -------------------------------------------------------------------------------- /sunkit_instruments/iris/iris.py: -------------------------------------------------------------------------------- 1 | """ 2 | This package provides Interface Region Imaging Spectrometer (IRIS) instrument 3 | routines. 4 | 5 | .. note:: 6 | 7 | More comprehensive IRIS tools are now being developed by the 8 | `IRIS instrument team. `__ 9 | """ 10 | 11 | import sunpy.map 12 | import sunpy.time 13 | from sunpy.io._file_tools import read_file 14 | 15 | __all__ = ["SJI_to_sequence"] 16 | 17 | 18 | def SJI_to_sequence(filename, start=0, stop=None, hdu=0): 19 | """ 20 | Read a SJI file and return a `sunpy.map.MapSequence`. 21 | 22 | .. warning:: 23 | This function is a very early beta and is not stable. Further work is 24 | on going to improve SunPy IRIS support. 25 | 26 | Parameters 27 | ---------- 28 | filename: `str` 29 | File to read. 30 | start: `int`, optional 31 | Temporal axis index to create `~sunpy.map.MapSequence` from. 32 | Defaults to 0, which will start from the beginning. 33 | stop: `int`, optional 34 | Temporal index to stop `~sunpy.map.MapSequence` at. 35 | Defaults to `None`, which will use the entire index. 36 | hdu: `int`, optional 37 | The hdu index to use, defaults to 0. 38 | 39 | Returns 40 | ------- 41 | `~sunpy.map.MapSequence` 42 | A map sequence of the SJI data. 43 | """ 44 | 45 | hdus = read_file(filename) 46 | # Get the time delta 47 | time_range = sunpy.time.TimeRange(hdus[hdu][1]["STARTOBS"], hdus[hdu][1]["ENDOBS"]) 48 | splits = time_range.split(hdus[hdu][0].shape[0]) 49 | 50 | if not stop: 51 | stop = len(splits) 52 | 53 | headers = [hdus[hdu][1]] * (stop - start) 54 | datas = hdus[hdu][0][start:stop] 55 | 56 | # Make the cube: 57 | iris_cube = sunpy.map.Map(list(zip(datas, headers)), sequence=True) 58 | # Set the date/time 59 | 60 | for i, m in enumerate(iris_cube): 61 | m.meta["DATE-OBS"] = splits[i].center.isot 62 | 63 | return iris_cube 64 | -------------------------------------------------------------------------------- /sunkit_instruments/iris/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunpy/sunkit-instruments/590dd43244c63673d2f0f9e93022698d0791e179/sunkit_instruments/iris/tests/__init__.py -------------------------------------------------------------------------------- /sunkit_instruments/iris/tests/test_iris.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import numpy as np 4 | import pytest 5 | 6 | import sunpy.map 7 | 8 | from sunkit_instruments import iris 9 | from sunkit_instruments.data.test import rootdir 10 | 11 | try: 12 | from sunpy.util.exceptions import SunpyMetadataWarning 13 | except ImportError: 14 | from sunpy.util.exceptions import SunpyUserWarning as SunpyMetadataWarning 15 | 16 | 17 | def test_SJI_to_sequence(): 18 | test_data = os.path.join( 19 | rootdir, "iris_l2_20130801_074720_4040000014_SJI_1400_t000.fits" 20 | ) 21 | iris_cube = iris.SJI_to_sequence(test_data, start=0, stop=None, hdu=0) 22 | 23 | assert isinstance(iris_cube, sunpy.map.MapSequence) 24 | assert isinstance(iris_cube.maps[0], sunpy.map.sources.SJIMap) 25 | assert len(iris_cube.maps) == 2 26 | assert iris_cube.maps[0].meta["DATE-OBS"] != iris_cube.maps[1].meta["DATE-OBS"] 27 | 28 | 29 | def test_iris_rot(): 30 | test_data = os.path.join( 31 | rootdir, "iris_l2_20130801_074720_4040000014_SJI_1400_t000.fits" 32 | ) 33 | iris_cube = iris.SJI_to_sequence(test_data, start=0, stop=None, hdu=0) 34 | irismap = iris_cube.maps[0] 35 | with pytest.warns(SunpyMetadataWarning, match="Missing metadata for observer"): 36 | irismap_rot = irismap.rotate() 37 | 38 | assert isinstance(irismap_rot, sunpy.map.sources.SJIMap) 39 | 40 | np.testing.assert_allclose(irismap_rot.meta["pc1_1"], 1) 41 | np.testing.assert_allclose(irismap_rot.meta["pc1_2"], 0, atol=1e-7) 42 | np.testing.assert_allclose(irismap_rot.meta["pc2_1"], 0, atol=1e-7) 43 | np.testing.assert_allclose(irismap_rot.meta["pc2_2"], 1) 44 | -------------------------------------------------------------------------------- /sunkit_instruments/lyra/__init__.py: -------------------------------------------------------------------------------- 1 | from .lyra import * # NOQA 2 | -------------------------------------------------------------------------------- /sunkit_instruments/lyra/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunpy/sunkit-instruments/590dd43244c63673d2f0f9e93022698d0791e179/sunkit_instruments/lyra/tests/__init__.py -------------------------------------------------------------------------------- /sunkit_instruments/response/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | A subpackage for computing instrument responses 3 | """ 4 | 5 | from sunkit_instruments.response.thermal import SourceSpectra, get_temperature_response 6 | -------------------------------------------------------------------------------- /sunkit_instruments/response/abstractions.py: -------------------------------------------------------------------------------- 1 | """This module defines abstractions for computing instrument response.""" 2 | import abc 3 | 4 | import astropy.units as u 5 | 6 | __all__ = ["AbstractChannel"] 7 | 8 | 9 | class AbstractChannel(abc.ABC): 10 | """ 11 | An abstract base class for defining instrument channels. 12 | 13 | For all methods and properties defined here, see the 14 | topic guide on instrument response for more information. 15 | """ 16 | 17 | @u.quantity_input 18 | def wavelength_response( 19 | self, obstime=None 20 | ) -> u.cm**2 * u.DN * u.steradian / (u.photon * u.pixel): 21 | """ 22 | Instrument response as a function of wavelength 23 | 24 | The wavelength response is the effective area with 25 | the conversion factors from photons to DN and steradians 26 | to pixels. 27 | 28 | Parameters 29 | ---------- 30 | obstime: any format parsed by `~sunpy.time.parse_time`, optional 31 | If specified, this is used to compute the time-dependent 32 | instrument degradation. 33 | """ 34 | area_eff = self.effective_area(obstime=obstime) 35 | return ( 36 | area_eff 37 | * self.energy_per_photon 38 | * self.pixel_solid_angle 39 | * self.camera_gain 40 | / self.energy_per_electron 41 | ) 42 | 43 | @u.quantity_input 44 | def effective_area(self, obstime=None) -> u.cm**2: 45 | """ 46 | Effective area as a function of wavelength. 47 | 48 | The effective area is the geometrical collecting area 49 | weighted by the mirror reflectance, filter transmittance, 50 | quantum efficiency, and instrument degradation. 51 | 52 | Parameters 53 | ---------- 54 | obstime: any format parsed by `sunpy.time.parse_time`, optional 55 | If specified, this is used to compute the time-dependent 56 | instrument degradation. 57 | """ 58 | return ( 59 | self.geometrical_area 60 | * self.mirror_reflectance 61 | * self.filter_transmittance 62 | * self.effective_quantum_efficiency 63 | * self.degradation(obstime=obstime) 64 | ) 65 | 66 | @property 67 | @u.quantity_input 68 | def energy_per_photon(self) -> u.eV / u.photon: 69 | return self.wavelength.to("eV", equivalencies=u.spectral()) / u.photon 70 | 71 | @abc.abstractmethod 72 | @u.quantity_input 73 | def degradation(self, obstime=None) -> u.dimensionless_unscaled: ... 74 | 75 | @property 76 | @abc.abstractmethod 77 | @u.quantity_input 78 | def geometrical_area(self) -> u.cm**2: ... 79 | 80 | @property 81 | @abc.abstractmethod 82 | @u.quantity_input 83 | def mirror_reflectance(self) -> u.dimensionless_unscaled: ... 84 | 85 | @property 86 | @abc.abstractmethod 87 | @u.quantity_input 88 | def filter_transmittance(self) -> u.dimensionless_unscaled: ... 89 | 90 | @property 91 | @abc.abstractmethod 92 | @u.quantity_input 93 | def effective_quantum_efficiency(self) -> u.dimensionless_unscaled: ... 94 | 95 | @property 96 | @abc.abstractmethod 97 | @u.quantity_input 98 | def camera_gain(self) -> u.DN / u.electron: ... 99 | 100 | @property 101 | @abc.abstractmethod 102 | @u.quantity_input 103 | def energy_per_electron(self) -> u.eV / u.electron: ... 104 | 105 | @property 106 | @abc.abstractmethod 107 | @u.quantity_input 108 | def pixel_solid_angle(self) -> u.steradian / u.pixel: ... 109 | 110 | @property 111 | @abc.abstractmethod 112 | @u.quantity_input 113 | def wavelength(self) -> u.Angstrom: ... 114 | -------------------------------------------------------------------------------- /sunkit_instruments/response/tests/test_channel.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | 4 | import astropy.units as u 5 | 6 | from sunkit_instruments.response import SourceSpectra 7 | from sunkit_instruments.response.abstractions import AbstractChannel 8 | 9 | 10 | class TestChannel(AbstractChannel): 11 | @property 12 | @u.quantity_input 13 | def wavelength(self) -> u.Angstrom: 14 | return np.linspace(100, 200, 100) * u.AA 15 | 16 | @u.quantity_input 17 | def degradation(self, obstime=None) -> u.dimensionless_unscaled: 18 | return 1.0 19 | 20 | @property 21 | @u.quantity_input 22 | def geometrical_area(self) -> u.cm**2: 23 | return 10 * u.cm**2 24 | 25 | @property 26 | @u.quantity_input 27 | def mirror_reflectance(self) -> u.dimensionless_unscaled: 28 | return np.exp( 29 | -((self.wavelength - self.wavelength[0]) / self.wavelength[0]).decompose() 30 | ) 31 | 32 | @property 33 | @u.quantity_input 34 | def filter_transmittance(self) -> u.dimensionless_unscaled: 35 | return np.exp( 36 | -((self.wavelength - 150 * u.AA) ** 2 / (1 * u.AA) ** 2).decompose() 37 | ) 38 | 39 | @property 40 | @u.quantity_input 41 | def effective_quantum_efficiency(self) -> u.dimensionless_unscaled: 42 | return np.ones(self.wavelength.shape) 43 | 44 | @property 45 | @u.quantity_input 46 | def camera_gain(self) -> u.DN / u.electron: 47 | return 2 * u.DN / u.electron 48 | 49 | @property 50 | @u.quantity_input 51 | def energy_per_electron(self) -> u.eV / u.electron: 52 | return 3.65 * u.eV / u.electron 53 | 54 | @property 55 | @u.quantity_input 56 | def pixel_solid_angle(self) -> u.steradian / u.pixel: 57 | return (1 * u.arcsec) ** 2 / u.pixel 58 | 59 | 60 | @pytest.fixture 61 | def fake_channel(): 62 | return TestChannel() 63 | 64 | 65 | def test_effective_area(fake_channel): 66 | assert isinstance(fake_channel.effective_area(), u.Quantity) 67 | 68 | 69 | def test_wavelength_response(fake_channel): 70 | assert isinstance(fake_channel.wavelength_response(), u.Quantity) 71 | 72 | 73 | @pytest.fixture 74 | def fake_spectra(): 75 | temperature = np.logspace(5, 8, 100) * u.K 76 | density = 1e15 * u.cm ** (-3) * u.K / temperature 77 | wavelength = np.linspace(50, 250, 1000) * u.AA 78 | data = np.random.rand(*temperature.shape + wavelength.shape) * u.Unit( 79 | "photon cm3 s-1 sr-1 Angstrom-1" 80 | ) 81 | return SourceSpectra(temperature, wavelength, data, density=density) 82 | 83 | 84 | def test_spectra_repr(fake_spectra): 85 | assert isinstance(fake_spectra.__repr__(), str) 86 | 87 | 88 | @pytest.mark.parametrize('obstime', [None, '2020-01-01']) 89 | def test_temperature_response(fake_channel, fake_spectra, obstime): 90 | temp_response = fake_spectra.temperature_response(fake_channel, obstime=obstime) 91 | assert isinstance(temp_response, u.Quantity) 92 | assert temp_response.shape == fake_spectra.temperature.shape 93 | -------------------------------------------------------------------------------- /sunkit_instruments/response/tests/test_source_spectra.py: -------------------------------------------------------------------------------- 1 | """ 2 | Smoke tests for SourceSpectra 3 | """ 4 | import numpy as np 5 | import pytest 6 | 7 | import astropy.units as u 8 | 9 | from sunkit_instruments.response import SourceSpectra 10 | 11 | 12 | @pytest.mark.parametrize(('density', 'meta'), [ 13 | (None, None), 14 | (1e15*u.K/u.cm**3, None), 15 | (None, {'abundance_model': 'test'}), 16 | (1e15*u.K/u.cm**3, {'abundance_model': 'test'}), 17 | ]) 18 | def test_create_source_spectra(density, meta): 19 | temperature = np.logspace(4, 9, 100) * u.K 20 | wavelength = np.linspace(1, 1000, 50) * u.Angstrom 21 | if density is not None: 22 | density = density / temperature 23 | data_shape = temperature.shape + wavelength.shape 24 | data = np.random.rand(*data_shape) * u.Unit("photon cm3 s-1 sr-1 Angstrom-1") 25 | spec = SourceSpectra( 26 | temperature, 27 | wavelength, 28 | data, 29 | density=density, 30 | meta=meta, 31 | ) 32 | assert isinstance(spec.meta, dict) 33 | assert isinstance(spec.data, u.Quantity) 34 | assert isinstance(spec.wavelength, u.Quantity) 35 | assert isinstance(spec.temperature, u.Quantity) 36 | if density is not None: 37 | assert isinstance(spec.density, u.Quantity) 38 | else: 39 | with pytest.raises(ValueError, match="No density data available."): 40 | spec.density 41 | -------------------------------------------------------------------------------- /sunkit_instruments/response/thermal.py: -------------------------------------------------------------------------------- 1 | """ 2 | Classes for computing the temperature response 3 | """ 4 | import xarray 5 | 6 | import astropy.units as u 7 | 8 | __all__ = ["SourceSpectra", "get_temperature_response"] 9 | 10 | 11 | def get_temperature_response(channel, spectra, obstime=None): 12 | """ 13 | Calculate the temperature response function for a given instrument channel 14 | and input spectra. 15 | 16 | Parameters 17 | ---------- 18 | channel: `~sunkit_instruments.response.abstractions.AbstractChannel` 19 | spectra: `~sunkit_instruments.response.SourceSpectra` 20 | obstime: any format parsed by `sunpy.time.parse_time` , optional 21 | 22 | Returns 23 | ------- 24 | temperature: `~astropy.units.Quantity` 25 | response: `~astropy.units.Quantity` 26 | 27 | See Also 28 | -------- 29 | sunkit_instruments.response.SourceSpectra.temperature_response 30 | """ 31 | return spectra.temperature, spectra.temperature_response(channel, obstime=obstime) 32 | 33 | 34 | class SourceSpectra: 35 | """ 36 | Source spectra as a function of temperature and wavelength. 37 | 38 | The source spectra describes how a plasma emits (under the optically-thin 39 | assumption) as a function of both temperature and wavelength. This source 40 | spectra is typically computed using a database like CHIANTI by summing the 41 | emission spectra of many ions as well as the continuum emission. For more 42 | information, see the topic guide on instrument response. 43 | 44 | Parameters 45 | ---------- 46 | temperature: `~astropy.units.Quantity` 47 | 1D array describing the variation along the temperature axis. 48 | wavelength: `~astropy.units.Quantity` 49 | 1D array describing the variation along the wavelength axis. 50 | spectra: `~astropy.units.Quantity` 51 | Source spectra as a 2D array. The first axis should correspond to temperature and the 52 | second axis should correspond to wavelength. 53 | density: `~astropy.units.Quantity`, optional 54 | 1D array describing the variation in density along the temperature axis. It is assumed 55 | that temperature and density are dependent. 56 | meta: `dict`, optional 57 | Any optional metadata to attach to the spectra, e.g. abundance model, CHIANTI version. 58 | """ 59 | 60 | @u.quantity_input 61 | def __init__( 62 | self, 63 | temperature: u.K, 64 | wavelength: u.Angstrom, 65 | spectra: u.photon * u.cm**3 / (u.s * u.Angstrom * u.steradian), 66 | density: u.cm ** (-3) = None, 67 | meta=None, 68 | ): 69 | self.meta = meta 70 | coords = { 71 | "temperature": xarray.Variable( 72 | "temperature", 73 | temperature.value, 74 | attrs={"unit": temperature.unit.to_string()}, 75 | ), 76 | "wavelength": xarray.Variable( 77 | "wavelength", 78 | wavelength.value, 79 | attrs={"unit": wavelength.unit.to_string()}, 80 | ), 81 | } 82 | if density is not None: 83 | coords["density"] = xarray.Variable( 84 | "temperature", density.value, attrs={"unit": density.unit.to_string()} 85 | ) 86 | self._da = xarray.DataArray( 87 | spectra.data, 88 | dims=["temperature", "wavelength"], 89 | coords=coords, 90 | attrs={"unit": spectra.unit.to_string(), **self.meta}, 91 | ) 92 | 93 | def __repr__(self): 94 | return self._da.__repr__() 95 | 96 | def __str__(self): 97 | return self._da.__str__() 98 | 99 | def _repr_html_(self): 100 | return self._da._repr_html_() 101 | 102 | @property 103 | def meta(self): 104 | return self._meta 105 | 106 | @meta.setter 107 | def meta(self, x): 108 | if x is None: 109 | self._meta = {} 110 | elif isinstance(x, dict): 111 | self._meta = x 112 | else: 113 | raise TypeError(f'Unsupported metadata type {type(x)}') 114 | 115 | @property 116 | @u.quantity_input 117 | def temperature(self) -> u.K: 118 | return u.Quantity(self._da.temperature.data, self._da.temperature.attrs["unit"]) 119 | 120 | @property 121 | @u.quantity_input 122 | def wavelength(self) -> u.Angstrom: 123 | return u.Quantity(self._da.wavelength.data, self._da.wavelength.attrs["unit"]) 124 | 125 | @property 126 | @u.quantity_input 127 | def density(self) -> u.cm**(-3): 128 | if "density" in self._da.coords: 129 | return u.Quantity(self._da.density.data, self._da.density.attrs["unit"]) 130 | else: 131 | raise ValueError("No density data available.") 132 | 133 | @property 134 | @u.quantity_input 135 | def data(self) -> u.photon * u.cm**3 / (u.s * u.Angstrom * u.steradian): 136 | return u.Quantity(self._da.data, self._da.attrs["unit"]) 137 | 138 | @u.quantity_input 139 | def temperature_response( 140 | self, channel, obstime=None 141 | ) -> u.cm**5 * u.DN / (u.pixel * u.s): 142 | """ 143 | Temperature response function for a given instrument channel. 144 | 145 | The temperature response function describes the sensitivity of an imaging 146 | instrument as a function of temperature. The temperature response is 147 | calculated by integrating the source spectra over the wavelength dimension, 148 | weighted by the wavelength response of the instrument. 149 | 150 | Parameters 151 | ---------- 152 | channel: `~sunkit_instruments.response.abstractions.AbstractChannel` 153 | The relevant instrument channel object used to compute the wavelength 154 | response function. 155 | obstime: any format parsed by `sunpy.time.parse_time`, optional 156 | A time of a particular observation. This is used to calculated any 157 | time-dependent instrument degradation. 158 | """ 159 | wave_response = channel.wavelength_response(obstime=obstime) 160 | da_response = xarray.DataArray( 161 | wave_response.to_value(wave_response.unit), 162 | dims=['wavelength'], 163 | coords=[xarray.Variable('wavelength', 164 | channel.wavelength.to_value('Angstrom'), 165 | attrs={'unit': 'Angstrom'}),], 166 | attrs={'unit': wave_response.unit.to_string()} 167 | ) 168 | spec_interp = self._da.interp( 169 | wavelength=da_response.wavelength, 170 | kwargs={'bounds_error': False, 'fill_value': 0.0}, 171 | ) 172 | spec_interp_weighted = spec_interp * da_response 173 | temp_response = spec_interp_weighted.integrate(coord='wavelength') 174 | final_unit = (u.Unit(spec_interp.unit) 175 | * u.Unit(da_response.attrs['unit']) 176 | * u.Unit(spec_interp_weighted.wavelength.unit)) 177 | return u.Quantity(temp_response.data, final_unit) 178 | -------------------------------------------------------------------------------- /sunkit_instruments/rhessi/__init__.py: -------------------------------------------------------------------------------- 1 | from .rhessi import * # NOQA 2 | -------------------------------------------------------------------------------- /sunkit_instruments/rhessi/rhessi.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module provides processing routines programs to process and analyze RHESSI 3 | data. 4 | """ 5 | 6 | import re 7 | import csv 8 | 9 | import numpy as np 10 | 11 | import astropy.units as u 12 | from astropy.time import Time, TimeDelta 13 | 14 | from sunpy.coordinates import sun 15 | from sunpy.io._file_tools import read_file 16 | from sunpy.time import TimeRange, parse_time 17 | 18 | __all__ = [ 19 | "parse_observing_summary_hdulist", 20 | "backprojection", 21 | "parse_observing_summary_dbase_file", 22 | "_build_energy_bands", 23 | "uncompress_countrate", 24 | "imagecube2map", 25 | ] 26 | 27 | 28 | # Measured fixed grid parameters 29 | grid_pitch = ( 30 | 4.52467, 31 | 7.85160, 32 | 13.5751, 33 | 23.5542, 34 | 40.7241, 35 | 70.5309, 36 | 122.164, 37 | 211.609, 38 | 366.646, 39 | ) 40 | grid_orientation = ( 41 | 3.53547, 42 | 2.75007, 43 | 3.53569, 44 | 2.74962, 45 | 3.92596, 46 | 2.35647, 47 | 0.786083, 48 | 0.00140674, 49 | 1.57147, 50 | ) 51 | 52 | lc_linecolors = ( 53 | "black", 54 | "pink", 55 | "green", 56 | "blue", 57 | "brown", 58 | "red", 59 | "navy", 60 | "orange", 61 | "green", 62 | ) 63 | 64 | 65 | def parse_observing_summary_dbase_file(filename): 66 | """ 67 | Parse the RHESSI observing summary database file. 68 | 69 | This file lists the name of observing summary files 70 | for specific time ranges along with other info. 71 | 72 | .. note:: 73 | This API is currently limited to providing data from whole days only. 74 | 75 | Parameters 76 | ---------- 77 | filename : `str` 78 | The filename of the obssumm dbase file. 79 | 80 | Returns 81 | ------- 82 | `dict` 83 | Return a `dict` containing the parsed data in the dbase file. 84 | 85 | Examples 86 | -------- 87 | >>> import sunkit_instruments.rhessi as rhessi 88 | >>> rhessi.parse_observing_summary_dbase_file(fname) # doctest: +SKIP 89 | 90 | References 91 | ---------- 92 | https://hesperia.gsfc.nasa.gov/ssw/hessi/doc/guides/hessi_data_access.htm#Observing%20Summary%20Data 93 | """ 94 | # An example dbase file can be found at: 95 | # https://hesperia.gsfc.nasa.gov/hessidata/dbase/hsi_obssumm_filedb_200311.txt 96 | 97 | with open(filename) as fd: 98 | reader = csv.reader(fd, delimiter=" ", skipinitialspace=True) 99 | _ = next(reader) # skip 'HESSI Filedb File:' row 100 | _ = next(reader) # skip 'Created: ...' row 101 | _ = next(reader) # skip 'Number of Files: ...' row 102 | column_names = next(reader) # ['Filename', 'Orb_st', 'Orb_end',...] 103 | 104 | obssumm_filename = [] 105 | orbit_start = [] 106 | orbit_end = [] 107 | start_time = [] 108 | end_time = [] 109 | status_flag = [] 110 | number_of_packets = [] 111 | 112 | for row in reader: 113 | obssumm_filename.append(row[0]) 114 | orbit_start.append(int(row[1])) 115 | orbit_end.append(int(row[2])) 116 | start_time.append(Time.strptime(row[3], "%d-%b-%y")) # skip time 117 | end_time.append(Time.strptime(row[5], "%d-%b-%y")) # skip time 118 | status_flag.append(int(row[7])) 119 | number_of_packets.append(int(row[8])) 120 | 121 | return { 122 | column_names[0].lower(): obssumm_filename, 123 | column_names[1].lower(): orbit_start, 124 | column_names[2].lower(): orbit_end, 125 | column_names[3].lower(): start_time, 126 | column_names[4].lower(): end_time, 127 | column_names[5].lower(): status_flag, 128 | column_names[6].lower(): number_of_packets, 129 | } 130 | 131 | 132 | def parse_observing_summary_hdulist(hdulist): 133 | """ 134 | Parse a RHESSI observation summary file. 135 | 136 | Parameters 137 | ---------- 138 | hdulist : `list` 139 | The HDU list from the fits file. 140 | 141 | Returns 142 | ------- 143 | out : `dict` 144 | Returns a dictionary. 145 | """ 146 | header = hdulist[0].header 147 | 148 | reference_time_ut = parse_time(hdulist[5].data.field("UT_REF")[0], format="utime") 149 | time_interval_sec = hdulist[5].data.field("TIME_INTV")[0] 150 | # label_unit = fits[5].data.field('DIM1_UNIT')[0] 151 | # labels = fits[5].data.field('DIM1_IDS') 152 | labels = [ 153 | "3 - 6 keV", 154 | "6 - 12 keV", 155 | "12 - 25 keV", 156 | "25 - 50 keV", 157 | "50 - 100 keV", 158 | "100 - 300 keV", 159 | "300 - 800 keV", 160 | "800 - 7000 keV", 161 | "7000 - 20000 keV", 162 | ] 163 | 164 | # The data stored in the fits file are "compressed" countrates stored as 165 | # one byte 166 | compressed_countrate = np.array(hdulist[6].data.field("countrate")) 167 | 168 | countrate = uncompress_countrate(compressed_countrate) 169 | dim = np.array(countrate[:, 0]).size 170 | 171 | time_array = parse_time(reference_time_ut) + TimeDelta( 172 | time_interval_sec * np.arange(dim) * u.second 173 | ) 174 | 175 | # TODO generate the labels for the dict automatically from labels 176 | data = {"time": time_array, "data": countrate, "labels": labels} 177 | 178 | return header, data 179 | 180 | 181 | def uncompress_countrate(compressed_countrate): 182 | """ 183 | Convert the compressed count rate inside of observing summary file from a 184 | compressed byte to a true count rate. 185 | 186 | Parameters 187 | ---------- 188 | compressed_countrate : `bytes` array 189 | A compressed count rate returned from an observing summary file. 190 | 191 | References 192 | ---------- 193 | `Hsi_obs_summ_decompress.pro `_ 194 | """ 195 | 196 | # Ensure uncompressed counts are between 0 and 255 197 | if (compressed_countrate.min() < 0) or (compressed_countrate.max() > 255): 198 | raise ValueError( 199 | f"Expected uncompressed counts {compressed_countrate} to in range 0-255" 200 | ) 201 | 202 | # TODO Must be a better way than creating entire lookup table on each call 203 | ll = np.arange(0, 16, 1) 204 | lkup = np.zeros(256, dtype="int") 205 | _sum = 0 206 | for i in range(0, 16): 207 | lkup[16 * i : 16 * (i + 1)] = ll * 2**i + _sum 208 | if i < 15: 209 | _sum = lkup[16 * (i + 1) - 1] + 2**i 210 | 211 | return lkup[compressed_countrate] 212 | 213 | 214 | def hsi_linecolors(): 215 | """ 216 | Define discrete colors to use for RHESSI plots. 217 | 218 | Returns 219 | ------- 220 | `tuple` : 221 | A tuple of names of colours. 222 | 223 | References 224 | ---------- 225 | `hsi_linecolors.pro `__ 226 | """ 227 | return ("black", "magenta", "lime", "cyan", "y", "red", "blue", "orange", "olive") 228 | 229 | 230 | def _backproject( 231 | calibrated_event_list, detector=8, pixel_size=(1.0, 1.0), image_dim=(64, 64) 232 | ): 233 | """ 234 | Given a stacked calibrated event list fits file create a back projection 235 | image for an individual detectors. 236 | 237 | Parameters 238 | ---------- 239 | calibrated_event_list : `str` 240 | Filename of a RHESSI calibrated event list. 241 | detector : `int`, optional 242 | The detector number. 243 | pixel_size : `tuple`, optional 244 | A length 2 tuple with the size of the pixels in arcseconds. 245 | Defaults to ``(1, 1)``. 246 | image_dim : `tuple`, optional 247 | A length 2 tuple with the size of the output image in number of pixels. 248 | Defaults to ``(64, 64)``. 249 | 250 | Returns 251 | ------- 252 | `numpy.ndarray` 253 | A backprojection image. 254 | """ 255 | # info_parameters = fits[2] 256 | # detector_efficiency = info_parameters.data.field('cbe_det_eff$$REL') 257 | 258 | afits = read_file(calibrated_event_list) 259 | 260 | fits_detector_index = detector + 2 261 | detector_index = detector - 1 262 | grid_angle = np.pi / 2.0 - grid_orientation[detector_index] 263 | harm_ang_pitch = grid_pitch[detector_index] / 1 264 | 265 | phase_map_center = afits[fits_detector_index].data.field("phase_map_ctr") 266 | this_roll_angle = afits[fits_detector_index].data.field("roll_angle") 267 | modamp = afits[fits_detector_index].data.field("modamp") 268 | grid_transmission = afits[fits_detector_index].data.field("gridtran") 269 | count = afits[fits_detector_index].data.field("count") 270 | 271 | tempa = (np.arange(image_dim[0] * image_dim[1]) % image_dim[0]) - ( 272 | image_dim[0] - 1 273 | ) / 2.0 274 | tempb = ( 275 | tempa.reshape(image_dim[0], image_dim[1]) 276 | .transpose() 277 | .reshape(image_dim[0] * image_dim[1]) 278 | ) 279 | 280 | pixel = np.array(list(zip(tempa, tempb))) * pixel_size[0] 281 | phase_pixel = (2 * np.pi / harm_ang_pitch) * ( 282 | np.outer(pixel[:, 0], np.cos(this_roll_angle - grid_angle)) 283 | - np.outer(pixel[:, 1], np.sin(this_roll_angle - grid_angle)) 284 | ) + phase_map_center 285 | phase_modulation = np.cos(phase_pixel) 286 | gridmod = modamp * grid_transmission 287 | probability_of_transmission = gridmod * phase_modulation + grid_transmission 288 | bproj_image = np.inner(probability_of_transmission, count).reshape(image_dim) 289 | 290 | return bproj_image 291 | 292 | 293 | @u.quantity_input 294 | def backprojection( 295 | calibrated_event_list, 296 | pixel_size: u.arcsec = (1.0, 1.0) * u.arcsec, 297 | image_dim: u.pix = (64, 64) * u.pix, 298 | ): 299 | """ 300 | Given a stacked calibrated event list fits file create a back projection 301 | image. 302 | 303 | .. warning:: 304 | 305 | The image will not be in the right orientation. 306 | 307 | Parameters 308 | ---------- 309 | calibrated_event_list : `str` 310 | Filename of a RHESSI calibrated event list. 311 | pixel_size : `tuple`, optional 312 | A length 2 tuple with the size of the pixels in arcsecond 313 | `~astropy.units.Quantity`. Defaults to ``(1, 1) * u.arcsec``. 314 | image_dim : `tuple`, optional 315 | A length 2 tuple with the size of the output image in number of pixel 316 | `~astropy.units.Quantity` Defaults to ``(64, 64) * u.pix``. 317 | 318 | Returns 319 | ------- 320 | `sunpy.map.sources.RHESSIMap` 321 | A backprojection map. 322 | """ 323 | # import sunpy.map in here so that net and timeseries don't end up importing map 324 | import sunpy.map 325 | 326 | pixel_size = pixel_size.to(u.arcsec) 327 | image_dim = np.array(image_dim.to(u.pix).value, dtype=int) 328 | 329 | afits = read_file(calibrated_event_list) 330 | info_parameters = afits[2] 331 | xyoffset = info_parameters.data.field("USED_XYOFFSET")[0] 332 | time_range = TimeRange( 333 | info_parameters.data.field("ABSOLUTE_TIME_RANGE")[0], format="utime" 334 | ) 335 | 336 | image = np.zeros(image_dim) 337 | 338 | # find out what detectors were used 339 | det_index_mask = afits[1].data.field("det_index_mask")[0] 340 | detector_list = (np.arange(9) + 1) * np.array(det_index_mask) 341 | for detector in detector_list: 342 | if detector > 0: 343 | image = image + _backproject( 344 | calibrated_event_list, 345 | detector=detector, 346 | pixel_size=pixel_size.value, 347 | image_dim=image_dim, 348 | ) 349 | 350 | dict_header = { 351 | "DATE-OBS": time_range.center.strftime("%Y-%m-%d %H:%M:%S"), 352 | "CDELT1": pixel_size[0], 353 | "NAXIS1": image_dim[0], 354 | "CRVAL1": xyoffset[0], 355 | "CRPIX1": image_dim[0] / 2 + 0.5, 356 | "CUNIT1": "arcsec", 357 | "CTYPE1": "HPLN-TAN", 358 | "CDELT2": pixel_size[1], 359 | "NAXIS2": image_dim[1], 360 | "CRVAL2": xyoffset[1], 361 | "CRPIX2": image_dim[0] / 2 + 0.5, 362 | "CUNIT2": "arcsec", 363 | "CTYPE2": "HPLT-TAN", 364 | "HGLT_OBS": 0, 365 | "HGLN_OBS": 0, 366 | "RSUN_OBS": sun.angular_radius(time_range.center).value, 367 | "RSUN_REF": sunpy.sun.constants.radius.value, 368 | "DSUN_OBS": sun.earth_distance(time_range.center).value 369 | * sunpy.sun.constants.au.value, 370 | } 371 | 372 | result_map = sunpy.map.Map(image, dict_header) 373 | 374 | return result_map 375 | 376 | 377 | def _build_energy_bands(label, bands): 378 | """ 379 | Creates a list of strings with the correct formatting for axis labels. 380 | 381 | Parameters 382 | ---------- 383 | label: `str` 384 | The ``label`` to use as a basis. 385 | bands: `list` of `str` 386 | The bands to append to the ``label``. 387 | 388 | Returns 389 | ------- 390 | `list` of `str` 391 | Each string is an energy band and its unit. 392 | 393 | Example 394 | ------- 395 | >>> from sunkit_instruments.rhessi import _build_energy_bands 396 | >>> _build_energy_bands('Energy bands (keV)', ['3 - 6', '6 - 12', '12 - 25']) 397 | ['3 - 6 keV', '6 - 12 keV', '12 - 25 keV'] 398 | """ 399 | 400 | unit_pattern = re.compile(r"^.+\((?P\w+)\)$") 401 | 402 | matched = unit_pattern.match(label) 403 | 404 | if matched is None: 405 | raise ValueError( 406 | f"Unable to find energy unit in '{label}' " 407 | f"using REGEX '{unit_pattern.pattern}'" 408 | ) 409 | 410 | unit = matched.group("UNIT").strip() 411 | 412 | return [f"{band} {unit}" for band in bands] 413 | 414 | 415 | def imagecube2map(rhessi_imagecube_file): 416 | """ 417 | Extracts single map images from a RHESSI flare image datacube. Currently 418 | assumes input to be 4D. 419 | 420 | This function is analogous to the ``hsi_fits2map.pro`` functionality available in SSW. 421 | 422 | Parameters 423 | ---------- 424 | rhessi_imagecube_file : `str` 425 | Path or URL to image datacube .fits 426 | 427 | Returns 428 | ------- 429 | `dict` of `sunpy.map.MapSequence` 430 | Each energy band has a list of maps where the index of the lists represent the time step 431 | """ 432 | # import sunpy.map in here so that net and timeseries don't end up importing map 433 | from sunpy.io._file_tools import read_file 434 | from sunpy.map import Map 435 | 436 | f = read_file(rhessi_imagecube_file) 437 | header = f[0].header 438 | 439 | # make sure datacube is a RHESSI cube 440 | if header["INSTRUME"] != "RHESSI": 441 | raise ValueError(f"Expected a RHESSI datacube, got: {header['INSTRUME']}") 442 | 443 | # remove those (non-standard) headers to avoid user warnings (they are 0 anyway) 444 | del header["CROTACN1"] 445 | del header["CROTACN2"] 446 | del header["CROTA"] 447 | 448 | d_min = {} 449 | d_max = {} 450 | e_ax = f[1].data[0]["ENERGY_AXIS"].reshape((-1, 2)) # reshape energy axis to be 2D 451 | t_ax = f[1].data[0]["TIME_AXIS"].reshape((-1, 2)) # reshape time axis to be 2D 452 | data = f[0].data.reshape( 453 | tuple([1] * (4 - len(f[0].data.shape))) + f[0].data.shape 454 | ) # reshape data to be 4D 455 | for e in range(e_ax.shape[0]): 456 | d_min[e] = 1e10 457 | d_max[e] = -1e10 458 | for t in range(t_ax.shape[0]): 459 | d_min[e] = min(d_min[e], data[t][e].min()) 460 | d_max[e] = max(d_max[e], data[t][e].max()) 461 | 462 | maps = {} # result dictionary 463 | for e in range(e_ax.shape[0]): 464 | header["ENERGY_L"] = e_ax[e][0] 465 | header["ENERGY_H"] = e_ax[e][1] 466 | header["DATAMIN"] = d_min[e] 467 | header["DATAMAX"] = d_max[e] 468 | key = f"{int(header['ENERGY_L'])}-{int(header['ENERGY_H'])} keV" 469 | map_list = [] 470 | for t in range(t_ax.shape[0]): 471 | header["DATE_OBS"] = parse_time(t_ax[t][0], format="utime").to_value("isot") 472 | header["DATE_END"] = parse_time(t_ax[t][1], format="utime").to_value("isot") 473 | map_list.append(Map(data[t][e], header)) # extract image Map 474 | maps[key] = Map(map_list, sequence=True) 475 | return maps 476 | -------------------------------------------------------------------------------- /sunkit_instruments/rhessi/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunpy/sunkit-instruments/590dd43244c63673d2f0f9e93022698d0791e179/sunkit_instruments/rhessi/tests/__init__.py -------------------------------------------------------------------------------- /sunkit_instruments/rhessi/tests/test_rhessi.py: -------------------------------------------------------------------------------- 1 | import textwrap 2 | from unittest import mock 3 | 4 | import numpy as np 5 | import pytest 6 | 7 | import sunpy.map 8 | from sunpy.io._file_tools import read_file 9 | from sunpy.time import is_time_equal, parse_time 10 | 11 | from sunkit_instruments import rhessi 12 | from sunkit_instruments.data.test import get_test_filepath 13 | 14 | 15 | @pytest.fixture 16 | def cross_month_timerange(): 17 | """ 18 | Time range which crosses a month boundary. 19 | 20 | Dbase files are monthly therefore this is to make sure that two 21 | dbase files are returned. 22 | """ 23 | return sunpy.time.TimeRange(("2016/01/25", "2016/02/05")) 24 | 25 | 26 | def test_backprojection(): 27 | """ 28 | Test that backprojection returns a map with the expected time. 29 | """ 30 | test_filename = "hsi_calib_ev_20020220_1106_20020220_1106_25_40.fits" 31 | amap = rhessi.backprojection(get_test_filepath(test_filename)) 32 | assert isinstance(amap, sunpy.map.GenericMap) 33 | assert is_time_equal(amap.date, parse_time((2002, 2, 20, 11, 6, 21))) 34 | 35 | 36 | def test_parse_obssum_dbase_file(): 37 | fname = get_test_filepath("hsi_obssumm_filedb_201104.txt") 38 | obssum = rhessi.parse_observing_summary_dbase_file(fname) 39 | assert obssum["filename"][0] == "hsi_obssumm_20110401_043.fit" 40 | assert obssum["filename"][-1] == "hsi_obssumm_20110430_029.fit" 41 | 42 | assert obssum["orb_st"][0] == 0 43 | assert obssum["orb_st"][-1] == 0 44 | 45 | assert obssum["orb_end"][0] == 0 46 | assert obssum["orb_end"][-1] == 0 47 | 48 | assert obssum["start_time"][0] == parse_time((2011, 4, 1, 0, 0, 0)) 49 | assert obssum["start_time"][-1] == parse_time((2011, 4, 30, 0, 0, 0)) 50 | 51 | assert obssum["end_time"][0] == parse_time((2011, 4, 2, 0, 0, 0)) 52 | assert obssum["end_time"][-1] == parse_time((2011, 5, 1, 0, 0, 0)) 53 | 54 | assert obssum["status_flag"][0] == 0 55 | assert obssum["status_flag"][-1] == 0 56 | 57 | assert obssum["npackets"][0] == 0 58 | assert obssum["npackets"][-1] == 0 59 | 60 | 61 | def test_parse_observing_summary_dbase_file(): 62 | """ 63 | Test that we get the observing summary database file with the content we 64 | expect. 65 | """ 66 | obssum = rhessi.parse_observing_summary_dbase_file( 67 | get_test_filepath("hsi_obssumm_filedb_201104.txt") 68 | ) 69 | 70 | assert obssum["filename"][0][0:20] == "hsi_obssumm_20110401" 71 | assert obssum["filename"][1][0:20] == "hsi_obssumm_20110402" 72 | 73 | assert obssum["orb_st"][0] == 0 74 | assert obssum["orb_st"][-1] == 0 75 | 76 | assert obssum["orb_end"][0] == 0 77 | assert obssum["orb_end"][-1] == 0 78 | 79 | assert obssum["start_time"][0] == parse_time((2011, 4, 1, 0, 0, 0)) 80 | assert obssum["start_time"][-1] == parse_time((2011, 4, 30, 0, 0, 0)) 81 | 82 | assert obssum["end_time"][0] == parse_time((2011, 4, 2, 0, 0, 0)) 83 | assert obssum["end_time"][-1] == parse_time((2011, 5, 1, 0, 0, 0)) 84 | 85 | assert obssum["status_flag"][0] == 0 86 | assert obssum["status_flag"][-1] == 0 87 | 88 | assert obssum["npackets"][0] == 0 89 | assert obssum["npackets"][-1] == 0 90 | 91 | 92 | def test_get_parse_obssum_hdulist(): 93 | hdulist = read_file(get_test_filepath("hsi_obssumm_20110404_042.fits.gz")) 94 | header, _data = rhessi.parse_observing_summary_hdulist(hdulist) 95 | assert header.get("DATE_OBS") == "2011-04-04T00:00:00.000" 96 | assert header.get("DATE_END") == "2011-04-05T00:00:00.000" 97 | assert header.get("TELESCOP") == "HESSI" 98 | 99 | 100 | def test_uncompress_countrate(): 101 | """ 102 | Test that function fails if given uncompressed counts out of range. 103 | """ 104 | # Should only accept bytearr (uncompressed counts must be 0 - 255) 105 | with pytest.raises(ValueError): 106 | rhessi.uncompress_countrate(np.array([-1, 300])) 107 | 108 | counts = rhessi.uncompress_countrate(np.array([0, 128, 255])) 109 | 110 | # Valid min, max 111 | assert counts[0] == 0 112 | assert counts[2] == 1015792 113 | 114 | # Random test value 115 | assert counts[1] == 4080 116 | 117 | 118 | # Test `rhessi.parse_obssumm_dbase_file(...)` 119 | 120 | 121 | def hessi_data(): 122 | return textwrap.dedent( 123 | """\ 124 | HESSI Filedb File: 125 | Created: 1972-04-14T12:41:26.000 126 | Number of Files: 2 127 | Filename Orb_st Orb_end Start_time End_time Status_flag Npackets Drift_start Drift_end Data source 128 | hsi_obssumm_19721101_139.fit 7 8 01-Nov-72 00:00:00 02-Nov-72 00:00:00 3 2 0.000 0.000 129 | hsi_obssumm_19721102_144.fit 9 10 02-Nov-72 00:00:00 03-Nov-72 00:00:00 4 1 0.000 0.000 130 | """ 131 | ) 132 | 133 | 134 | def test_parse_observing_summary_dbase_file_mock(): 135 | """ 136 | Ensure that all required data are extracted from the RHESSI observing 137 | summary database file mocked in ``hessi_data()``. 138 | """ 139 | mock_file = mock.mock_open(read_data=hessi_data()) 140 | 141 | dbase_data = {} 142 | with mock.patch("sunkit_instruments.rhessi.rhessi.open", mock_file, create=True): 143 | dbase_data = rhessi.parse_observing_summary_dbase_file(None) 144 | 145 | assert len(dbase_data.keys()) == 7 146 | 147 | # verify each of the 7 fields 148 | assert dbase_data["filename"] == [ 149 | "hsi_obssumm_19721101_139.fit", 150 | "hsi_obssumm_19721102_144.fit", 151 | ] 152 | assert dbase_data["orb_st"] == [7, 9] 153 | assert dbase_data["orb_end"] == [8, 10] 154 | assert dbase_data["start_time"] == [ 155 | parse_time((1972, 11, 1, 0, 0)), 156 | parse_time((1972, 11, 2, 0, 0)), 157 | ] 158 | assert dbase_data["end_time"] == [ 159 | parse_time((1972, 11, 2, 0, 0)), 160 | parse_time((1972, 11, 3, 0, 0)), 161 | ] 162 | assert dbase_data["status_flag"] == [3, 4] 163 | assert dbase_data["npackets"] == [2, 1] 164 | 165 | 166 | # Test `rhessi._build_energy_bands(...)` 167 | 168 | 169 | @pytest.fixture 170 | def raw_bands(): 171 | """ 172 | The RHESSI summary data standard energy bands. 173 | """ 174 | return [ 175 | "3 - 6", 176 | "6 - 12", 177 | "12 - 25", 178 | "25 - 50", 179 | "50 - 100", 180 | "100 - 300", 181 | "300 - 800", 182 | "800 - 7000", 183 | "7000 - 20000", 184 | ] 185 | 186 | 187 | def test_build_energy_bands_no_match(raw_bands): 188 | """ 189 | If an energy unit cannot be found in the ``label`` then raise a 190 | `ValueError` 191 | """ 192 | with pytest.raises(ValueError): 193 | rhessi._build_energy_bands(label="Energy bands GHz", bands=raw_bands) 194 | 195 | 196 | def test_build_energy_bands(raw_bands): 197 | """ 198 | Success case. 199 | """ 200 | built_ranges = rhessi._build_energy_bands( 201 | label="Energy bands (keV)", bands=raw_bands 202 | ) 203 | 204 | assert built_ranges == [ 205 | "3 - 6 keV", 206 | "6 - 12 keV", 207 | "12 - 25 keV", 208 | "25 - 50 keV", 209 | "50 - 100 keV", 210 | "100 - 300 keV", 211 | "300 - 800 keV", 212 | "800 - 7000 keV", 213 | "7000 - 20000 keV", 214 | ] 215 | 216 | 217 | def test_imagecube2map(): 218 | fname = get_test_filepath("hsi_imagecube_clean_20151214_2255_2tx2e.fits") 219 | maps = rhessi.imagecube2map(fname) 220 | 221 | assert list(maps.keys()) == ["3-6 keV", "6-12 keV"] 222 | assert len(maps["3-6 keV"]) == 2 223 | assert len(maps["6-12 keV"]) == 2 224 | assert isinstance(maps["3-6 keV"], sunpy.map.MapSequence) 225 | assert isinstance(maps["6-12 keV"], sunpy.map.MapSequence) 226 | assert maps["3-6 keV"][0].fits_header["DATAMIN"] == pytest.approx(0.0, abs=1e-4) 227 | assert maps["3-6 keV"][1].fits_header["DATAMIN"] == pytest.approx(0.0, abs=1e-4) 228 | assert maps["3-6 keV"][0].fits_header["DATAMAX"] == pytest.approx(0.0, abs=1e-4) 229 | assert maps["3-6 keV"][1].fits_header["DATAMAX"] == pytest.approx(0.0, abs=1e-4) 230 | assert maps["6-12 keV"][0].fits_header["DATAMIN"] == pytest.approx( 231 | -0.00765, abs=1e-4 232 | ) 233 | assert maps["6-12 keV"][1].fits_header["DATAMIN"] == pytest.approx( 234 | -0.00765, abs=1e-4 235 | ) 236 | assert maps["6-12 keV"][0].fits_header["DATAMAX"] == pytest.approx(0.1157, abs=1e-4) 237 | assert maps["6-12 keV"][1].fits_header["DATAMAX"] == pytest.approx(0.1157, abs=1e-4) 238 | 239 | 240 | def test_imagecube2map_edgecase(): 241 | fname = get_test_filepath("hsi_imagecube_clean_20150930_1307_1tx1e.fits") 242 | maps = rhessi.imagecube2map(fname) 243 | 244 | assert list(maps.keys()) == ["6-12 keV"] 245 | assert len(maps["6-12 keV"]) == 1 246 | assert isinstance(maps["6-12 keV"], sunpy.map.MapSequence) 247 | assert maps["6-12 keV"][0].fits_header["DATAMIN"] == pytest.approx( 248 | -0.0835, abs=1e-4 249 | ) 250 | assert maps["6-12 keV"][0].fits_header["DATAMAX"] == pytest.approx(1.9085, abs=1e-4) 251 | 252 | 253 | def test_imagecube2map_nonrhessi(): 254 | fname = get_test_filepath("go1520110607.fits") 255 | with pytest.raises(ValueError, match="Expected a RHESSI datacube*"): 256 | rhessi.imagecube2map(fname) 257 | -------------------------------------------------------------------------------- /sunkit_instruments/suvi/__init__.py: -------------------------------------------------------------------------------- 1 | from .io import * # NOQA 2 | from .suvi import * # NOQA 3 | -------------------------------------------------------------------------------- /sunkit_instruments/suvi/_variables.py: -------------------------------------------------------------------------------- 1 | # Allow all possible file extensions for FITS and netCDF 2 | FITS_FILE_EXTENSIONS = ( 3 | ".fits", 4 | ".fts", 5 | ".fits.gz", 6 | "fts.gz", 7 | ) 8 | NETCDF_FILE_EXTENSIONS = ( 9 | ".nc", 10 | ".nc.gz", 11 | ".cdf", 12 | ".cdf.gz", 13 | ) 14 | # Naming scheme for SUVI files 15 | COMPOSITE_MATCHES = [ 16 | "-l2-ci094", 17 | "-l2-ci131", 18 | "-l2-ci171", 19 | "-l2-ci195", 20 | "-l2-ci284", 21 | "-l2-ci304", 22 | ] 23 | L1B_MATCHES = [ 24 | "-L1b-Fe093", 25 | "-L1b-Fe131", 26 | "-L1b-Fe171", 27 | "-L1b-Fe195", 28 | "-L1b-Fe284", 29 | "-L1b-He303", 30 | ] 31 | VALID_WAVELENGTH_CHANNELS = [94, 131, 171, 195, 284, 304] 32 | VALID_SPACECRAFT = [16, 17, 18, 19] 33 | FLIGHT_MODEL = {16: "FM1", 17: "FM2", 18: "FM3", 19: "FM4"} 34 | # The setup for filter wheel 1 and 2 for the different exposure types 35 | FILTER_SETUP = { 36 | 94: { 37 | "long": {"FW1": "thin_zirconium", "FW2": "open"}, 38 | "short": {"FW1": "thin_zirconium", "FW2": "open"}, 39 | "short_flare": {"FW1": "thin_zirconium", "FW2": "thin_zirconium"}, 40 | }, 41 | 131: { 42 | "long": {"FW1": "thin_zirconium", "FW2": "open"}, 43 | "short": {"FW1": "thin_zirconium", "FW2": "open"}, 44 | "short_flare": {"FW1": "thin_zirconium", "FW2": "thin_zirconium"}, 45 | }, 46 | 171: { 47 | "long": {"FW1": "thin_aluminum", "FW2": "open"}, 48 | "short_flare": {"FW1": "thin_aluminum", "FW2": "thin_aluminum"}, 49 | }, 50 | 195: { 51 | "long": {"FW1": "thin_aluminum", "FW2": "open"}, 52 | "short_flare": {"FW1": "thin_aluminum", "FW2": "thin_aluminum"}, 53 | }, 54 | 284: { 55 | "long": {"FW1": "thin_aluminum", "FW2": "open"}, 56 | "short_flare": {"FW1": "thin_aluminum", "FW2": "thin_aluminum"}, 57 | }, 58 | 304: { 59 | "long": {"FW1": "thin_aluminum", "FW2": "open"}, 60 | "short_flare": {"FW1": "thin_aluminum", "FW2": "thin_aluminum"}, 61 | }, 62 | } 63 | # This is how the global attributes of the netCDF file get 64 | # mapped to the corresponding FITS header keywords. 65 | TAG_MAPPING = { 66 | "instrument_id": "INST_ID", 67 | "platform_ID": "TELESCOP", 68 | "instrument_type": "INSTRUME", 69 | "project": "PROJECT", 70 | "institution": "ORIGIN", 71 | "production_site": "PRODSITE", 72 | "naming_authority": "NAMEAUTH", 73 | "production_environment": "PROD_ENV", 74 | "production_data_source": "DATA_SRC", 75 | "processing_level": "LEVEL", 76 | "algorithm_version": "CREATOR", 77 | "title": "TITLE", 78 | "keywords_vocabulary": "KEYVOCAB", 79 | "date_created": "DATE", 80 | "orbital_slot": "ORB_SLOT", 81 | "dataset_name": "FILENAME", 82 | "iso_series_metadata_id": "ISO_META", 83 | "id": "UUID", 84 | "LUT_Filenames": "LUT_NAME", 85 | "license": "LICENSE", 86 | "keywords": "KEYWORDS", 87 | "summary": "SUMMARY", 88 | } 89 | # Mapping for the FITS header keywords and their comments 90 | TAG_COMMENT_MAPPING = { 91 | "SIMPLE": "file does conform to FITS standard", 92 | "BITPIX": "number of bits per data pixel", 93 | "NAXIS": "number of data axes", 94 | "NAXIS1": "length of data axis 1", 95 | "NAXIS2": "length of data axis 2", 96 | "EXTEND": "FITS dataset may contain extensions", 97 | "IMSENUMB": "[1] Image Serial Number", 98 | "CRPIX1": "[1] center of sun pixel in image along 1st axis", 99 | "CRPIX2": "[1] center of sun pixel in image along 2nd axis", 100 | "CDELT1": "[arcsec] 1st axis detector plate scale @ref pix", 101 | "CDELT2": "[arcsec] 2nd axis detector plate scale @ref pix", 102 | "DIAM_SUN": "[count] sun diameter in pixels", 103 | "CUNIT1": "1st axis detector plate scale units", 104 | "CUNIT2": "2nd axis detector plate scale units", 105 | "ORIENT": "orientation of image", 106 | "CROTA": "[degree] solar north pole angular offset", 107 | "SOLAR_B0": "[degree] solar equator angular offset", 108 | "PC1_1": "[1] 1st row, 1st col 2D transformation matrix", 109 | "PC1_2": "[1] 1st row, 2nd col 2D transformation matrix", 110 | "PC2_1": "[1] 2nd row, 1st col 2D transformation matrix", 111 | "PC2_2": "[1] 2nd row, 2nd col 2D transformation matrix", 112 | "CSYER1": "[arcsec] 1st axis systematic errors", 113 | "CSYER2": "[arcsec] 2nd axis systematic errors", 114 | "WCSNAME": "solar image coordinate system type", 115 | "CTYPE1": "1st axis coordinate system name", 116 | "CTYPE2": "2nd axis coordinate system name", 117 | "CRVAL1": "[degree] longitude of sun center for HPLN-TAN", 118 | "CRVAL2": "[degree] latitude of sun center for HPLT-TAN", 119 | "LONPOLE": "[degree] longitude of celestial north pole", 120 | "TIMESYS": "principal time system", 121 | "DATE-OBS": "sun observation start time on sat", 122 | "DATE-END": "sun observation end time on sat", 123 | "CMD_EXP": "[s] commanded imaging exposure time", 124 | "EXPTIME": "[s] actual imaging exposure time", 125 | "OBSGEO-X": "[m] observing platform ECEF X coordinate", 126 | "OBSGEO-Y": "[m] observing platform ECEF Y coordinate", 127 | "OBSGEO-Z": "[m] observing platform ECEF Z coordinate", 128 | "DSUN_OBS": "[m] distance to center of sun", 129 | "OBJECT": "object being viewed", 130 | "SCI_OBJ": "science objective of observation", 131 | "WAVEUNIT": "solar image wavelength units", 132 | "WAVELNTH": "[angstrom] solar image wavelength", 133 | "IMG_MIN": "[W m-2 sr-1] minimum radiance in image", 134 | "IMG_MAX": "[W m-2 sr-1] maximum radiance in image", 135 | "IMG_MEAN": "[W m-2 sr-1] mean radiance in image", 136 | "FILTER1": "forward filter setting mnemonic", 137 | "FILTER2": "aft filter setting mnemonic", 138 | "GOOD_PIX": "[count] number of good quality pixels in image", 139 | "FIX_PIX": "[count] number of corrected pixels in image", 140 | "SAT_PIX": "[count] number of saturated pixels in image", 141 | "MISS_PIX": "[count] number of missing pixels in image", 142 | "IMGTII": "[W m-2] total irradiance of image", 143 | "IMGTIR": "[W m-2 sr-1] total radiance of image", 144 | "IMG_SDEV": "[W m-2 sr-1] std dev of radiance in image", 145 | "EFF_AREA": "[m2] effective telescope area", 146 | "APSELPOS": "[1] aperture selector setting", 147 | "INSTRESP": "[count photon-1 cm-2] instrument response, used", 148 | "PHOT_ENG": "[J] photon energy, used in the calculation of r", 149 | "RSUN": "[count] solar angular radius in pixels", 150 | "HGLT_OBS": "[degree] Heliographic Stonyhurst Latitude of th", 151 | "HGLN_OBS": "[degree] Heliographic Stonyhurst Longitude of t", 152 | "HEEX_OBS": "[m] Heliocentric Earth Ecliptic X-axis coordina", 153 | "HEEY_OBS": "[m] Heliocentric Earth Ecliptic Y-axis coordina", 154 | "HEEZ_OBS": "[m] Heliocentric Earth Ecliptic Z-axis coordina", 155 | "FILTPOS1": "[1] forward filter wheel setting", 156 | "FILTPOS2": "[1] aft filter wheel setting", 157 | "YAW_FLIP": "[1] 0=upright 1=neither 2=inverted", 158 | "CCD_READ": "[1] CCD cnfg: 0=no cnfg 1=left amp 2=right amp", 159 | "ECLIPSE": "[1] sun obscured: 0=no eclipse 1=penumbra,prece", 160 | "CONTAMIN": "[angstrom] contamination thickness in angstroms", 161 | "CONT_FLG": "[1] contamination correction: 0=true 1=false", 162 | "DATE-BKE": "last contamination bake-out end time", 163 | "DER_SNR": "[W m-2 sr-1] CCD signal to noise ratio", 164 | "SAT_THR": "[W m-2 sr-1] CCD saturation point", 165 | "CCD_BIAS": "[count] CCD background electronic noise", 166 | "CCD_TMP1": "[degrees_C] sensor 1 camera temperature", 167 | "CCD_TMP2": "[degrees_C] sensor 2 camera temperature", 168 | "DATE-DFM": "median value dark frame time stamp", 169 | "NDFRAMES": "[count] number of source dark frames", 170 | "DATE-DF0": "1st observed dark frame time stamp", 171 | "DATE-DF1": "2nd observed dark frame time stamp", 172 | "DATE-DF2": "3rd observed dark frame time stamp", 173 | "DATE-DF3": "4th observed dark frame time stamp", 174 | "DATE-DF4": "5th observed dark frame time stamp", 175 | "DATE-DF5": "6th observed dark frame time stamp", 176 | "DATE-DF6": "7th observed dark frame time stamp", 177 | "DATE-DF7": "8th observed dark frame time stamp", 178 | "DATE-DF8": "9th observed dark frame time stamp", 179 | "DATE-DF9": "10th observed dark frame time stamp", 180 | "SOLCURR1": "[count] solar array current chan 1-4 in DN", 181 | "SOLCURR2": "[count] solar array current chan 5-8 in DN", 182 | "SOLCURR3": "[count] solar array current chan 9-12 in DN", 183 | "SOLCURR4": "[count] solar array current chan 13-16 in DN", 184 | "PCTL0ERR": "[percent] uncorrectable L0 error pct", 185 | "LONGSTRN": "The HEASARC Long String Convention may be used", 186 | } 187 | SOLAR_CLASSES = [ 188 | ("unlabeled", 0), 189 | ("outer_space", 1), 190 | ("bright_region", 3), 191 | ("filament", 4), 192 | ("prominence", 5), 193 | ("coronal_hole", 6), 194 | ("quiet_sun", 7), 195 | ("limb", 8), 196 | ("flare", 9), 197 | ] 198 | SOLAR_CLASS_NAME = {number: theme for theme, number in SOLAR_CLASSES} 199 | SOLAR_COLORS = { 200 | "unlabeled": "white", 201 | "outer_space": "black", 202 | "bright_region": "#F0E442", 203 | "filament": "#D55E00", 204 | "prominence": "#E69F00", 205 | "coronal_hole": "#009E73", 206 | "quiet_sun": "#0072B2", 207 | "limb": "#56B4E9", 208 | "flare": "#CC79A7", 209 | } 210 | -------------------------------------------------------------------------------- /sunkit_instruments/suvi/data/SUVI_FM1_gain.txt: -------------------------------------------------------------------------------- 1 | ; Gain Curve for GOES-16 SUVI FM1 2 | ; This file contains SUVI FM1 Gain as a function of Temperature (C) 3 | ; 4 | ; Source: GOES-16 SUVI LUT ("SUVI_CalibrationParameters(FM1A_ADR1340_DO_14_00_00)-770770136.0.h5") 5 | ; File Generated 24-Jan-2025, by cb 6 | ;------------------------------------------------------- 7 | ; Temperature [C] Gain [e- per DN] 8 | -89.256900000000002 35.413348810000002 9 | -88.329300000000003 35.458729409999997 10 | -87.400899999999993 35.504149159999997 11 | -86.471500000000006 35.549617830000003 12 | -85.540999999999997 35.595140309999998 13 | -84.609800000000007 35.640697039999999 14 | -83.677400000000006 35.686312469999997 15 | -82.744100000000003 35.731971940000001 16 | -81.809799999999996 35.777680330000003 17 | -80.874499999999998 35.823437640000002 18 | -79.938199999999995 35.869243869999998 19 | -79.001000000000005 35.915094130000000 20 | -78.062799999999996 35.960993320000000 21 | -77.123699999999999 36.006936539999998 22 | -76.183400000000006 36.052938460000000 23 | -75.242199999999997 36.098984420000001 24 | -74.299899999999994 36.145084179999998 25 | -73.356600000000000 36.191232880000001 26 | -72.412300000000002 36.237430490000001 27 | -71.467100000000002 36.283672140000000 28 | -70.520799999999994 36.329967600000003 29 | -69.573400000000007 36.376316869999997 30 | -68.624899999999997 36.422719960000002 31 | -67.675399999999996 36.469171969999998 32 | -66.724900000000005 36.515672899999998 33 | -65.773300000000006 36.562227649999997 34 | -64.820700000000002 36.608831330000001 35 | -63.866999999999997 36.655488810000001 36 | -62.912399999999998 36.702190330000001 37 | -61.956499999999998 36.748955449999997 38 | -60.999600000000001 36.795769489999998 39 | -60.041600000000003 36.842637340000003 40 | -59.082400000000000 36.889563899999999 41 | -58.122300000000003 36.936534490000000 42 | -57.161000000000001 36.983563789999998 43 | -56.198700000000002 37.030642010000001 44 | -55.235199999999999 37.077778940000002 45 | -54.270800000000001 37.124959900000000 46 | -53.305100000000003 37.172204460000003 47 | -52.338299999999997 37.219502830000003 48 | -51.370399999999997 37.266855020000001 49 | -50.401200000000003 37.314270800000003 50 | -49.431199999999997 37.361725730000003 51 | -48.459800000000001 37.409249150000001 52 | -47.487400000000001 37.456821490000003 53 | -46.514000000000003 37.504442750000003 54 | -45.539099999999998 37.552137389999999 55 | -44.563200000000002 37.599880960000000 56 | -43.586199999999998 37.647678350000000 57 | -42.607999999999997 37.695534440000003 58 | -41.628500000000003 37.743454130000003 59 | -40.648099999999999 37.791417850000002 60 | -39.666400000000003 37.839445169999998 61 | -38.683500000000002 37.887531189999997 62 | -37.699500000000000 37.935671030000002 63 | -36.714199999999998 37.983874470000003 64 | -35.727699999999999 38.032136620000003 65 | -34.740099999999998 38.080452590000000 66 | -33.751199999999997 38.128832150000001 67 | -32.761099999999999 38.177270419999999 68 | -31.769900000000000 38.225762500000002 69 | -------------------------------------------------------------------------------- /sunkit_instruments/suvi/data/SUVI_FM2_gain.txt: -------------------------------------------------------------------------------- 1 | ; Gain Curve for GOES-17 SUVI FM2 2 | ; This file contains SUVI FM2 Gain as a function of Temperature (C) 3 | ; 4 | ; Source: GOES-17 SUVI LUT ("SUVI_CalibrationParameters(FM2A_CDRL79Rev-_PR_14_00_01)-784348400.0.h5") 5 | ; File Generated 24-Jan-2025, by cb 6 | ;------------------------------------------------------- 7 | ; Temperature [C] Gain [e- per DN] 8 | -90.719999999999999 36.268599999999999 9 | -89.624666666666670 36.322777777777773 10 | -88.529333333333327 36.376979654320984 11 | -87.433999999999997 36.431231664751344 12 | -86.338666666666668 36.485314386630549 13 | -85.243333333333339 36.539375886524823 14 | -84.147999999999996 36.593443404255318 15 | -83.052666666666667 36.648027819007488 16 | -81.957333333333338 36.702072056737592 17 | -80.861999999999995 36.756139574468087 18 | -79.766666666666666 36.810207092198581 19 | -78.671333333333337 36.864274609929083 20 | -77.575999999999993 36.918792129497156 21 | -76.480666666666664 36.972903262411350 22 | -75.385333333333335 37.026965436081127 23 | -74.289999999999992 37.080977936239904 24 | -73.194666666666663 37.135108593749145 25 | -72.099333333333334 37.189440828117576 26 | -71.004000000000005 37.243627981187018 27 | -69.908666666666662 37.297702945600932 28 | -68.813333333333333 37.351685990389690 29 | -67.718000000000004 37.406156930368475 30 | -66.622666666666674 37.460035895450908 31 | -65.527333333333331 37.514144464105371 32 | -64.432000000000002 37.568425825371946 33 | -63.336666666666666 37.622764599323368 34 | -62.241333333333330 37.676687208224074 35 | -61.146000000000001 37.730714962470003 36 | -60.050666666666665 37.785160748272574 37 | -58.955333333333328 37.839265037553695 38 | -57.859999999999999 37.893507943617905 39 | -56.764666666666663 37.947366808593415 40 | -55.669333333333334 38.001828251996422 41 | -54.573999999999998 38.055775493850419 42 | -53.478666666666669 38.109738469948965 43 | -52.383333333333333 38.164159312228563 44 | -51.288000000000004 38.218315493243750 45 | -50.192666666666668 38.272550813879818 46 | -49.097333333333331 38.326753480951268 47 | -48.001999999999995 38.380614952920915 48 | -46.906666666666666 38.434977544318912 49 | -45.811333333333330 38.488788907417096 50 | -44.716000000000001 38.543269263157896 51 | -43.620666666666665 38.597415238890413 52 | -42.525333333333336 38.651456229957752 53 | -41.430000000000000 38.705851434903053 54 | -40.334666666666664 38.759750943649408 55 | -39.239333333333335 38.814051117292344 56 | -38.143999999999998 38.868215037224218 57 | -37.048666666666669 38.922339535186545 58 | -35.953333333333333 38.976435847727664 59 | -34.858000000000004 39.030417491252749 60 | -33.762666666666668 39.084819158576053 61 | -32.667333333333332 39.138879881051821 62 | -31.571999999999996 39.192958117450111 63 | -30.476666666666667 39.247346925566340 64 | -29.381333333333330 39.301295287148378 65 | -28.286000000000001 39.355514689064229 66 | -27.190666666666665 39.409777682135719 67 | -26.095333333333343 39.463611918838488 68 | -25.000000000000000 39.518000000000001 69 | -------------------------------------------------------------------------------- /sunkit_instruments/suvi/data/SUVI_FM3_gain.txt: -------------------------------------------------------------------------------- 1 | ; Gain Curve for GOES-18 SUVI FM3 2 | ; This file contains SUVI FM3 Gain as a function of Temperature (C) 3 | ; 4 | ; Source: GOES-18 SUVI LUT ("SUVI_CalibrationParameters(FM3A_CDRL79RevBv2_PR_14_00_01)-784348400.0.h5") 5 | ; File Generated 24-Jan-2025, by cb 6 | ;------------------------------------------------------- 7 | ; Temperature [C] Gain [e- per DN] 8 | -82.169230769230694 35.811930697007718 9 | -81.223076923076860 35.854537453979972 10 | -80.276923076923026 35.897144210952227 11 | -79.330769230769192 35.939750967924482 12 | -78.384615384615358 35.982357724896730 13 | -77.438461538461524 36.024964481868984 14 | -76.492307692307691 36.067571238841239 15 | -75.546153846153857 36.110177995813494 16 | -74.600000000000023 36.152784752785749 17 | -73.653846153846189 36.195391509758004 18 | -72.707692307692241 36.237998266730266 19 | -71.761538461538407 36.280605023702513 20 | -70.815384615384573 36.323211780674768 21 | -69.869230769230739 36.365818537647023 22 | -68.923076923076906 36.408425294619278 23 | -67.976923076923072 36.451032051591532 24 | -67.030769230769238 36.493638808563787 25 | -66.084615384615404 36.536245565536042 26 | -65.138461538461570 36.578852322508297 27 | -64.192307692307736 36.621459079480552 28 | -63.246153846153788 36.664065836452806 29 | -62.299999999999955 36.706672593425061 30 | -61.346153846153811 36.749625746795466 31 | -60.392307692307668 36.792578900165864 32 | -59.438461538461524 36.835532053536269 33 | -58.484615384615381 36.878485206906674 34 | -57.530769230769238 36.921438360277072 35 | -56.576923076923094 36.964391513647477 36 | -55.623076923076951 37.007344667017875 37 | -54.669230769230808 37.050297820388280 38 | -53.715384615384551 37.093250973758686 39 | -52.761538461538407 37.136204127129091 40 | -51.807692307692264 37.179157280499496 41 | -50.853846153846121 37.222110433869894 42 | -49.899999999999977 37.265063587240299 43 | -48.946153846153834 37.308016740610704 44 | -47.992307692307691 37.350969893981102 45 | -47.038461538461547 37.393923047351507 46 | -46.084615384615404 37.436876200721912 47 | -45.130769230769261 37.479829354092310 48 | -44.176923076923117 37.522782507462715 49 | -43.223076923076974 37.565735660833120 50 | -42.269230769230717 37.608688814203525 51 | -41.315384615384573 37.651641967573930 52 | -40.361538461538430 37.694595120944328 53 | -39.407692307692287 37.737548274314733 54 | -38.453846153846143 37.780501427685131 55 | -37.500000000000000 37.823454581055536 56 | -36.538461538461547 37.866754130824084 57 | -35.576923076922981 37.910053680592647 58 | -34.615384615384528 37.953353230361195 59 | -33.653846153846075 37.996652780129743 60 | -32.692307692307622 38.039952329898298 61 | -31.730769230769170 38.083251879666847 62 | -30.769230769230717 38.126551429435402 63 | -29.807692307692264 38.169850979203950 64 | -28.846153846153811 38.213150528972498 65 | -27.884615384615358 38.256450078741054 66 | -26.923076923076906 38.299749628509602 67 | -25.961538461538453 38.343049178278150 68 | -25.000000000000000 38.386348728046706 69 | -------------------------------------------------------------------------------- /sunkit_instruments/suvi/data/SUVI_FM4_gain.txt: -------------------------------------------------------------------------------- 1 | ; Gain Curve for GOES-19 SUVI FM4 2 | ; This file contains SUVI FM4 Gain as a function of Temperature (C) 3 | ; 4 | ; Source: GOES-19 SUVI LUT ("SUVI_CalibrationParameters(FM4A_CDRL79revC_ADR1532)-790407900.0.h5") 5 | ; File Generated 24-Jan-2025, by cb 6 | ;------------------------------------------------------- 7 | ; Temperature [C] Gain [e- per DN] 8 | -84.000000000000000 36.290798276275787 9 | -82.966666666666583 36.324213994994544 10 | -81.933333333333280 36.358221641336158 11 | -80.899999999999977 36.392821215300629 12 | -79.866666666666561 36.428012716887949 13 | -78.833333333333258 36.463796146098126 14 | -77.799999999999955 36.500171502931146 15 | -76.766666666666652 36.537138787387029 16 | -73.746153846153788 36.648590955361882 17 | -72.792307692307645 36.684837135921448 18 | -71.838461538461502 36.721587680846049 19 | -70.884615384615358 36.758842590135686 20 | -69.930769230769215 36.796601863790357 21 | -68.976923076923072 36.834865501810064 22 | -68.023076923076928 36.873633504194807 23 | -67.069230769230785 36.912905870944584 24 | -66.115384615384642 36.952682602059397 25 | -65.161538461538498 36.992963697539238 26 | -64.207692307692355 37.033749157384122 27 | -63.253846153846098 37.075038981594041 28 | -62.299999999999955 37.116833170168995 29 | -63.541666666666742 37.062526754388088 30 | -62.500000000000114 37.108028082749200 31 | -61.458333333333485 37.154130924449731 32 | -60.416666666666742 37.200835279489709 33 | -59.375000000000114 37.248141147869106 34 | -58.333333333333485 37.296048529587935 35 | -57.291666666666742 37.344557424646204 36 | -56.250000000000114 37.393667833043892 37 | -55.208333333333485 37.443379754781020 38 | -54.166666666666742 37.493693189857574 39 | -53.125000000000114 37.544608138273560 40 | -52.083333333333485 37.596124600028972 41 | -51.041666666666742 37.648242575123824 42 | -50.000000000000114 37.700962063558102 43 | -48.958333333333485 37.754283065331805 44 | -47.916666666666742 37.808205580444955 45 | -46.875000000000114 37.862729608897517 46 | -45.833333333333485 37.917855150689519 47 | -44.791666666666742 37.973582205820954 48 | -43.750000000000114 38.029910774291814 49 | -42.708333333333485 38.086840856102107 50 | -41.666666666666742 38.144372451251833 51 | -40.625000000000000 38.202505559740999 52 | -39.583333333333485 38.261240181569569 53 | -38.541666666666742 38.320576316737593 54 | -37.500000000000000 38.380513965245044 55 | -36.458333333333485 38.441053127091919 56 | -35.416666666666742 38.502193802278228 57 | -34.375000000000000 38.563935990803969 58 | -33.333333333333485 38.626279692669137 59 | -32.291666666666742 38.689224907873744 60 | -31.250000000000000 38.752771636417776 61 | -30.208333333333485 38.816919878301235 62 | -29.166666666666742 38.881669633524126 63 | -28.125000000000000 38.947020902086464 64 | -27.083333333333485 39.012973683988207 65 | -26.041666666666742 39.079527979229397 66 | -25.400000000000091 39.120824815129374 67 | -24.341666666666697 39.189436466480274 68 | -23.283333333333303 39.258669033584873 69 | -------------------------------------------------------------------------------- /sunkit_instruments/suvi/suvi.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import numpy as np 4 | from scipy import interpolate 5 | from scipy.ndimage import gaussian_filter 6 | 7 | from astropy import units as u 8 | 9 | import sunpy.map 10 | 11 | from sunkit_instruments.suvi._variables import ( 12 | FILTER_SETUP, 13 | FLIGHT_MODEL, 14 | VALID_SPACECRAFT, 15 | VALID_WAVELENGTH_CHANNELS, 16 | ) 17 | 18 | __all__ = [ 19 | "despike_l1b_file", 20 | "despike_l1b_array", 21 | "get_response", 22 | ] 23 | 24 | PATH_TO_FILES = Path(__file__).parent / "data" 25 | 26 | 27 | def _despike(image, dqf_mask, filter_width): 28 | """ 29 | Helper function to do the actual despiking. 30 | 31 | Parameters 32 | ---------- 33 | image : `numpy.ndarray` 34 | Array to despike. 35 | dqf_mask : `numpy.ndarray` 36 | Data quality flags array. 37 | filter_width: `int`, optional. 38 | The filter width for the Gaussian filter, default is 7. 39 | If NaNs are still present in the despiked image, try increasing this value. 40 | """ 41 | image_with_nans = np.copy(image) 42 | image_with_nans[np.where(dqf_mask == 4)] = np.nan 43 | indices = np.where(np.isnan(image_with_nans)) 44 | image_gaussian_filtered = gaussian_filter(image, filter_width) 45 | despiked_image = np.copy(image_with_nans) 46 | despiked_image[indices] = image_gaussian_filtered[indices] 47 | return despiked_image 48 | 49 | 50 | def despike_l1b_file(filename, filter_width=7): 51 | """ 52 | Despike SUVI L1b data and return a despiked `~sunpy.map.Map`. 53 | 54 | .. warning:: 55 | The despiking relies on the presence of the data quality 56 | flags (DQF) in the first extension of a SUVI L1b FITS file. 57 | Early in the mission, the DQF extension was not present 58 | yet, so the despiking cannot be done with this function 59 | for those early files. 60 | 61 | Parameters 62 | ---------- 63 | filename: `str` 64 | File to despike. 65 | filter_width: `int`, optional. 66 | The filter width for the Gaussian filter, default is 7. 67 | If NaNs are still present in the despiked image, try increasing this value. 68 | 69 | Returns 70 | ------- 71 | `~sunpy.map.Map` 72 | The despiked L1b image as a `~sunpy.map.Map`. 73 | """ 74 | # Avoid circular import 75 | from sunkit_instruments.suvi.io import read_suvi 76 | 77 | header, image, dqf_mask = read_suvi(filename) 78 | despiked_image = _despike(image, dqf_mask, filter_width) 79 | return sunpy.map.Map(despiked_image, header) 80 | 81 | 82 | def despike_l1b_array(data, dqf, filter_width=7): 83 | """ 84 | Despike SUVI L1b data and return a despiked `numpy.ndarray`. 85 | 86 | Parameters 87 | ---------- 88 | data : `numpy.ndarray` 89 | Array to despike. 90 | dqf : `numpy.ndarray` 91 | Data quality flags array. 92 | filter_width: `int`, optional. 93 | The filter width for the Gaussian filter, default is 7. 94 | If NaNs are still present in the despiked image, try increasing this value. 95 | 96 | Returns 97 | ------- 98 | `numpy.ndarray` 99 | The despiked L1b image as a numpy array. 100 | """ 101 | return _despike(data, dqf, filter_width) 102 | 103 | 104 | def get_response(request, spacecraft=16, ccd_temperature=-60.0, exposure_type="long"): 105 | """ 106 | Get the SUVI instrument response for a specific wavelength channel, 107 | spacecraft, CCD temperature, and exposure type. 108 | 109 | ``request`` can either be an L1b filename (FITS or netCDF), in which case all of those 110 | parameters are read automatically from the metadata, or the parameters 111 | can be passed manually, with ``request`` specifying the desired wavelength 112 | channel. 113 | 114 | Parameters 115 | ---------- 116 | request: `str` or `int`. 117 | Either an L1b filename (FITS or netCDF), or an integer 118 | specifying the wavelength channel. 119 | Those are the valid wavelength channels: 94, 131, 171, 195, 284, 304 120 | spacecraft: `int`, optional 121 | Which GOES spacecraft, default is 16. 122 | ccd_temperature: `float`, optional 123 | The CCD temperature, in degrees Celsius, default is -60. 124 | Needed for getting the correct gain number. 125 | exposure_type: `str`, optional 126 | The exposure type of the SUVI image. 127 | The exposure type is needed for the correct focal plane 128 | filter selection. 129 | 130 | Can be: 131 | * "long", "short", "short_flare" for 94 and 131 132 | * "long", "short_flare" for 171, 195, 284, and 304. 133 | 134 | Returns 135 | ------- 136 | `dict` 137 | The instrument response information. 138 | Keys: 139 | 140 | * "wavelength" 141 | * "effective_area" 142 | * "response" 143 | * "wavelength_channel" 144 | * "spacecraft" 145 | * "ccd_temperature" 146 | * "exposure_type" 147 | * "flight_model" 148 | * "gain" 149 | * "solid_angle" 150 | * "geometric_area" 151 | * "filter_setup" 152 | """ 153 | # Avoid circular import 154 | from sunkit_instruments.suvi.io import read_suvi 155 | 156 | if isinstance(request, str): 157 | header, _, _ = read_suvi(request) 158 | wavelength_channel = int(header["WAVELNTH"]) 159 | spacecraft = int(header["TELESCOP"].replace(" ", "").replace("G", "")) 160 | ccd_temperature = (header["CCD_TMP1"] + header["CCD_TMP2"]) / 2.0 161 | exposure_type = "_".join( 162 | header["SCI_OBJ"].replace(" ", "").split(sep="_")[3:] 163 | ).replace("_exposure", "") 164 | elif isinstance(request, int): 165 | wavelength_channel = request 166 | else: 167 | raise TypeError( 168 | f"Input not recognized, must be str for filename or int for wavelength channel, not {type(request)}" 169 | ) 170 | 171 | if wavelength_channel not in VALID_WAVELENGTH_CHANNELS: 172 | raise ValueError( 173 | f"Invalid wavelength channel: {wavelength_channel}" 174 | f"Valid wavelength channels are: {VALID_WAVELENGTH_CHANNELS}" 175 | ) 176 | if spacecraft not in VALID_SPACECRAFT: 177 | raise ValueError( 178 | f"Invalid spacecraft: {spacecraft} " 179 | f"Valid spacecraft are: {VALID_SPACECRAFT}" 180 | ) 181 | 182 | eff_area_file = ( 183 | PATH_TO_FILES 184 | / f"SUVI_{FLIGHT_MODEL[spacecraft]}_{wavelength_channel}A_eff_area.txt" 185 | ) 186 | gain_file = PATH_TO_FILES / f"SUVI_{FLIGHT_MODEL[spacecraft]}_gain.txt" 187 | 188 | eff_area = np.loadtxt(eff_area_file, skiprows=12) 189 | wave = eff_area[:, 0] * u.Angstrom 190 | if FILTER_SETUP[wavelength_channel][exposure_type]["FW2"] == "open": 191 | effective_area = eff_area[:, 1] * u.cm * u.cm 192 | else: 193 | effective_area = eff_area[:, 2] * u.cm * u.cm 194 | 195 | gain_table = np.loadtxt(gain_file, skiprows=7) 196 | temp_x = gain_table[:, 0] 197 | gain_y = gain_table[:, 1] 198 | gain_vs_temp = interpolate.interp1d(temp_x, gain_y) 199 | gain = gain_vs_temp(ccd_temperature) 200 | 201 | geometric_area = 19.362316 * u.cm * u.cm 202 | solid_angle = ((2.5 / 3600.0 * (np.pi / 180.0)) ** 2.0) * u.sr 203 | master_e_per_phot = ( 204 | (6.626068e-34 * (u.J / u.Hz)) * (2.99792458e8 * (u.m / u.s)) 205 | ) / (wave.to(u.m) * ((u.eV.to(u.J, 3.65)) * u.J)) 206 | response = effective_area * (master_e_per_phot / gain) 207 | 208 | response_info = { 209 | "wavelength": wave, 210 | "effective_area": effective_area * (u.ct / u.ph), 211 | "response": response, 212 | "wavelength_channel": wavelength_channel, 213 | "spacecraft": "GOES-" + str(spacecraft), 214 | "ccd_temperature": ccd_temperature * u.deg_C, 215 | "exposure_type": exposure_type, 216 | "flight_model": FLIGHT_MODEL[spacecraft], 217 | "gain": float(gain), 218 | "solid_angle": solid_angle, 219 | "geometric_area": geometric_area, 220 | "filter_setup": FILTER_SETUP[wavelength_channel][exposure_type], 221 | } 222 | return response_info 223 | -------------------------------------------------------------------------------- /sunkit_instruments/suvi/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunpy/sunkit-instruments/590dd43244c63673d2f0f9e93022698d0791e179/sunkit_instruments/suvi/tests/__init__.py -------------------------------------------------------------------------------- /sunkit_instruments/suvi/tests/conftest.py: -------------------------------------------------------------------------------- 1 | from tempfile import TemporaryDirectory 2 | 3 | import pytest 4 | from parfive import Downloader 5 | 6 | 7 | @pytest.fixture(scope="session") 8 | def L1B_FITS(): 9 | downloader = Downloader() 10 | url = "https://data.ngdc.noaa.gov/platforms/solar-space-observing-satellites/goes/goes16/l1b/suvi-l1b-fe171/2021/12/31/OR_SUVI-L1b-Fe171_G16_s20213650006108_e20213650006118_c20213650006321.fits.gz" 11 | with TemporaryDirectory() as d: 12 | downloader.enqueue_file(url, d) 13 | files = downloader.download() 14 | yield files[0] 15 | 16 | 17 | @pytest.fixture(scope="session") 18 | def L1B_NC(): 19 | downloader = Downloader() 20 | url = "https://noaa-goes16.s3.amazonaws.com/SUVI-L1b-Fe171/2021/365/00/OR_SUVI-L1b-Fe171_G16_s20213650022109_e20213650022119_c20213650022323.nc" 21 | with TemporaryDirectory() as d: 22 | downloader.enqueue_file(url, d) 23 | files = downloader.download() 24 | yield files[0] 25 | 26 | 27 | @pytest.fixture(scope="session") 28 | def L2_COMPOSITE(): 29 | downloader = Downloader() 30 | url = "https://data.ngdc.noaa.gov/platforms/solar-space-observing-satellites/goes/goes16/l2/data/suvi-l2-ci171/2021/12/31/dr_suvi-l2-ci171_g16_s20211231T000800Z_e20211231T001200Z_v1-0-1.fits" 31 | with TemporaryDirectory() as d: 32 | downloader.enqueue_file(url, d) 33 | files = downloader.download() 34 | yield files[0] 35 | -------------------------------------------------------------------------------- /sunkit_instruments/suvi/tests/test_io.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | 4 | from astropy.io import fits 5 | 6 | import sunpy.map 7 | from sunpy.map.sources.suvi import SUVIMap 8 | from sunpy.util.exceptions import SunpyUserWarning 9 | 10 | from sunkit_instruments import suvi 11 | 12 | # Test files are all remote data. 13 | pytestmark = pytest.mark.remote_data 14 | 15 | 16 | def test_read_suvi_l1b_nc(L1B_NC): 17 | l1b_nc_header, l1b_nc_data, l1b_nc_dqf = suvi.read_suvi(L1B_NC) 18 | assert isinstance(l1b_nc_header, fits.header.Header) 19 | assert l1b_nc_data.shape == (1280, 1280) 20 | assert l1b_nc_dqf.shape == (1280, 1280) 21 | 22 | 23 | def test_read_suvi_l1b_fits(L1B_FITS): 24 | l1b_fits_header, l1b_fits_data, l1b_fits_dqf = suvi.read_suvi(L1B_FITS) 25 | assert isinstance(l1b_fits_header, fits.header.Header) 26 | assert l1b_fits_data.shape == (1280, 1280) 27 | assert l1b_fits_dqf.shape == (1280, 1280) 28 | 29 | 30 | def test_read_suvi_l2_composite(L2_COMPOSITE): 31 | l2_header, l2_data, _ = suvi.read_suvi(L2_COMPOSITE) 32 | assert isinstance(l2_header, fits.header.Header) 33 | assert l2_data.shape == (1280, 1280) 34 | 35 | 36 | @pytest.mark.xfail 37 | def test_suvi_fix_l1b_header(L1B_FITS): 38 | header = suvi.io._fix_l1b_header(L1B_FITS) 39 | assert isinstance(header, fits.header.Header) 40 | 41 | 42 | def test_files_to_map_l1b_nc(L1B_NC): 43 | one = suvi.files_to_map(L1B_NC) 44 | collection = suvi.files_to_map([L1B_NC, L1B_NC, L1B_NC, L1B_NC]) 45 | collection_despike = suvi.files_to_map( 46 | [L1B_NC, L1B_NC, L1B_NC, L1B_NC], despike_l1b=True 47 | ) 48 | 49 | assert isinstance(one, sunpy.map.GenericMap) 50 | assert isinstance(collection, sunpy.map.MapSequence) 51 | assert isinstance(collection_despike, sunpy.map.MapSequence) 52 | np.testing.assert_equal(collection[0].data, one.data) 53 | assert not np.array_equal(collection[0].data, collection_despike[0].data) 54 | 55 | with pytest.warns(SunpyUserWarning, match="List of data/headers is empty."): 56 | suvi.files_to_map([L1B_NC, L1B_NC, L1B_NC, L1B_NC], only_short_exposures=True) 57 | 58 | 59 | @pytest.mark.xfail 60 | def test_files_to_map_l1b_fits(L1B_FITS): 61 | one = suvi.files_to_map(L1B_FITS) 62 | collection = suvi.files_to_map([L1B_FITS, L1B_FITS, L1B_FITS, L1B_FITS]) 63 | collection_despike = suvi.files_to_map( 64 | [L1B_FITS, L1B_FITS, L1B_FITS, L1B_FITS], despike_l1b=True 65 | ) 66 | 67 | assert isinstance(one, sunpy.map.GenericMap) 68 | assert isinstance(collection, sunpy.map.MapSequence) 69 | assert isinstance(collection_despike, sunpy.map.MapSequence) 70 | np.testing.assert_equal(collection[0].data, one.data) 71 | assert not np.array_equal(collection[0].data, collection_despike[0].data) 72 | 73 | with pytest.warns(SunpyUserWarning, match="List of data/headers is empty."): 74 | suvi.files_to_map( 75 | [L1B_FITS, L1B_FITS, L1B_FITS, L1B_FITS], only_short_exposures=True 76 | ) 77 | 78 | 79 | def test_files_to_map_nc(L1B_NC): 80 | l1b_nc_map = suvi.files_to_map(L1B_NC) 81 | assert isinstance(l1b_nc_map, SUVIMap) 82 | 83 | 84 | def test_files_to_map_fit(L1B_FITS): 85 | l1b_fits_map = suvi.files_to_map(L1B_FITS) 86 | assert isinstance(l1b_fits_map, SUVIMap) 87 | 88 | 89 | def test_files_to_map_l2_composite(L2_COMPOSITE): 90 | l2_map = suvi.files_to_map(L2_COMPOSITE) 91 | assert isinstance(l2_map, SUVIMap) 92 | -------------------------------------------------------------------------------- /sunkit_instruments/suvi/tests/test_suvi.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import numpy as np 4 | import pytest 5 | 6 | from sunkit_instruments import suvi 7 | 8 | 9 | @pytest.mark.remote_data 10 | def test_suvi_despiking_fits(L1B_FITS): 11 | _, l1b_fits_data, _ = suvi.read_suvi( 12 | L1B_FITS, 13 | ) 14 | despiked_l1b_fits_data = l1b_fits_data 15 | despiked_l1b_fits_data = suvi.despike_l1b_file(L1B_FITS) 16 | assert not np.array_equal(l1b_fits_data, despiked_l1b_fits_data) 17 | 18 | 19 | @pytest.mark.remote_data 20 | def test_suvi_despiking_nc(L1B_NC): 21 | _, l1b_nc_data, _ = suvi.read_suvi(L1B_NC) 22 | despiked_l1b_nc_data = l1b_nc_data 23 | despiked_l1b_nc_data = suvi.despike_l1b_file(L1B_NC) 24 | assert not np.array_equal(l1b_nc_data, despiked_l1b_nc_data) 25 | 26 | 27 | @pytest.mark.remote_data 28 | def test_get_response_nc(L1B_NC): 29 | l1b_nc_response = suvi.get_response(L1B_NC) 30 | assert l1b_nc_response["wavelength_channel"] == 171 31 | 32 | 33 | @pytest.mark.remote_data 34 | def test_get_response_fits(L1B_FITS): 35 | l1b_fits_response = suvi.get_response(L1B_FITS) 36 | assert l1b_fits_response["wavelength_channel"] == 171 37 | 38 | 39 | @pytest.mark.remote_data 40 | def test_get_response_wavelength(): 41 | response_195 = suvi.get_response(195) 42 | assert response_195["wavelength_channel"] == 195 43 | 44 | 45 | @pytest.mark.remote_data 46 | @pytest.mark.parametrize("spacecraft", [16, 17, 18, 19]) 47 | def test_get_response_spacecraft_number(spacecraft): 48 | response_195 = suvi.get_response(195, spacecraft=spacecraft) 49 | assert response_195["wavelength_channel"] == 195 50 | 51 | 52 | def test_get_response_bad_spacecraft_number(): 53 | with pytest.raises(ValueError, match=re.escape("Invalid spacecraft: 0 Valid spacecraft are: [16, 17, 18, 19]")): 54 | suvi.get_response(195, spacecraft=0) 55 | -------------------------------------------------------------------------------- /sunkit_instruments/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains package tests. 3 | """ 4 | -------------------------------------------------------------------------------- /sunkit_instruments/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module provides general utility functions. 3 | """ 4 | -------------------------------------------------------------------------------- /sunkit_instruments/version.py: -------------------------------------------------------------------------------- 1 | # NOTE: First try _dev.scm_version if it exists and setuptools_scm is installed 2 | # This file is not included in sunpy wheels/tarballs, so otherwise it will 3 | # fall back on the generated _version module. 4 | try: 5 | try: 6 | from ._dev.scm_version import version 7 | except ImportError: 8 | from ._version import version 9 | except Exception: 10 | import warnings 11 | 12 | warnings.warn( 13 | f'could not determine {__name__.split(".")[0]} package version; this indicates a broken installation' 14 | ) 15 | del warnings 16 | 17 | version = "0.0.0" 18 | 19 | 20 | # We use LooseVersion to define major, minor, micro, but ignore any suffixes. 21 | def split_version(version): 22 | pieces = [0, 0, 0] 23 | 24 | try: 25 | from distutils.version import LooseVersion 26 | 27 | for j, piece in enumerate(LooseVersion(version).version[:3]): 28 | pieces[j] = int(piece) 29 | 30 | except Exception: 31 | pass 32 | 33 | return pieces 34 | 35 | 36 | major, minor, bugfix = split_version(version) 37 | 38 | del split_version # clean up namespace. 39 | 40 | release = "dev" not in version 41 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | min_version = 4.0 3 | requires = 4 | tox-pypi-filter>=0.14 5 | envlist = 6 | py{310,311,312,313}{,-online} 7 | py313-devdeps 8 | py310-oldestdeps 9 | codestyle 10 | build_docs 11 | 12 | [testenv] 13 | pypi_filter = https://raw.githubusercontent.com/sunpy/sunpy/main/.test_package_pins.txt 14 | allowlist_externals = 15 | /bin/sh 16 | minimum_dependencies 17 | # Run the tests in a temporary directory to make sure that we don't import 18 | # the package from the source tree 19 | change_dir = .tmp/{envname} 20 | description = 21 | run tests 22 | devdeps: with the latest developer version of key dependencies 23 | oldestdeps: with the oldest supported version of key dependencies 24 | online: that require remote data (as well as the offline ones) 25 | pass_env = 26 | # A variable to tell tests we are on a CI system 27 | CI 28 | # Custom compiler locations (such as ccache) 29 | CC 30 | # Location of locales (needed by sphinx on some systems) 31 | LOCALE_ARCHIVE 32 | # If the user has set a LC override we should follow it 33 | LC_ALL 34 | setenv = 35 | MPLBACKEND = agg 36 | SUNPY_SAMPLEDIR = {env:SUNPY_SAMPLEDIR:{toxinidir}/.tox/sample_data/} 37 | devdeps,build_docs,online: HOME = {envtmpdir} 38 | PARFIVE_HIDE_PROGRESS = True 39 | devdeps: PIP_EXTRA_INDEX_URL = https://pypi.anaconda.org/astropy/simple https://pypi.anaconda.org/scientific-python-nightly-wheels/simple https://pypi.anaconda.org/liberfa/simple 40 | deps = 41 | devdeps: git+https://github.com/sunpy/sunpy 42 | # Handle minimum dependencies via minimum_dependencies 43 | oldestdeps: minimum_dependencies 44 | online: pytest-rerunfailures 45 | extras = 46 | all 47 | tests 48 | commands_pre = 49 | oldestdeps: minimum_dependencies sunkit_instruments --filename requirements-min.txt 50 | oldestdeps: pip install -r requirements-min.txt 51 | pip freeze --all --no-input 52 | commands = 53 | # To amend the pytest command for different factors you can add a line 54 | # which starts with a factor like `online: --remote-data=any \` 55 | # If you have no factors which require different commands this is all you need: 56 | pytest \ 57 | -vvv \ 58 | -r fEs \ 59 | --pyargs sunkit_instruments \ 60 | --cov-report=xml \ 61 | --cov=sunkit_instruments \ 62 | --cov-config={toxinidir}/.coveragerc \ 63 | {toxinidir}/docs \ 64 | online: --remote-data=any \ 65 | online: --reruns 2 --reruns-delay 15 \ 66 | {posargs} 67 | 68 | [testenv:codestyle] 69 | pypi_filter = 70 | skip_install = true 71 | description = Run all style and file checks with pre-commit 72 | deps = 73 | pre-commit 74 | commands = 75 | pre-commit install-hooks 76 | pre-commit run --color always --all-files --show-diff-on-failure 77 | 78 | [testenv:build_docs] 79 | usedevelop = true 80 | change_dir = docs 81 | description = Invoke sphinx-build to build the HTML docs 82 | extras = 83 | all 84 | docs 85 | commands = 86 | sphinx-build -j auto --color -W --keep-going -b html -d _build/.doctrees . _build/html {posargs} 87 | python -c 'import pathlib; print("Documentation available under file://\{0\}".format(pathlib.Path(r"{toxinidir}") / "docs" / "_build" / "index.html"))' 88 | --------------------------------------------------------------------------------